diff --git a/.changeset/calm-services-start.md b/.changeset/calm-services-start.md new file mode 100644 index 0000000000..94b57738d8 --- /dev/null +++ b/.changeset/calm-services-start.md @@ -0,0 +1,7 @@ +--- +"@opencode-ai/client": patch +"@opencode-ai/protocol": patch +"@opencode-ai/cli": patch +--- + +Expose background-service lifecycle status, preserve one process-held owner through startup and failure, reconnect TUIs without activating replacement, and stop exact service instances gracefully. diff --git a/.changeset/calm-sessions-header.md b/.changeset/calm-sessions-header.md new file mode 100644 index 0000000000..666393c22e --- /dev/null +++ b/.changeset/calm-sessions-header.md @@ -0,0 +1,5 @@ +--- +"@opencode-ai/cli": patch +--- + +Expose a TUI plugin slot at the top of the session view. diff --git a/.changeset/clean-sessions-generate.md b/.changeset/clean-sessions-generate.md new file mode 100644 index 0000000000..c7f0d2c909 --- /dev/null +++ b/.changeset/clean-sessions-generate.md @@ -0,0 +1,7 @@ +--- +"@opencode-ai/client": patch +"@opencode-ai/plugin": patch +"@opencode-ai/protocol": patch +--- + +Expose transient, read-only session generation through the HTTP API, generated clients, and V2 plugin session context. diff --git a/.changeset/fresh-composers-slot.md b/.changeset/fresh-composers-slot.md new file mode 100644 index 0000000000..9c6c47b3c6 --- /dev/null +++ b/.changeset/fresh-composers-slot.md @@ -0,0 +1,5 @@ +--- +"@opencode-ai/cli": patch +--- + +Expose a TUI plugin slot above the session composer. diff --git a/.gitattributes b/.gitattributes index 18177b31a5..27a99603b2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ packages/core/migration/**/snapshot.json linguist-generated packages/core/src/database/migration.gen.ts linguist-generated +packages/core/src/**/*.txt text eol=lf diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1c41a66faa..7498bc24c9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,6 +6,7 @@ on: branches: - ci - dev + - v2 - beta - fix/npm-native-binary-install - snapshot-* @@ -31,6 +32,9 @@ permissions: contents: write packages: write +env: + OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && 'next') || '' }} + jobs: version: runs-on: blacksmith-4vcpu-ubuntu-2404 @@ -117,12 +121,61 @@ jobs: outputs: version: ${{ needs.version.outputs.version }} + build-node-cli: + needs: version + if: github.repository == 'anomalyco/opencode' + strategy: + fail-fast: false + matrix: + settings: + - target: linux-arm64 + host: blacksmith-4vcpu-ubuntu-2404-arm + - target: linux-x64 + host: blacksmith-4vcpu-ubuntu-2404 + - target: darwin-arm64 + host: macos-26 + - target: windows-arm64 + host: blacksmith-4vcpu-windows-2025 + - target: windows-x64 + host: blacksmith-4vcpu-windows-2025 + runs-on: ${{ matrix.settings.host }} + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + + - uses: ./.github/actions/setup-bun + with: + install-flags: --os=* --cpu=* + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "26.4.0" + + - name: Build + run: bun packages/cli/script/build-node.ts --target=${{ matrix.settings.target }} --skip-install --outdir=dist/node + env: + OPENCODE_VERSION: ${{ needs.version.outputs.version }} + OPENCODE_RELEASE: ${{ needs.version.outputs.release }} + + - name: Verify service lifecycle + if: matrix.settings.target != 'windows-arm64' + working-directory: packages/cli + run: bun run script/service-smoke.ts --node + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: opencode-node-cli-${{ matrix.settings.target }} + path: packages/cli/dist/node/cli-node-* + if-no-files-found: error + sign-cli-windows: needs: - build-cli - version runs-on: blacksmith-4vcpu-windows-2025 - if: github.repository == 'anomalyco/opencode' + if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' env: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} @@ -219,8 +272,9 @@ jobs: build-electron: needs: + - build-cli - version - if: github.repository == 'anomalyco/opencode' + if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' continue-on-error: false env: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} @@ -315,7 +369,10 @@ jobs: env: OPENCODE_VERSION: ${{ needs.version.outputs.version }} OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} + OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }} RUST_TARGET: ${{ matrix.settings.target }} + GH_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} - name: Build run: bun run build @@ -339,7 +396,8 @@ jobs: env: OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} GH_TOKEN: ${{ steps.committer.outputs.token }} - CSC_KEYCHAIN: build.keychain + CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} + CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_API_KEY: ${{ runner.temp }}/apple-api-key.p8 APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} @@ -404,6 +462,7 @@ jobs: needs: - version - build-cli + - build-node-cli - sign-cli-windows - build-electron if: always() && !failure() && !cancelled() @@ -442,6 +501,7 @@ jobs: path: packages/opencode/dist - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + if: github.ref_name != 'v2' with: name: opencode-cli-signed-windows path: packages/opencode/dist @@ -451,6 +511,12 @@ jobs: name: opencode-preview-cli path: packages/cli/dist + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: opencode-node-cli-* + path: packages/cli/dist/node + merge-multiple: true + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 if: needs.version.outputs.release with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c69de1d93b..b486b68a93 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,6 +4,7 @@ on: push: branches: - dev + - v2 pull_request: workflow_dispatch: @@ -69,18 +70,36 @@ jobs: env: OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }} + - name: Verify compiled service lifecycle + if: always() + timeout-minutes: 10 + working-directory: packages/cli + run: | + bun run script/build.ts --single --skip-install + bun run script/service-smoke.ts + + - name: Setup Node build runtime + if: always() + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "26.4.0" + + - name: Verify Node build + if: always() + timeout-minutes: 15 + working-directory: packages/cli + run: | + bun run script/build-node.ts --single --skip-install --outdir=dist/node + bun run script/service-smoke.ts --node + - name: Check generated client if: runner.os == 'Linux' working-directory: packages/client run: bun run check:generated - - name: Run HttpApi exerciser gates - if: runner.os == 'Linux' - working-directory: packages/opencode - run: bun run test:httpapi - e2e: name: e2e (${{ matrix.settings.name }}) + if: github.ref_name != 'v2' && github.head_ref != 'v2' strategy: fail-fast: false matrix: diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index fc9a52797c..5c83a8e691 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -2,9 +2,9 @@ name: typecheck on: push: - branches: [dev] + branches: [dev, v2] pull_request: - branches: [dev] + branches: [dev, v2] workflow_dispatch: jobs: diff --git a/.gitignore b/.gitignore index 006cab8c27..183888091c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,10 @@ node_modules playground tmp dist +dist-node ts-dist .turbo +.typecheck-profiles **/.serena .serena/ **/.omo @@ -24,6 +26,7 @@ Session.vim a.out target .scripts +.cache .direnv/ # Local dev files diff --git a/.opencode/command/translate.md b/.opencode/command/translate.md index de18ae2ee8..8d493f4a81 100644 --- a/.opencode/command/translate.md +++ b/.opencode/command/translate.md @@ -1,6 +1,6 @@ --- description: translate English to other languages -model: opencode/gpt-5.6-sol +model: opencode/claude-opus-4-8 --- run git diff and translate changed english doc and UI copy files to other international languages. Translate all languages in parallel to save time. diff --git a/.opencode/skills/opencode-drive/SKILL.md b/.opencode/skills/opencode-drive/SKILL.md new file mode 100644 index 0000000000..6633526b10 --- /dev/null +++ b/.opencode/skills/opencode-drive/SKILL.md @@ -0,0 +1,254 @@ +--- +name: opencode-drive +description: Use when an agent needs drive OpenCode via a script or interact with an isolated instance +--- + +# OpenCode Drive + +Use `opencode-drive` to launch an isolated OpenCode instance and control it via commands or a script. + +There are two modes. Always default to using a script unless specifically directed to be interactive (connect +to an existing running instance, or start a new one, and make a few changes to the UI and read it, and iterate +on changes). + +Scripts allow you to run a full walkthrough in one run. When the script is done opencode-drive exits, +stops all processes, and cleans up all artifacts. + +# Prepare The Environment + +Use `init` when files must be added to the isolated home or project before OpenCode starts. It prints the artifact directory without launching OpenCode. A later `start` with the same name reuses it. + +```bash +artifacts=$(opencode-drive init --name demo) +cp -R ./fixtures/home/. "$artifacts/" +cp -R ./fixtures/project/. "$artifacts/files/" +opencode-drive start --name demo --dev ~/projects/opencode +``` + +The simulated project is under `$artifacts/files`. Running `start` without a prior `init` initializes the artifacts automatically. + +# Scripted usage + +You can write scripts that walk through entire flows, and gives you full access to controlling +the backend too. See examples of the script API at the bottom of this file. + +After creating or editing a script, always typecheck it before running. Never skip this step: + +```bash +opencode-drive check ./reproduce-stale-exploring-empty.ts +``` + +Run it by passing `--script` to start: + +```bash +opencode-drive start --name auto-stop-reproduction --script ./reproduce-stale-exploring-empty.ts +``` + +It will output information about the run, including paths to log files which you can read +to inspect what happened. If you need to dig into failures that aren't clear, read those log +files. If the script is unsuccessful, automatically fix the script and run it again. + +Scripts use one typed definition object. `setup` runs before OpenCode starts, +and `fs.writeFile` always writes inside the simulated project. + +You can read the full typed API here: https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/src/script/types.ts + +```ts +import { defineScript } from "opencode-drive" + +export default defineScript({ + async setup({ fs, config }) { + config.autoupdate = false + await fs.writeFile("src/example.ts", "export const value = 1\n") + }, + + async run({ ui, llm }) { + await ui.submit("Open src/example.ts") + await llm.send(llm.text("The file exports `value`.")) + await ui.waitFor("The file exports `value`.") + }, +}) +``` + +`setup` receives the current OpenCode config object, which starts from the +default drive config unless the prepared instance already has one. When a script +needs custom config, mutate this `config` parameter instead of generating and +writing a new config object from scratch, so the script keeps the default +provider/model settings unless it intentionally changes them. + +Note that the simulated model is a GPT model type, and opencode uses the `patch` tool for working with files Do not use a `edit` or `write` tool to edit files. + +Use `launch: "manual"` when the script needs to launch the server and every TUI +itself (this is extremely rare, do not use this unless explicitly asked). In this +mode `ui` is typed as `null`; call `server.launch()` exactly +once before launching clients. Each `clients.launch(name)` result provides the +same UI methods as the automatic client. You can see an example of this API +here: https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/examples/multiple-clients.ts + +Use the exported `wait(milliseconds)` utility for an unconditional delay. + +`await llm.send(...)` waits for the next request and resolves after OpenCode +acknowledges its complete response. `llm.queue(...)` declares responses in +advance. Chunks may be built with `text`, `reasoning`, `toolCall`, `raw`, +`finish`, and `disconnect`. A normal response receives `finish("stop")` +automatically unless it yields or queues an explicit terminal event. + +`llm.text(text, { delay, chunkSize })` defaults to a 2 ms delay and a +15-character target varied by plus or minus 5 per chunk. + +`llm.reasoning` accepts the same options, and `llm.pause(milliseconds)` adds a +delay between any two outputs. + +Use `llm.serve` for an ongoing typed response generator: + +```ts +llm.serve(async function* (request, index) { + yield llm.reasoning(`Handling request ${index + 1}`) + yield llm.text(`Received ${request.id}`) + yield llm.finish("stop") +}) +``` + +The backend connection, response cleanup, cancellation, and recording +completion are automatic. + +You can see some example scripts here: + +- https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/examples/simple.ts +- https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/examples/serve.ts + +## Prune + +- `prune` removes artifact directories. These are always cleaned up after running a script + successfully, but leftover on failed runs. Always call this if a script fails. + +```bash +opencode-drive prune --name demo + +// --force cleans up all artifcat directories +opencode-dirve prune --force +``` + +# Live interaction usage + +- Always give headless instances a unique `--name`. Visible instances may omit it. +- A normal headless `start` detaches automatically and returns after the instance is ready. +- Do not add `&`; the long-running owner already runs in the background. +- Configure simulated model responses after startup when needed. +- Send ordered UI commands with `send`. +- Always stop the instance when finished. + +```bash +opencode-drive start --name demo + +opencode-drive send --name demo \ + --command.ui.type '{"text":"Explain this project"}' \ + --command.ui.enter + +opencode-drive stop --name demo +``` + +## Send UI Commands + +- Every `send` opens a connection to the named instance, runs its commands in order, and exits. +- Combine typing and Enter in one command when submitting a prompt. +- JSON-valued commands require one JSON argument. +- Multiple command flags execute from left to right. + +Commands: + +- `--command.ui.type ` types into the focused editor. Arguments: `text` string. +- `--command.ui.press ` presses a key. Arguments: `key` string; optional `modifiers` object with boolean `ctrl`, `shift`, `meta`, `super`, or `hyper`. +- `--command.ui.enter` presses Enter. Arguments: none. +- `--command.ui.arrow ` presses an arrow key. Arguments: `direction` is `up`, `down`, `left`, or `right`. +- `--command.ui.focus ` focuses an element. Arguments: `target` is the numeric element `num` returned by `ui.state`. +- `--command.ui.click ` clicks an element. Arguments: numeric `target`, `x`, and `y`; use the element `num` returned by `ui.state` as `target`. +- `--command.ui.state` prints focus and interactive element metadata as JSON. Arguments: none. +- `--command.ui.matches ` prints whether literal, case-sensitive text appears on screen. Arguments: `text` string. + +```bash +opencode-drive send --name demo \ + --command.ui.type '{"text":"Find the relevant code and explain it"}' \ + --command.ui.enter + +opencode-drive send --name demo \ + --command.ui.press '{"key":"p","modifiers":{"ctrl":true}}' + +opencode-drive send --name demo \ + --command.ui.arrow '{"direction":"down"}' + +opencode-drive send --name demo \ + --command.ui.focus '{"target":12}' + +opencode-drive send --name demo \ + --command.ui.click '{"target":12,"x":4,"y":1}' + +opencode-drive send --name demo \ + --command.ui.matches '{"text":"OpenCode"}' +``` + +To read the UI state and see information about interactable elements, use the `ui.state` command: + +```bash +opencode-drive send --name demo --command.ui.state +``` + +## Configure LLM Responses + +- `responses` controls what the LLM responds with +- Only use this if you are wanting to reproduce an exact type of response +- Defaults are `text,reasoning,diff,tool` with `write,apply_patch`. +- Supported types are `text`, `reasoning`, `diff`, and `tool`. +- `--tools` limits generated tool calls to names offered by OpenCode. + +```bash +opencode-drive responses --name demo \ + --types text,reasoning,diff,tool \ + --tools write,apply_patch + +opencode-drive responses --name demo \ + --types tool \ + --tools read,glob,grep +``` + +## Inspect The UI + +- `ui.state` prints focus and interactive element metadata as JSON. +- `ui.matches` checks for literal, case-sensitive screen text. +- `screenshot` prints the generated image path. + +```bash +opencode-drive screenshot --name demo +``` + +## Lifecycle + +- `stop` waits for recording export and owner cleanup before returning. + +```bash +opencode-drive stop --name demo +``` + +# Record The UI + +- Start with `--record` to capture a headless instance from its first rendered frame. +- `stop` finishes the recording, exports an MP4, and prints its path. + +```bash +opencode-drive start --name demo --record + +opencode-drive send --name demo \ + --command.ui.type '{"text":"Show me the current architecture"}' \ + --command.ui.enter + +opencode-drive stop --name demo +``` + +# Artifacts dir + +- `dir` prints the artifact directory for the instance. + +```bash +opencode-drive dir --name demo +``` + diff --git a/.opencode/skills/sample-skill/SKILL.md b/.opencode/skills/sample-skill/SKILL.md new file mode 100644 index 0000000000..87b9e96907 --- /dev/null +++ b/.opencode/skills/sample-skill/SKILL.md @@ -0,0 +1,31 @@ +--- +name: sample-skill +description: Use when the user says sample skill, skill demo, or asks how an opencode SKILL.md should be structured; demonstrates a tiny project-local skill with practical assistant workflow guidance. +--- + +# Sample Skill + +This is a minimal project-local opencode skill. It exists as a reference for how a skill is structured and as a tiny reusable workflow the assistant can load when the user asks for a skill example. + +## When To Use + +- Use when the user asks for a sample skill or skill template. +- Use when demonstrating the required `SKILL.md` frontmatter and body format. +- Do not use for unrelated coding tasks just because a skill exists. + +## Workflow + +- Confirm the specific outcome the user wants if the request is ambiguous. +- Inspect the relevant files before changing anything. +- Make the smallest correct change. +- Verify the result with a focused read, typecheck, test, or other lightweight check when available. +- Summarize the changed files and any required restart or reload step. + +## Example Response Style + +When this skill is relevant, keep responses direct and actionable: + +```text +I created a project-local skill at .opencode/skills/sample-skill/SKILL.md. +Restart opencode for the new skill to be discovered by future sessions. +``` diff --git a/AGENTS.md b/AGENTS.md index cd2327e888..14f9f27f14 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,7 @@ - To regenerate the legacy JavaScript SDK, run `./packages/sdk/js/script/build.ts`. - After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`. Do not edit `src/generated` or `src/generated-effect` directly. - Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk-next` composes Client, Core, and Server. +- Do not modify `packages/opencode` unless the user explicitly asks for V1 work. `packages/opencode` is the V1 implementation and is present for reference only. New implementation changes should land in the V2 package set: `packages/core`, `packages/cli`, `packages/server`, `packages/protocol`, `packages/schema`, and related generated client surfaces when required. - The default branch in this repo is `dev`. - Local `main` ref may not exist; use `dev` or `origin/dev` for diffs. @@ -24,6 +25,7 @@ Examples: `fix(tui): simplify thinking toggle styling`, `docs: update contributi - Keep things in one function unless composable or reusable - Do not extract single-use helpers preemptively. Inline the logic at the call site unless the helper is reused, hides a genuinely complex boundary, or has a clear independent name that improves the caller. +- Before adding complexity for a speculative or vanishingly unlikely race or security edge case, explain the concrete failure mode, likelihood, and complexity cost to the user and get their buy-in. Do not silently expand scope for theoretical robustness. - Avoid `try`/`catch` where possible - Avoid using the `any` type - Use Bun APIs when possible, like `Bun.file()` @@ -150,12 +152,15 @@ const table = sqliteTable("session", { ## 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. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; idle or missing interruption is a no-op. +- Keep durable events minimal: record irreducible new facts and do not repeat state derivable by folding the ordered aggregate history. Enrich projections and read models with previous or derived state when consumers need self-contained views. +- Keep durable prompt admission separate from model execution. `SessionV2.prompt(...)` admits one durable `session_pending` 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, consuming the pending row in the same event transaction; `session_pending` stores only unconsumed work. +- 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. Retry of an already-promoted input reconciles against the projected message and the durable admitted event rather than a retained row. +- Keep `SessionExecution` process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; interruption of a known but idle or locally unowned Session is a no-op, while the public API rejects an unknown Session. - 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. +- Preserve one explicit `llm.stream(request)` call per Physical Attempt and reload projected history before durable continuation. Most Steps have one Physical Attempt; overflow-triggered compaction recovery may rebuild one Step for a second attempt. 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 continuation recovery requires a separate explicit design before it may retry provider work. A drain has no durable identity or transcript boundary. -- Keep delivery vocabulary explicit. Prompts steer by default and promote at the next safe provider-turn boundary while the current drain requires continuation. An explicit `queue` input remains pending until the Session would otherwise become idle; promote one queued input at that boundary, then reevaluate continuation before promoting another. Promoting any new user input resets the selected agent's provider-turn allowance; a batch of steers resets it once. +- Keep delivery vocabulary explicit. Prompts steer by default and promote at the next safe step boundary while the current drain requires continuation. An explicit `queue` input remains pending until the Session would otherwise become idle; promote one queued input at that boundary, then reevaluate continuation before promoting another. Promoting any new user input resets the selected agent's step allowance; a batch of steers resets it once. +- One step is one logical LLM call; its durable record covers only the model-visible span. Do not write "provider turn", and do not use bare "turn" for a single call: "turn" is reserved for the future assistant-turn unit containing all steps from prompt promotion until the session would go idle. - Keep EventV2 replay owner claims separate from clustered Session execution ownership. -- Keep the System Context algebra, registry, and built-ins in `src/system-context`; keep Context Source producers with their observed domains, and keep Session History selection plus Context Epoch persistence Session-owned. +- Keep the Instructions algebra and built-ins in `src/instructions`; keep instruction producers with their observed domains, and keep Session History selection plus `InstructionState` and `InstructionEntry` persistence Session-owned. `InstructionDiscovery` observes ambient global and upward-project instructions. The runner composes built-ins, discovery, guidance, and entries explicitly in `loadInstructions`; there is no instruction registry. +- `session.instructions.updated` stores only changed source keys and content hashes. Blob values live once in `instruction_blob`; `instruction_state` is a rebuildable fold cache, never primary state. Render initial instructions and chronological updates from values during request assembly. Completed compaction moves the instruction epoch; Session movement and committed revert clear it. Unavailable sources retain the last value and block only the initial complete delta. diff --git a/CONTEXT.md b/CONTEXT.md deleted file mode 100644 index 5e5955d344..0000000000 --- a/CONTEXT.md +++ /dev/null @@ -1,225 +0,0 @@ -# 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 - -**Session History**: -The projected chronological conversation selected for a provider turn after applying the active compaction and **Context Epoch** cutoffs. -_Avoid_: Session Context - -**Context Source**: -One independently observed typed value within the **System Context**, represented by a stable key, JSON codec, infallible loader, pure baseline/update renderers, and an optional removal renderer for dynamic sources. -_Avoid_: Prompt fragment - -**System Context Registry**: -The Location-scoped registry of ordered, scoped producers that contribute to the current **System Context**. - -**Mid-Conversation System Message**: -A durable chronological instruction that tells the model the newly effective state of a changed **Context Source**. -_Avoid_: System update, system notification, raw text diff - -**Context Epoch**: -The span during which one initially rendered **System Context** remains the immutable provider-cache baseline, ending at completed compaction, Session movement, or an incompatible context transition that requires a fresh baseline. - -**Baseline System Context**: -The full **System Context** rendered at the start of a **Context Epoch**. -_Avoid_: Live system prompt - -**Context Snapshot**: -The overwriteable model-hidden JSON state used to compare each **Context Source** with the value last admitted to a provider turn. - -**Unavailable Context**: -An expected temporary inability to observe a **Context Source** 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. - -**Admitted Prompt**: -A durable user input accepted into the Session inbox but not yet included in **Session History**. - -**Prompt Promotion**: -The durable transition that removes an **Admitted Prompt** from pending input and appends its user message to **Session History**. - -**Provider Turn**: -One request to a model provider and the response projected from that request. - -**Session Drain**: -One process-local execution span that promotes eligible input and runs required **Provider Turns** until no immediate continuation remains. A Session Drain has no durable identity or transcript boundary. - -**Model Tool Output**: -The bounded projection of a Core-executed tool result persisted in Session history and replayed to the model. A tool may shape this projection semantically, but the Tool Registry enforces the final size limit. - -**Managed Tool Output File**: -A temporary file created under OpenCode's shared tool-output directory to retain complete output that was too large for Session history. - -**Model Request Options**: -Provider-semantic model settings selected from the Catalog and active Session variant before the LLM protocol adapter encodes them for a provider request. -_Avoid_: Request body, wire options - -**Generation Controls**: -Provider-neutral sampling and output controls, partitioned from provider semantics and compatibility wire fields when model metadata enters the Catalog. - -**Native Continuation Metadata**: -Opaque protocol-shaped data attached to assistant content and required to continue that content natively with a compatible model, such as a reasoning signature or provider-hosted item identifier. - -**PTY Environment**: -The host-supplied environment overlay applied by the server when creating a PTY, observed for the request Location and resolved PTY working directory. - -**OpenCode Client**: -The generated Promise and Effect APIs derived from the public `HttpApi`; **Embedded OpenCode** shares the Effect API through an in-memory `HttpClient` against the same router and handlers. -_Avoid_: Remote client - -**SDK Contract IR**: -The runtime-neutral compiled representation of the authoritative `HttpApi`, preserving encoded and decoded type projections plus transport metadata so independent SDK emitters can choose their public value model and runtime interpreter. - -**Embedded OpenCode**: -A scoped in-process host that structurally extends the **OpenCode Client**, supplies an in-memory HTTP transport, and exposes additional same-process capabilities directly. -_Avoid_: Local implementation - -**Page**: -A bounded ordered result containing `items` and opaque `previous` and `next` cursor links for navigating the same query in either direction. -_Avoid_: Response envelope - -## Relationships - -- A **System Context** is an opaque carrier composed from zero or more **Context Sources**. -- **Session History** contains projected conversational messages and admitted **Mid-Conversation System Messages**; the active **Baseline System Context** remains separate provider-request state. -- The **System Context Registry** uses stable-keyed scoped contributions to assemble the current **System Context**; contributor removal naturally removes its sources at the next **Safe Provider-Turn Boundary**. -- A changed **Context Source** may produce one **Mid-Conversation System Message** containing its newly effective state. -- A **Mid-Conversation System Message** persists the exact combined rendered text sent to the model. -- The current **Context Snapshot** advances atomically with the corresponding durable **Mid-Conversation System Message**. -- A **Context Snapshot** stores one codec-encoded JSON value and, for removable dynamic sources, a pre-rendered removal message per stable **Context Source** key. -- Changes from multiple **Context Sources** 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**. -- An **Admitted Prompt** is replayable pending input, not yet model-visible **Session History**. -- **Prompt Promotion** atomically consumes the pending inbox entry and appends its model-visible user message. -- Steering prompts promote at the next **Safe Provider-Turn Boundary** while the current **Session Drain** still requires continuation. Promoting any newly admitted user input resets the selected agent's provider-turn allowance; multiple prompts promoted at one boundary reset it once. -- A queued prompt does not promote while the current **Session Drain** requires continuation. The runner promotes one queued prompt when the Session would otherwise become idle, then reevaluates continuation before promoting another. -- A **Session Drain** is process-local coordination rather than a durable domain entity. Durable recovery must reason from prompts, projected history, provider attempts, and tool state rather than inventing an enclosing execution identity. -- The first provider turn renders the latest complete **Baseline System Context** and initializes its **Context Snapshot** without emitting a redundant **Mid-Conversation System Message**; unavailable initial context blocks the turn instead of persisting an incomplete baseline. -- Initial **System Context** preparation precedes the first durable input promotion so an unavailable baseline leaves that input pending and retryable; ordinary reconciliation remains after promotion. -- Compaction starts a new **Context Epoch** with a freshly rendered **Baseline System Context** and **Context Snapshot**; prior **Mid-Conversation System Messages** remain durable audit history but leave projected model history. -- A newly registered core or plugin-defined **Context Source** absent from the current snapshot emits its baseline rendering once at the next **Safe Provider-Turn Boundary**. -- **Context Source** keys are stable and namespaced; duplicate keys fail composition. `SystemContext.combine(...)` preserves caller order; the **System Context Registry** evaluates producers concurrently and combines them in stable contribution-key order so rendered context remains deterministic. -- Each **Context Source** loader returns one coherent typed value. `SystemContext.make(...)` hides that value type so differently typed sources compose uniformly. Its codec compares and stores that value; its pure renderers produce model-visible baseline, update, and removal text only when needed. -- `SystemContext.initialize(...)` observes a composed **System Context** once and produces a fresh **Baseline System Context** with its **Context Snapshot**. -- `SystemContext.reconcile(...)` observes a composed **System Context** once and returns exactly one next action: unchanged, updated, replacement ready, or replacement blocked. -- `SystemContext.replace(...)` renders a fresh generation after completed compaction or another baseline-replacing transition; it reports replacement blocked while previously admitted context is unavailable. -- **Unavailable Context** uses stale-while-revalidate semantics and is distinct from a successfully loaded absence, which may emit removal text. -- Ordinary **Context Source** loaders return values directly; loaders that intentionally use stale-while-revalidate may explicitly return **Unavailable Context**. -- Nested project instruction discovery after successful reads remains a follow-up; when implemented, discovered instructions must be admitted durably at the next **Safe Provider-Turn Boundary**. -- Location-scoped services naturally re-resolve effective context when a moved session next runs in its destination location. -- Moving a Session clears its active **Context Epoch**, so the destination must initialize a complete baseline before another prompt can promote. -- 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. -- The first instruction-service slice observes global and upward project `AGENTS.md` files as one ordered aggregate **Context Source** at each **Safe Provider-Turn Boundary**. -- Built-in and instruction context producers register through the **System Context Registry** with stable contribution keys. Plugin-defined context registration and hot-reload lifecycle remain a follow-up built on the same scoped registry seam. -- Selected-agent available-skill guidance is a **Context Source** composed with Location-wide registry sources immediately before Context Epoch admission. It lists only names and descriptions permitted for that agent; skill bodies and locations are exposed only through the permission-checked `skill` tool. -- The selected agent and model are sampled when a provider turn starts. Changes admitted after that boundary apply to the next provider turn and do not restart the current turn. -- Selected-agent available-skill guidance remains a **Context Source**. An agent switch that changes that guidance produces a **Mid-Conversation System Message** while preserving the current baseline. -- Local tool authorization and pending permission requests retain the effective agent of the provider turn that issued the call; a later agent switch cannot change that call's policy. -- 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 Session-message history; normal user-facing transcript surfaces may hide them. -- The date **Context Source** 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 the exact joined text used for the active provider-cache prefix. -- Completed compaction starts a new **Context Epoch** on the next provider attempt, folding the current complete **System Context** into a fresh baseline and removing earlier **Mid-Conversation System Messages** from active model history. -- A model/provider switch preserves the current **Context Epoch** and chronological conversation history; the new selection applies to the next provider turn. -- **Native Continuation Metadata** remains in durable history. Provider-turn projection includes it only for a successful exact originating provider/model match; failed turns and incompatible models omit opaque metadata, while non-empty visible reasoning lowers to ordinary assistant text after a model switch. This conservative relation may widen only when recorded provider tests establish compatibility. -- **Model Request Options** remain provider-semantic through Catalog resolution. The Session runner maps them into the LLM package's provider-option namespace; the selected protocol adapter alone owns provider wire encoding. -- **Generation Controls**, protocol-semantic **Model Request Options**, and compatibility request body fields are separate Catalog domains. A shared ingestion adapter partitions legacy and models.dev AI-SDK-shaped options before routing. -- The **PTY Environment** is a server concern rather than a Core PTY concern. PTY creation merges caller values, then the host overlay, then Core-forced terminal invariants such as `TERM` and `OPENCODE_TERMINAL`. -- Networked and **Embedded OpenCode** use the same **OpenCode Client** and preserve the full HTTP encoding, routing, middleware, and decoding boundary; only the `HttpClient` transport differs. -- The Effect-native network constructor obtains `HttpClient.HttpClient` from its environment so callers own transport selection, recording, tracing, retries, and tests. Convenience runtimes may provide a fetch transport separately. -- Creating **Embedded OpenCode** is scoped. Closing its owning Scope releases the in-process server resources, database resources, registrations, and fibers. -- **Embedded OpenCode** exposes shared client capabilities and embedded-only capabilities on one object; consumers do not navigate through a nested `.client` property. -- The beta **OpenCode Client** currently uses plural consumer-facing capability groups such as `sessions`; whether the stable Session namespace should instead be singular `session` must be settled before stabilization. Internal server identifiers do not implicitly define public client names. -- Server's concrete `HttpApi` is authoritative for shared **OpenCode Client** capabilities. Codegen compiles its Session group directly; the Effect runtime uses an equivalent Protocol-only projection so generated artifacts remain independent of Core and Server. -- SDK generation reflects the public `HttpApi` once into an **SDK Contract IR**. Promise and Effect emitters share endpoint structure and transport metadata without being required to expose identical public values: an emitter may select encoded wire types, decoded domain types, compile-time brands, runtime validation, and its own execution abstraction independently. -- The first Effect emitter is the rich projection: it exposes decoded Effect-native values, preserves brands and schema transformations, performs runtime schema decoding, and delegates transport interpretation to `HttpApiClient`. Lighter wire-shaped Effect output remains possible through another emitter policy rather than constraining the shared IR. -- The rich Effect emitter regenerates private executable schemas when the **SDK Contract IR** proves that their transport semantics can be reproduced exactly. Contracts with authoritative custom transformations use the import-based Effect emitter against a Protocol-only client projection whose generated transport output is tested against Server's concrete API; the Promise emitter still derives zero-Effect structural wire types from the same IR. -- `@opencode-ai/protocol` owns Session endpoint construction and middleware placement. Server supplies concrete middleware keys to produce the authoritative build-time API; the client projection supplies transport-only keys without importing Core or Server at runtime. -- The first Promise emitter targets the same clean domain-oriented method organization rather than Hey API source compatibility. It returns unwrapped values directly, rejects declared and infrastructure failures, and begins with minimal client-level transport configuration; result wrappers, interceptors, and legacy generated signatures are outside the initial surface. -- The first Promise emitter parses response syntax and trusts its generated structural types; it does not perform runtime structural validation. Malformed payload syntax fails, while a syntactically valid shape mismatch is not detected at the SDK boundary. Standalone validator generation remains an optional future emitter policy. -- Declared Promise-client failures retain their tagged structural wire values and have generated type guards. Consumers do not depend on generated `Error` subclass identity, preserving discrimination across package copies and realms while remaining structurally aligned with Effect domain errors. -- Promise-client infrastructure failures use one generated `ClientError` class with a structured reason such as transport failure, unexpected status, unsupported content type, or malformed response. Promise methods reject with either a tagged declared domain failure or `ClientError`, matching the Effect client's conceptual domain/infrastructure error division. -- Promise methods accept a separate optional per-call transport-options argument containing `AbortSignal` and header overrides. Cancellation and transport metadata do not enter the domain input object; broader interceptor and response-mode APIs remain deferred. -- Promise streaming methods return a lazy `AsyncIterable` directly rather than a Promise-wrapped stream object. Iteration opens the connection, `AbortSignal` cancels it, and ending iteration closes the underlying request; the Effect emitter analogously returns `Stream` directly. -- Promise SSE connection establishment, declared HTTP failures, and infrastructure failures occur during `AsyncIterable` iteration, beginning with its first `next()` call, rather than during synchronous method construction. -- Neither generated streaming runtime automatically reconnects after disconnection. Promise `AsyncIterable` and Effect `Stream` fail explicitly; live consumers refresh and resubscribe, while durable sequence-based resume remains explicit composition above the generated client. -- Promise client construction is synchronous and network-free. It requires `baseUrl`, defaults to `globalThis.fetch`, accepts client-level headers, and merges them with per-call header overrides. -- Effect client construction accepts an explicit `baseUrl` and obtains `HttpClient.HttpClient` from the Effect environment. It does not install fetch or duplicate per-call transport policy; callers transform/provide the client for headers, tracing, retries, recording, and tests, while fiber interruption owns cancellation. -- Promise and Effect emitters each own their generated public type modules. The **SDK Contract IR**, not a physically shared generated type package, is the common source; this permits zero-Effect wire types and rich decoded Effect types to evolve independently. -- Promise and Effect network clients ship from `@opencode-ai/client` behind isolated root and `/effect` exports. The root has no runtime path to Effect; `/effect` imports only Effect, Schema, and Protocol. -- The Effect-native scoped host belongs to `@opencode-ai/sdk-next`, which will assume the existing `@opencode-ai/sdk` name after legacy consumers migrate. Client remains network-only and SDK depends one-way on Client. -- SDK executes Server's assembled `HttpRouter` in memory. It opens no listener and performs no network I/O, while preserving Server routing, middleware, codecs, handlers, and errors. -- The Effect Client and SDK re-export their decoded datatype facade from Schema so callers do not depend on internal package locations or Core's versioned names. -- A capability intended for both networked and **Embedded OpenCode** belongs in the authoritative public `HttpApi`; embedded-only same-process capabilities extend **Embedded OpenCode** separately. -- `sessions.events({ sessionID, after })` is a public durable Session event stream. It verifies the Session, replays durable events after the optional aggregate sequence, continues with newly committed durable events, excludes live-only fragments, and is transported as SSE in both networked and embedded modes. -- `events.subscribe()` is a distinct public instance-wide live stream for Session and non-Session activity. It has no replay guarantee and includes connection, heartbeat, and instance-disposal lifecycle events; consumers recover from disconnection by refreshing authoritative state. -- A Session ID is not an optional filter on `events.subscribe()`: instance-wide live events and durable Session events have different schemas, replay guarantees, cursors, lifecycle events, and failure behavior. -- The initial common OpenCode Client does not expose server-global event aggregation. `events.subscribe()` is bounded to the connected OpenCode instance or workspace; any future cross-instance administrative stream requires a separately designed API. -- `events.subscribe()` does not automatically reconnect after transport loss. The live-only stream fails with `ClientError`; consumers refresh authoritative state before explicitly opening a new subscription because events missed during disconnection cannot be replayed. -- `sessions.events({ sessionID, after })` returns the generated HTTP client's cold durable event stream and does not build reconnection policy into the endpoint or client constructor. Transport loss fails the stream with `ClientError`. Callers may compose an explicit resuming stream above it by retaining the last observed durable sequence and opening a new subscription with `after`; any reusable resume helper remains a separate API design question. -- The stable `sessions.list(...)` design returns a **Page** in both networked and **Embedded OpenCode**; embedded execution does not define a separate unbounded array-returning list operation. The beta client currently preserves the existing HTTP `{ data, cursor }` envelope until emitter-level Page projection is implemented. -- Session list cursors are opaque branded values carrying continuation query and ordering state. Consumers pass them back unchanged and do not inspect storage anchors or encoded filter fields. -- A Session list continuation accepts only its opaque cursor. Scope, filters, ordering, and page size are fixed by the initial query and carried by that cursor. -- `sessions.messages(...)` returns a **Page** and uses the same cursor discipline as `sessions.list(...)`: the initial request supplies `sessionID`, ordering, and page size; continuation supplies `sessionID` plus only an opaque branded message cursor carrying ordering, page size, direction, and message anchor. Using a cursor with another Session is invalid. -- `sessions.message({ sessionID, messageID })` is a required resource lookup. An unknown Session fails with `SessionNotFoundError`; a known Session with an absent or differently owned message fails with `MessageNotFoundError` without disclosing cross-Session ownership. Absence is not represented as `undefined` across the public HTTP boundary. -- `sessions.interrupt({ sessionID })` first verifies that the durable Session exists, failing with `SessionNotFoundError` otherwise. For a known Session, interruption is idempotent: idle, already-settled, or locally unowned execution is a no-op. -- `sessions.active()` snapshots the current process's foreground Session drain registry as a record of Session IDs to `{ type: "running" }`. Missing IDs are inactive; background subagents and tasks do not make their parent Session active, and process restart clears the registry. -- `sessions.context({ sessionID })` preserves the existing message-only operation. It returns projected conversational messages selected as Session context; it does not include or represent the complete provider request context, whose baseline system context and other contributions remain separate. -- **Open question**: Should a future, separately named operation expose the complete provider request context, including baseline system context, selected source contributions, and context-epoch metadata? -- `sessions.prompt(...)` exposes `resume?: boolean`. Omitting it preserves durable admission followed by an advisory execution wake; `resume: false` requests durable admit-only behavior. -- The public operation remains `sessions.prompt(...)`; `SessionInput.admit` is the internal primitive, while the public `Admission` result and `resume` option express its durable admission semantics. -- `sessions.create(...)` accepts an optional `location`. Omission resolves through the connected OpenCode instance's default or current location; an explicit value selects a known location. Networked and embedded transports use the same handler semantics. -- `sessions.switchAgent({ sessionID, agent })` is part of the common client alongside `sessions.switchModel(...)`. It affects subsequent Session activity and fails with `SessionNotFoundError` for an unknown Session. -- The **Embedded OpenCode** Layer delegates to the same scoped creation path; it does not define a second implementation. -- A **PTY Environment** adapter observes plugins in the request Location while passing the resolved PTY working directory to the hook; standalone servers use an empty adapter. -- A **Mid-Conversation System Message** lowers to the provider's native chronological instruction role when supported and to a wrapped chronological fallback otherwise. -- When the effective aggregate instruction set changes, its **Mid-Conversation System Message** includes the complete current ordered set and supersedes the prior aggregate value; when no ambient instructions remain, the message states that previously loaded instructions no longer apply. -- Ambient project instruction discovery honors `OPENCODE_DISABLE_PROJECT_CONFIG`; global instructions remain eligible. -- Oversized textual **Model Tool Output** retains a bounded preview in Session history while its complete text moves to managed tool-output storage. Arbitrary structured-result size is a separate concern. -- One tool settlement receives one aggregate textual limit, using the configured maximum lines or UTF-8 bytes, whichever is reached first. The limit is provider-independent; token pressure belongs to context assembly and compaction. -- Generic truncation preserves the beginning and end of textual output. Tools may apply a more meaningful strategy before the Tool Registry enforces the final limit. -- A truncated **Model Tool Output** identifies its complete text both in the bounded model-visible preview and as a typed managed output path. Managed output paths do not modify the tool's validated structured result. -- A **Managed Tool Output File** is temporary and may expire after its retention period. The bounded **Model Tool Output**, not the file, is the durable replayable record. -- Failure to retain a **Managed Tool Output File** does not change a successful tool operation into a failed one. The Session records an explicitly lossy bounded output without a path, while operators receive diagnostics for the storage failure. -- Once a tool operation succeeds, bounding its **Model Tool Output** and publishing its one durable settlement form an interruption-safe completion region. Raw oversized success is never published before a later correction. -- When a structured-only result would exceed the **Model Tool Output** limit, its validated structured value remains unchanged for Session consumers while model replay uses a bounded textual JSON preview and optional managed output path. -- Existing tool-managed output paths survive generic bounding. A fallback file retains exactly the complete projected text received by the Tool Registry and never claims to reconstruct output already discarded by tool-specific shaping. -- **Managed Tool Output Files** use globally unique names in one shared flat directory. Their absolute paths are readable and searchable by ordinary tools; other absolute paths remain outside Location-scoped filesystem authority. -- Provider-executed tool results remain provider-native transcript facts outside generic Tool Registry bounding. Their context control requires provider-aware pruning or compaction because some providers require exact structured round-trip payloads. - -## Client contract architecture - -Semantic values that mean the same thing internally and publicly live in the lightweight Schema leaf. Core consumes Schema for domain behavior; Protocol composes Schema values into paths, payloads, envelopes, errors, cursors, and streams; Server imports both, hosts Protocol's exact groups, and owns protocol/domain adaptation. The root Promise client remains zero-Effect, `/effect` depends on Effect plus Schema and Protocol, and `@opencode-ai/sdk-next` composes the scoped in-process host above Client, Core, and Server. - -Shared public records are plain objects declared with `Schema.Struct`. A same-name inferred interface gives object records readable TypeScript signatures without constructors, prototypes, or nominal identity; unions retain explicit type aliases. - -Before stabilizing the client API: - -- Keep additional public schemas in Schema and additional network groups in Protocol; neither package may transitively load databases, Drizzle, Session execution, providers, watchers, native modules, or WASM. -- Keep concrete Location middleware keys in Server while Protocol owns their placement. Client projections may supply transport-only keys, but must prove generated equivalence with Server's concrete API. -- Project the existing list response envelope to the stable client **Page** shape and enforce separate initial-query and cursor-continuation inputs without changing the hosted V2 wire contract. -- Settle the stable consumer namespace (`session` versus the current beta `sessions`) and use an explicit codegen annotation if the consumer name should differ from the server group identifier. -- Preserve V2 route paths, operation IDs, codecs, errors, middleware behavior, and OpenAPI output while making this change. -- Preserve browser-safe `@opencode-ai/client` and `@opencode-ai/client/effect` bundles through import-boundary tests. -- Define embedded-host placement before supporting multiple hosts over one database. Hosts that share durable Session storage must also share process-local Session execution coordination, or each host must receive isolated storage explicitly. -- Keep an embedded request scope alive until any streamed response body finishes. The initial non-streaming Session surface does not exercise this lifetime boundary; Session and instance event streams must do so before joining the embedded client. - -## 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 Sources**, or narrow its semantics. diff --git a/artifacts/tui-thinking-disclosure/thinking-flat-markdown-disclosure.mp4 b/artifacts/tui-thinking-disclosure/thinking-flat-markdown-disclosure.mp4 new file mode 100644 index 0000000000..7df912aad1 Binary files /dev/null and b/artifacts/tui-thinking-disclosure/thinking-flat-markdown-disclosure.mp4 differ diff --git a/bun.lock b/bun.lock index e905ada8c5..1d50a2d541 100644 --- a/bun.lock +++ b/bun.lock @@ -14,8 +14,11 @@ }, "devDependencies": { "@actions/artifact": "5.0.1", + "@ast-grep/cli": "0.44.0", "@tsconfig/bun": "catalog:", "@types/mime-types": "3.0.1", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.3", "@typescript/native-preview": "catalog:", "glob": "13.0.5", "husky": "9.1.7", @@ -27,9 +30,30 @@ "turbo": "2.10.2", }, }, + "packages/ai": { + "name": "@opencode-ai/ai", + "version": "1.17.20", + "dependencies": { + "@opencode-ai/schema": "workspace:*", + "@smithy/eventstream-codec": "4.2.14", + "@smithy/util-utf8": "4.2.2", + "aws4fetch": "1.0.20", + "effect": "catalog:", + "google-auth-library": "10.5.0", + }, + "devDependencies": { + "@clack/prompts": "1.0.0-alpha.1", + "@effect/platform-node": "catalog:", + "@opencode-ai/http-recorder": "workspace:*", + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:", + }, + }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@corvu/drawer": "catalog:", "@dnd-kit/abstract": "0.5.0", @@ -37,7 +61,6 @@ "@dnd-kit/helpers": "0.5.0", "@dnd-kit/solid": "0.5.0", "@kobalte/core": "catalog:", - "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "workspace:*", @@ -66,7 +89,7 @@ "diff": "catalog:", "effect": "catalog:", "fuzzysort": "catalog:", - "ghostty-web": "github:anomalyco/ghostty-web#83c0a07b8628b748aed073b232cb4b52a6ca11c1", + "ghostty-web": "github:anomalyco/ghostty-web#513463a6f1190253057e8a3f0dac8f6ee8393553", "luxon": "catalog:", "marked": "catalog:", "marked-shiki": "catalog:", @@ -96,47 +119,73 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.18.11", + "version": "1.18.3", "bin": { - "lildax": "./bin/lildax.cjs", + "opencode2": "./bin/opencode2.cjs", }, "dependencies": { "@effect/platform-node": "catalog:", + "@opencode-ai/client": "workspace:*", "@opencode-ai/core": "workspace:*", - "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/plugin": "workspace:*", + "@opencode-ai/schema": "workspace:*", "@opencode-ai/server": "workspace:*", "@opencode-ai/tui": "workspace:*", "@opentui/core": "catalog:", + "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", "@parcel/watcher": "2.5.1", "effect": "catalog:", + "fuzzysort": "catalog:", + "immer": "11.1.4", + "jsonc-parser": "3.3.1", + "open": "10.1.2", + "opentui-spinner": "catalog:", + "semver": "catalog:", "solid-js": "catalog:", + "strip-ansi": "7.1.2", + "uqr": "0.1.3", + "ws": "8.21.0", }, "devDependencies": { + "@lydell/node-pty-darwin-arm64": "1.2.0-beta.12", + "@lydell/node-pty-darwin-x64": "1.2.0-beta.12", + "@lydell/node-pty-linux-arm64": "1.2.0-beta.12", + "@lydell/node-pty-linux-x64": "1.2.0-beta.12", + "@lydell/node-pty-win32-arm64": "1.2.0-beta.12", + "@lydell/node-pty-win32-x64": "1.2.0-beta.12", + "@opencode-ai/protocol": "workspace:*", "@opencode-ai/script": "workspace:*", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", + "@types/semver": "catalog:", "@typescript/native-preview": "catalog:", + "vite": "catalog:", + "vite-plugin-solid": "catalog:", }, }, "packages/client": { "name": "@opencode-ai/client", + "version": "1.17.13", "dependencies": { "@opencode-ai/protocol": "workspace:*", "@opencode-ai/schema": "workspace:*", }, "devDependencies": { "@effect/platform-node": "catalog:", - "@opencode-ai/core": "workspace:*", "@opencode-ai/httpapi-codegen": "workspace:*", - "@opencode-ai/server": "workspace:*", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@typescript/native-preview": "catalog:", "effect": "catalog:", }, "peerDependencies": { - "effect": "4.0.0-beta.83", + "effect": "4.0.0-beta.98", }, "optionalPeers": [ "effect", @@ -144,7 +193,7 @@ }, "packages/codemode": { "name": "@opencode-ai/codemode", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "acorn": "8.15.0", "effect": "catalog:", @@ -158,7 +207,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -194,7 +243,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -221,7 +270,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -243,7 +292,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -267,7 +316,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -287,7 +336,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.18.11", + "version": "1.18.3", "bin": { "opencode": "./bin/opencode", }, @@ -303,7 +352,7 @@ "@ai-sdk/google": "3.0.73", "@ai-sdk/google-vertex": "4.0.128", "@ai-sdk/groq": "3.0.31", - "@ai-sdk/mistral": "3.0.51", + "@ai-sdk/mistral": "3.0.27", "@ai-sdk/openai": "3.0.84", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/perplexity": "3.0.26", @@ -318,11 +367,13 @@ "@effect/sql-sqlite-bun": "catalog:", "@ff-labs/fff-bun": "0.9.4", "@lydell/node-pty": "catalog:", + "@modelcontextprotocol/sdk": "1.29.0", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", + "@opencode-ai/ai": "workspace:*", + "@opencode-ai/codemode": "workspace:*", "@opencode-ai/effect-drizzle-sqlite": "workspace:*", "@opencode-ai/effect-sqlite-node": "workspace:*", - "@opencode-ai/llm": "workspace:*", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/schema": "workspace:*", "@openrouter/ai-sdk-provider": "2.9.0", @@ -339,7 +390,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.12.1", + "gitlab-ai-provider": "6.11.1", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -350,6 +401,7 @@ "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", + "resolve.exports": "catalog:", "semver": "^7.6.3", "turndown": "7.2.0", "venice-ai-sdk-provider": "2.1.1", @@ -381,16 +433,16 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@zip.js/zip.js": "2.7.62", - "drizzle-orm": "catalog:", "effect": "catalog:", "electron-context-menu": "4.1.2", "electron-log": "^5", "electron-store": "11.0.2", "electron-updater": "6.8.9", "electron-window-state": "^5.0.3", + "marked": "^15", }, "devDependencies": { "@actions/artifact": "4.0.0", @@ -433,9 +485,15 @@ "@parcel/watcher-win32-x64": "2.5.1", }, }, + "packages/docs": { + "name": "@opencode-ai/docs", + "devDependencies": { + "mint": "4.2.666", + }, + }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -449,7 +507,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "effect": "catalog:", }, @@ -461,7 +519,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -493,7 +551,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -509,12 +567,12 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { - "@effect/platform-node": "4.0.0-beta.83", - "@effect/platform-node-shared": "4.0.0-beta.83", + "@effect/platform-node-shared": "4.0.0-beta.98", }, "devDependencies": { + "@effect/platform-node": "catalog:", "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", "@types/node": "catalog:", @@ -523,11 +581,12 @@ "typescript": "catalog:", }, "peerDependencies": { - "effect": "4.0.0-beta.83", + "effect": "catalog:", }, }, "packages/httpapi-codegen": { "name": "@opencode-ai/httpapi-codegen", + "version": "0.0.0", "dependencies": { "effect": "catalog:", "prettier": "3.6.2", @@ -538,28 +597,9 @@ "@typescript/native-preview": "catalog:", }, }, - "packages/llm": { - "name": "@opencode-ai/llm", - "version": "1.18.11", - "dependencies": { - "@opencode-ai/schema": "workspace:*", - "@smithy/eventstream-codec": "4.2.14", - "@smithy/util-utf8": "4.2.2", - "aws4fetch": "1.0.20", - "effect": "catalog:", - }, - "devDependencies": { - "@clack/prompts": "1.0.0-alpha.1", - "@effect/platform-node": "catalog:", - "@opencode-ai/http-recorder": "workspace:*", - "@tsconfig/bun": "catalog:", - "@types/bun": "catalog:", - "@typescript/native-preview": "catalog:", - }, - }, "packages/opencode": { "name": "opencode", - "version": "1.18.11", + "version": "1.18.3", "bin": { "opencode": "./bin/opencode", }, @@ -578,7 +618,7 @@ "@ai-sdk/google": "3.0.73", "@ai-sdk/google-vertex": "4.0.128", "@ai-sdk/groq": "3.0.31", - "@ai-sdk/mistral": "3.0.51", + "@ai-sdk/mistral": "3.0.27", "@ai-sdk/openai": "3.0.84", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/perplexity": "3.0.26", @@ -596,8 +636,10 @@ "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", + "@opencode-ai/ai": "workspace:*", + "@opencode-ai/cli": "workspace:*", + "@opencode-ai/client": "workspace:*", "@opencode-ai/codemode": "workspace:*", - "@opencode-ai/llm": "workspace:*", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/protocol": "workspace:*", "@opencode-ai/schema": "workspace:*", @@ -632,7 +674,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.12.1", + "gitlab-ai-provider": "6.11.1", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -690,10 +732,14 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@ai-sdk/provider": "3.0.8", + "@opencode-ai/ai": "workspace:*", + "@opencode-ai/client": "workspace:*", + "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "workspace:*", + "@standard-schema/spec": "^1.1.0", "effect": "catalog:", "zod": "catalog:", }, @@ -701,6 +747,7 @@ "@opentui/core": "catalog:", "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", + "@tsconfig/bun": "catalog:", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", @@ -719,6 +766,7 @@ }, "packages/protocol": { "name": "@opencode-ai/protocol", + "version": "1.17.11", "dependencies": { "@opencode-ai/schema": "workspace:*", "effect": "catalog:", @@ -727,10 +775,12 @@ "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@typescript/native-preview": "catalog:", + "typescript": "catalog:", }, }, "packages/schema": { "name": "@opencode-ai/schema", + "version": "1.17.11", "dependencies": { "effect": "catalog:", }, @@ -738,6 +788,7 @@ "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@typescript/native-preview": "catalog:", + "typescript": "catalog:", }, }, "packages/script": { @@ -755,10 +806,14 @@ "dependencies": { "@opencode-ai/client": "workspace:*", "@opencode-ai/core": "workspace:*", + "@opencode-ai/plugin": "workspace:*", + "@opencode-ai/schema": "workspace:*", "@opencode-ai/server": "workspace:*", "effect": "catalog:", }, "devDependencies": { + "@opencode-ai/httpapi-codegen": "workspace:*", + "@opencode-ai/protocol": "workspace:*", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@typescript/native-preview": "catalog:", @@ -766,7 +821,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "cross-spawn": "catalog:", }, @@ -781,10 +836,12 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { + "@effect/platform-node": "catalog:", "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", + "@opencode-ai/simulation": "workspace:*", "drizzle-orm": "catalog:", "effect": "catalog:", }, @@ -796,10 +853,9 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@kobalte/core": "catalog:", - "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -811,6 +867,7 @@ "@solid-primitives/media": "2.3.3", "@solid-primitives/resize-observer": "2.1.3", "@solidjs/meta": "catalog:", + "@solidjs/router": "catalog:", "diff": "catalog:", "dompurify": "3.3.1", "fuzzysort": "catalog:", @@ -838,9 +895,26 @@ "vite": "catalog:", }, }, + "packages/simulation": { + "name": "@opencode-ai/simulation", + "version": "1.17.13", + "dependencies": { + "@fontsource/commit-mono": "5.2.5", + "@napi-rs/canvas": "1.0.2", + "@opencode-ai/ai": "workspace:*", + "@opencode-ai/core": "workspace:*", + "@opentui/core": "catalog:", + "effect": "catalog:", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", + }, + }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -853,7 +927,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@ibm/plex": "6.4.1", "@kobalte/core": "catalog:", @@ -887,7 +961,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -906,7 +980,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -938,7 +1012,6 @@ "@types/node": "catalog:", "@types/react": "18.0.25", "react": "18.2.0", - "react-dom": "18.2.0", "solid-js": "catalog:", "storybook": "^10.2.13", "storybook-solidjs-vite": "^10.0.9", @@ -948,24 +1021,29 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { + "@opencode-ai/client": "workspace:*", "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", - "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/simulation": "workspace:*", "@opencode-ai/ui": "workspace:*", "@opentui/core": "catalog:", "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", + "@solid-primitives/event-bus": "1.1.2", "clipboardy": "4.0.0", "diff": "catalog:", "effect": "catalog:", "fuzzysort": "catalog:", + "get-east-asian-width": "catalog:", "open": "10.1.2", "opentui-spinner": "catalog:", "remeda": "catalog:", "solid-js": "catalog:", + "string-width": "catalog:", "strip-ansi": "7.1.2", + "uqr": "0.1.3", }, "devDependencies": { "@tsconfig/bun": "catalog:", @@ -975,7 +1053,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@kobalte/core": "catalog:", "@pierre/diffs": "catalog:", @@ -1001,7 +1079,6 @@ "remend": "catalog:", "shiki": "catalog:", "solid-list": "catalog:", - "solid-sonner": "catalog:", "strip-ansi": "7.1.2", }, "devDependencies": { @@ -1027,7 +1104,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -1059,6 +1136,34 @@ "typescript": "catalog:", }, }, + "packages/www": { + "name": "@opencode-ai/www", + "version": "1.17.18", + "dependencies": { + "@cloudflare/vite-plugin": "1.44.0", + "@tailwindcss/vite": "4.3.2", + "@tanstack/react-router": "1.170.17", + "@tanstack/react-start": "1.168.27", + "@tanstack/router-plugin": "1.168.19", + "fumadocs-core": "16.11.1", + "fumadocs-mdx": "15.1.0", + "fumadocs-ui": "16.11.1", + "react": "19.2.7", + "react-dom": "19.2.7", + "tailwindcss": "4.3.2", + "vite": "8.1.4", + }, + "devDependencies": { + "@types/mdx": "2.0.14", + "@types/node": "catalog:", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.3", + "@typescript/native-preview": "catalog:", + "@vitejs/plugin-react": "6.0.3", + "typescript": "catalog:", + "wrangler": "4.110.0", + }, + }, }, "trustedDependencies": [ "esbuild", @@ -1075,14 +1180,12 @@ "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", - "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", - "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", - "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", + "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "pacote@21.5.0": "patches/pacote@21.5.0.patch", - "@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch", + "effect@4.0.0-beta.98": "patches/effect@4.0.0-beta.98.patch", }, "overrides": { "@opentui/core": "catalog:", @@ -1094,9 +1197,9 @@ "catalog": { "@cloudflare/workers-types": "4.20251008.0", "@corvu/drawer": "0.2.4", - "@effect/opentelemetry": "4.0.0-beta.83", - "@effect/platform-node": "4.0.0-beta.83", - "@effect/sql-sqlite-bun": "4.0.0-beta.83", + "@effect/opentelemetry": "4.0.0-beta.98", + "@effect/platform-node": "4.0.0-beta.98", + "@effect/sql-sqlite-bun": "4.0.0-beta.98", "@hono/standard-validator": "0.2.0", "@hono/zod-validator": "0.4.2", "@kobalte/core": "0.13.11", @@ -1132,8 +1235,9 @@ "dompurify": "3.3.1", "drizzle-kit": "1.0.0-rc.2", "drizzle-orm": "1.0.0-rc.2", - "effect": "4.0.0-beta.83", + "effect": "4.0.0-beta.98", "fuzzysort": "3.1.0", + "get-east-asian-width": "1.6.0", "hono": "4.10.7", "hono-openapi": "1.1.2", "luxon": "3.6.1", @@ -1142,12 +1246,13 @@ "opentui-spinner": "0.0.7", "remeda": "2.26.0", "remend": "1.3.0", + "resolve.exports": "2.0.3", "semver": "7.7.4", "shiki": "4.2.0", "solid-js": "1.9.10", "solid-list": "0.3.0", - "solid-sonner": "0.3.1", "sst": "4.13.1", + "string-width": "7.2.0", "tailwindcss": "4.1.11", "typescript": "5.8.2", "ulid": "3.0.1", @@ -1204,7 +1309,7 @@ "@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="], - "@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.51", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.40" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-83eXY6p0lUFhSuMvNDmTKDuMciK5XDAWDlNh5c0L80tKjmtCFRItA1MZHp4IKe1r7eK8Rb5nN7qtxqMLUFRIRw=="], + "@ai-sdk/mistral": ["@ai-sdk/mistral@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-ZXe7nZQgliDdjz5ufH5RKpHWxbN72AzmzzKGbF/z+0K9GN5tUCnftrQRvTRFHA5jAzTapcm2BEevmGLVbMkW+A=="], "@ai-sdk/openai": ["@ai-sdk/openai@3.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ALmj/53EXpcRqMbGpPJPP4UOSWw0q4VGpnDo7YctvsynjkrKDmoneDG/1a7VQnSPYHnJp6tTRMf5ZdxZ5whulg=="], @@ -1222,6 +1327,8 @@ "@ai-sdk/xai": ["@ai-sdk/xai@3.0.102", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.56", "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.35" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NeQyOR7OCqDMgaLS4uNX/ep/HrwUzzFYLzXQSRoqLy2jsnqxAJhsgltRwAwf+ADjyPBIAKEOestWnIQA+LrLrQ=="], + "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.5", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw=="], + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], @@ -1230,6 +1337,26 @@ "@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="], + "@ark/schema": ["@ark/schema@0.55.0", "", { "dependencies": { "@ark/util": "0.55.0" } }, "sha512-IlSIc0FmLKTDGr4I/FzNHauMn0MADA6bCjT1wauu4k6MyxhC1R9gz0olNpIRvK7lGGDwtc/VO0RUDNvVQW5WFg=="], + + "@ark/util": ["@ark/util@0.55.0", "", {}, "sha512-aWFNK7aqSvqFtVsl1xmbTjGbg91uqtJV7Za76YGNEwIO4qLjMfyY8flmmbhooYMuqPCO2jyxu8hve943D+w3bA=="], + + "@ast-grep/cli": ["@ast-grep/cli@0.44.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@ast-grep/cli-darwin-arm64": "0.44.0", "@ast-grep/cli-darwin-x64": "0.44.0", "@ast-grep/cli-linux-arm64-gnu": "0.44.0", "@ast-grep/cli-linux-x64-gnu": "0.44.0", "@ast-grep/cli-win32-arm64-msvc": "0.44.0", "@ast-grep/cli-win32-ia32-msvc": "0.44.0", "@ast-grep/cli-win32-x64-msvc": "0.44.0" }, "bin": { "sg": "sg", "ast-grep": "ast-grep" } }, "sha512-Jf4PuP7XjzsMa3m9gYxmzV8KyWZc4w1ZzKe/t0+90wWxmSasQJe6AtMkJxHEi98MGgfAF1nWziqjDd0/6EsBjA=="], + + "@ast-grep/cli-darwin-arm64": ["@ast-grep/cli-darwin-arm64@0.44.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bF7euu/hF/cYg4510z8110vh60rrqfrBdsfRqVGd6xqNSPENu7CJnTVN/Z4Nk5U1NM8YKzUD+dYx1ySUJ0CUNQ=="], + + "@ast-grep/cli-darwin-x64": ["@ast-grep/cli-darwin-x64@0.44.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0fI9caQGp1dFcmBATNlVytIRdAeYb91v1D2xjMIi1bSX+l8Uj846JUiaimUGBuBZmyFq+BScoWM4RnprEmZMpQ=="], + + "@ast-grep/cli-linux-arm64-gnu": ["@ast-grep/cli-linux-arm64-gnu@0.44.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-JB6EUnqEtGGtyg1GqNquld/++1CvaWD7r84IwwhddX1qx0NmDoHyn2mKd8vnQ24Z0RkV3g7y7foMLakELbGtDw=="], + + "@ast-grep/cli-linux-x64-gnu": ["@ast-grep/cli-linux-x64-gnu@0.44.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rNL0LsI682D9EMzfaGVEtZa1xaqTtGb2I+Zk4ZzidX6u+fF7f79wdqyKahKjXzoIrGkuhkoL3gcyLKAtQd9+qg=="], + + "@ast-grep/cli-win32-arm64-msvc": ["@ast-grep/cli-win32-arm64-msvc@0.44.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-lqD0MhGQAddh2YoV/brKQ6GVcFLmRiTBwIElutwedaUvRCdasTGukFPYuSWk/iI8Kv19xom6s7l+mGuZ7v+xwQ=="], + + "@ast-grep/cli-win32-ia32-msvc": ["@ast-grep/cli-win32-ia32-msvc@0.44.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-ZJrnS+2OkNfwyr6yrN69glP67uybBxDvl9mqZvh1J44vB3OFn9U9c+cVAoZIAo7JD5F4rZNxwyu3gcy4+xuwEA=="], + + "@ast-grep/cli-win32-x64-msvc": ["@ast-grep/cli-win32-x64-msvc@0.44.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OJEo7f95YYaSuS1byUB7ZctbzxoA7/wCoAol+pt6pvfdW/8Wq+L1qU28glwx7dQ0HgTsnPZbWpXQwmZpCBHhZg=="], + "@astrojs/check": ["@astrojs/check@0.9.6", "", { "dependencies": { "@astrojs/language-server": "^2.16.1", "chokidar": "^4.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "bin/astro-check.js" } }, "sha512-jlaEu5SxvSgmfGIFfNgcn5/f+29H61NJzEMfAZ82Xopr4XBchXB1GVlcJsE+elUlsYSbXlptZLX+JMG3b/wZEA=="], "@astrojs/cloudflare": ["@astrojs/cloudflare@12.6.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-xhJptF5tU2k5eo70nIMyL1Udma0CqmUEnGSlGyFflLqSY82CRQI6nWZ/xZt0ZvmXuErUjIx0YYQNfZsz5CNjLQ=="], @@ -1258,6 +1385,10 @@ "@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.4", "", { "dependencies": { "yaml": "^2.8.3" } }, "sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A=="], + "@asyncapi/parser": ["@asyncapi/parser@3.4.0", "", { "dependencies": { "@asyncapi/specs": "^6.8.0", "@openapi-contrib/openapi-schema-to-json-schema": "~3.2.0", "@stoplight/json": "3.21.0", "@stoplight/json-ref-readers": "^1.2.2", "@stoplight/json-ref-resolver": "^3.1.5", "@stoplight/spectral-core": "^1.18.3", "@stoplight/spectral-functions": "^1.7.2", "@stoplight/spectral-parsers": "^1.0.2", "@stoplight/spectral-ref-resolver": "^1.0.3", "@stoplight/types": "^13.12.0", "@types/json-schema": "^7.0.11", "@types/urijs": "^1.19.19", "ajv": "^8.17.1", "ajv-errors": "^3.0.0", "ajv-formats": "^2.1.1", "avsc": "^5.7.5", "js-yaml": "^4.1.0", "jsonpath-plus": "^10.0.0", "node-fetch": "2.6.7" } }, "sha512-Sxn74oHiZSU6+cVeZy62iPZMFMvKp4jupMFHelSICCMw1qELmUHPvuZSr+ZHDmNGgHcEpzJM5HN02kR7T4g+PQ=="], + + "@asyncapi/specs": ["@asyncapi/specs@6.8.1", "", { "dependencies": { "@types/json-schema": "^7.0.11" } }, "sha512-czHoAk3PeXTLR+X8IUaD+IpT+g+zUvkcgMDJVothBsan+oHN3jfcFcFUNdOPAAFoUCQN1hXF1dWuphWy05THlA=="], + "@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=="], "@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="], @@ -1452,6 +1583,8 @@ "@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=="], + "@canvas/image-data": ["@canvas/image-data@1.1.0", "", {}, "sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA=="], + "@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=="], "@clack/core": ["@clack/core@1.0.0-alpha.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rFbCU83JnN7l3W1nfgCqqme4ZZvTTgsiKQ6FM0l+r0P+o2eJpExcocBUWUIwnDzL76Aca9VhUdWmB2MbUv+Qyg=="], @@ -1506,13 +1639,13 @@ "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], - "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.83", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.83" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-cPfCfp/ghu0itbX6Dqjdr4N0rbjng5ON4sUpnLHV5JJySG8zZpWmuOZLWIrfrNKT2ctYR1BYmp1aYCgkItaJLw=="], + "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.98", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.98" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-ITfK8xhcl+9GXOvPwzADWkOQ+dgUGZrJNefT3r2+uLFmzjyKRLtHzhLOl6lZaLSsf5io13+nmt8adfMRQPq+oA=="], - "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.83", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.83", "mime": "^4.1.0", "undici": "^8.2.0" }, "peerDependencies": { "effect": "^4.0.0-beta.83", "ioredis": "^5.7.0" } }, "sha512-RmpVGu/+X/Bif3/g1Rzj8oFzTOknoVB3yHCa0b179vytPpKe+Kj9ZwKNcAnKWqHUDkbSPBq1Ca60mvOHr2/+LQ=="], + "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.98", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.98", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98", "ioredis": "^5.7.0" } }, "sha512-IQu1TiLXQEDSGkDBllyYjVadf+UqdjptryqX4mmktVTTbGDq7X4uVxe7cSgXuqZvyfG6kagTzwj2lfynxOaKQg=="], - "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.83", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.83" } }, "sha512-+yr/+PJmKTgmJq1QOINSBPgLu7Cjc4CZcotBXnGjyDEizOmimFgTkN2B8PBJAKIKUWYWfobjXqC+58/VhhPKAw=="], + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.98", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98" } }, "sha512-iySXaffnCJX1sNAIp79ghhIeui9E5qwUQyqd1VLPkB9UNO4vdpd9B5fTEXwe7S/GusL4jsk9vSvX38XJgRFG1w=="], - "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.83", "", { "peerDependencies": { "effect": "^4.0.0-beta.83" } }, "sha512-6OaxLsWffxkh9pXYUSyj/AxjVb9URY2rG9U6atjxClWy30Jx77R9Pm3Rrc7cQ63kQurePavEw1bQbzQ/SILiQQ=="], + "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.98", "", { "peerDependencies": { "effect": "^4.0.0-beta.98" } }, "sha512-cc41uLhYBqexdbTNu4dlui+31E8hcVLEapLySa0C8d60FmBY8IEAV/RD3oF+6pqPslKEZ9p1+XVLdDm0iflw5Q=="], "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], @@ -1544,11 +1677,11 @@ "@emmetio/stream-reader-utils": ["@emmetio/stream-reader-utils@0.1.0", "", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="], - "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], + "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], - "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], "@emotion/is-prop-valid": ["@emotion/is-prop-valid@0.8.8", "", { "dependencies": { "@emotion/memoize": "0.7.4" } }, "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA=="], @@ -1656,10 +1789,16 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + "@fontsource/commit-mono": ["@fontsource/commit-mono@5.2.5", "", {}, "sha512-htX8yQWtiPt5L1Hzh4sirvfUJT2+KYiquDB/Q2sY2tWQYplpBUOD5zHnIM3k36Hnm4V+JIIqA/wmwupSQ68WjA=="], + "@fontsource/ibm-plex-mono": ["@fontsource/ibm-plex-mono@5.2.5", "", {}, "sha512-G09N3GfuT9qj3Ax2FDZvKqZttzM3v+cco2l8uXamhKyXLdmlaUDH5o88/C3vtTHj2oT7yRKsvxz9F+BXbWKMYA=="], "@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="], + "@fuma-translate/react": ["@fuma-translate/react@1.0.2", "", { "peerDependencies": { "@types/react": "*", "react": "^19.2.0", "react-dom": "^19.2.0" }, "optionalPeers": ["@types/react"] }, "sha512-uOiOtBx3nRXR8Nu1GzBf1tApgF1FErDBTHxRIAQeyQdyOoZbrNRN6H4kDCWObY4qyGeGbHydG0DHzgeUgFDMIw=="], + + "@fumadocs/tailwind": ["@fumadocs/tailwind@0.1.0", "", { "peerDependencies": { "tailwindcss": "^4.0.0" }, "optionalPeers": ["tailwindcss"] }, "sha512-nF/DCAwOR21HZ4AkjIOv3Iqwyqywzb6pdyeMcoa+aZzirXj5ntvNZbe3jJ0v3ehhtrRfYYeXBezvjn8ZmV+fuQ=="], + "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], "@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="], @@ -1684,6 +1823,8 @@ "@ibm/telemetry-js": ["@ibm/telemetry-js@1.11.0", "", { "bin": { "ibmtelemetry": "dist/collect.js" } }, "sha512-RO/9j+URJnSfseWg9ZkEX9p+a3Ousd33DBU7rOafoZB08RqdzxFVYJ2/iM50dkBuD0o7WX7GYt1sLbNgCoE+pA=="], + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], @@ -1696,6 +1837,10 @@ "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], @@ -1708,6 +1853,10 @@ "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="], "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], @@ -1718,10 +1867,44 @@ "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="], + + "@inquirer/checkbox": ["@inquirer/checkbox@4.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA=="], + + "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="], + + "@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="], + + "@inquirer/editor": ["@inquirer/editor@4.2.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/external-editor": "^1.0.3", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ=="], + + "@inquirer/expand": ["@inquirer/expand@4.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew=="], + + "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], + + "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], + + "@inquirer/input": ["@inquirer/input@4.3.1", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g=="], + + "@inquirer/number": ["@inquirer/number@3.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg=="], + + "@inquirer/password": ["@inquirer/password@4.0.23", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA=="], + + "@inquirer/prompts": ["@inquirer/prompts@7.9.0", "", { "dependencies": { "@inquirer/checkbox": "^4.3.0", "@inquirer/confirm": "^5.1.19", "@inquirer/editor": "^4.2.21", "@inquirer/expand": "^4.0.21", "@inquirer/input": "^4.2.5", "@inquirer/number": "^3.0.21", "@inquirer/password": "^4.0.21", "@inquirer/rawlist": "^4.1.9", "@inquirer/search": "^3.2.0", "@inquirer/select": "^4.4.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@4.1.11", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw=="], + + "@inquirer/search": ["@inquirer/search@3.2.2", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA=="], + + "@inquirer/select": ["@inquirer/select@4.4.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w=="], + + "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], + "@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.7", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg=="], @@ -1756,6 +1939,12 @@ "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], + "@jsep-plugin/assignment": ["@jsep-plugin/assignment@1.3.0", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ=="], + + "@jsep-plugin/regex": ["@jsep-plugin/regex@1.0.4", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg=="], + + "@jsep-plugin/ternary": ["@jsep-plugin/ternary@1.1.4", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg=="], + "@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="], "@jsx-email/body": ["@jsx-email/body@1.0.2", "", { "peerDependencies": { "react": "^18.2.0" } }, "sha512-NjR2tgLH4XGfGkm+O8kcVwi9MBqZsXZCLlmk3HlMux3/n/+a5zB+yhJqXWZBJl2i+6cSF+E2O6hK11ekyK9WWQ=="], @@ -1830,6 +2019,26 @@ "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], + "@mintlify/cli": ["@mintlify/cli@4.0.1269", "", { "dependencies": { "@inquirer/prompts": "7.9.0", "@mintlify/common": "1.0.985", "@mintlify/link-rot": "3.0.1172", "@mintlify/models": "0.0.333", "@mintlify/prebuild": "1.0.1131", "@mintlify/previewing": "4.0.1197", "@mintlify/validation": "0.1.769", "adm-zip": "0.5.16", "chalk": "5.2.0", "color": "4.2.3", "detect-port": "1.5.1", "front-matter": "4.0.2", "fs-extra": "11.2.0", "ink": "6.3.0", "inquirer": "12.3.0", "js-yaml": "4.1.1", "mdast-util-mdx-jsx": "3.2.0", "open": "8.4.2", "openid-client": "6.8.2", "posthog-node": "5.17.2", "react": "19.2.3", "semver": "7.7.2", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "4.3.6" }, "optionalDependencies": { "keytar": "7.9.0" }, "bin": { "mint": "bin/index.js", "mintlify": "bin/index.js" } }, "sha512-l9b7InT55JWXV7TU7Jr4Wrijv4/gMFHLQyWJ7fcjqpSxAetR+xNyeEARRlcf7OicGTjZuvmRGGfj75kp3O4p7A=="], + + "@mintlify/common": ["@mintlify/common@1.0.985", "", { "dependencies": { "@asyncapi/parser": "3.4.0", "@asyncapi/specs": "6.8.1", "@mintlify/mdx": "3.0.4", "@mintlify/models": "0.0.333", "@mintlify/openapi-parser": "0.0.8", "@mintlify/validation": "0.1.769", "@sindresorhus/slugify": "2.2.0", "@types/mdast": "4.0.4", "acorn": "8.11.2", "acorn-jsx": "5.3.2", "color-blend": "4.0.0", "estree-util-to-js": "2.0.0", "estree-walker": "3.0.3", "front-matter": "4.0.2", "hast-util-from-html": "2.0.3", "hast-util-to-html": "9.0.4", "hast-util-to-text": "4.0.2", "hex-rgb": "5.0.0", "ignore": "7.0.5", "js-yaml": "4.1.1", "lodash": "4.18.1", "mdast-util-from-markdown": "2.0.2", "mdast-util-gfm": "3.0.0", "mdast-util-mdx": "3.0.0", "mdast-util-mdx-jsx": "3.1.3", "micromark-extension-gfm": "3.0.0", "micromark-extension-mdx-jsx": "3.0.1", "micromark-extension-mdxjs": "3.0.0", "openapi-types": "12.1.3", "postcss": "8.5.14", "rehype-stringify": "10.0.1", "remark": "15.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.0", "remark-math": "6.0.0", "remark-mdx": "3.1.0", "remark-parse": "11.0.0", "remark-rehype": "11.1.1", "remark-stringify": "11.0.0", "sucrase": "3.34.0", "tailwindcss": "3.4.17", "unified": "11.0.5", "unist-builder": "4.0.0", "unist-util-map": "4.0.0", "unist-util-remove": "4.0.0", "unist-util-remove-position": "5.0.0", "unist-util-visit": "5.0.0", "unist-util-visit-parents": "6.0.1", "vfile": "6.0.3", "xss": "1.0.15" } }, "sha512-eJPeR99AKgVifXLdiA2hhfNy2+CmZ3zqQAscRXmFbJFST8SgsUj6rU3D2fx0XYt38b7T3LqEMD7DH5ipSC+Zfg=="], + + "@mintlify/link-rot": ["@mintlify/link-rot@3.0.1172", "", { "dependencies": { "@mintlify/common": "1.0.985", "@mintlify/models": "0.0.333", "@mintlify/prebuild": "1.0.1131", "@mintlify/previewing": "4.0.1197", "@mintlify/scraping": "4.0.849", "@mintlify/validation": "0.1.769", "fs-extra": "11.1.0", "unist-util-visit": "4.1.2" } }, "sha512-8962sk/WO/0YcSkHTiaVZ2DvrCb8TC4BPgp21a9qW6nsPKKNixMhsC2uUB90D+gOzPTejJl0NNd4gLk4fA3SKw=="], + + "@mintlify/mdx": ["@mintlify/mdx@3.0.4", "", { "dependencies": { "@shikijs/transformers": "^3.11.0", "@shikijs/twoslash": "^3.12.2", "arktype": "^2.1.26", "hast-util-to-string": "^3.0.1", "mdast-util-from-markdown": "^2.0.2", "mdast-util-gfm": "^3.1.0", "mdast-util-mdx-jsx": "^3.2.0", "mdast-util-to-hast": "^13.2.0", "next-mdx-remote-client": "^1.0.3", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-smartypants": "^3.0.2", "shiki": "^3.11.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@radix-ui/react-popover": "^1.1.15", "react": "^18.3.1", "react-dom": "^18.3.1" } }, "sha512-tJhdpnM5ReJLNJ2fuDRIEr0zgVd6id7/oAIfs26V46QlygiLsc8qx4Rz3LWIX51rUXW/cfakjj0EATxIciIw+g=="], + + "@mintlify/models": ["@mintlify/models@0.0.333", "", { "dependencies": { "axios": "1.16.1", "openapi-types": "12.1.3" } }, "sha512-0uAsuTsV8gYCDpv4aA0MWilQu+a/mrK+G6q5FVcgunbEZ3meeCXfrQFTviaEw7A+0cR/7Pc2KLA66cPDm+3Qdg=="], + + "@mintlify/openapi-parser": ["@mintlify/openapi-parser@0.0.8", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-9MBRq9lS4l4HITYCrqCL7T61MOb20q9IdU7HWhqYMNMM1jGO1nHjXasFy61yZ8V6gMZyyKQARGVoZ0ZrYN48Og=="], + + "@mintlify/prebuild": ["@mintlify/prebuild@1.0.1131", "", { "dependencies": { "@mintlify/common": "1.0.985", "@mintlify/openapi-parser": "0.0.8", "@mintlify/scraping": "4.0.849", "@mintlify/validation": "0.1.769", "chalk": "5.3.0", "favicons": "7.2.0", "front-matter": "4.0.2", "fs-extra": "11.1.0", "js-yaml": "4.1.1", "openapi-types": "12.1.3", "sharp": "0.33.5", "sharp-ico": "0.1.5", "unist-util-visit": "4.1.2", "uuid": "11.1.1" } }, "sha512-EbPf1/z1m8K/Jl4qXggiMQwfdqXLF25sj+d5SHaGl6T0auTOYMayN578Rb/qM4fjhhlBQwub919Zsq9syYeYUA=="], + + "@mintlify/previewing": ["@mintlify/previewing@4.0.1197", "", { "dependencies": { "@mintlify/common": "1.0.985", "@mintlify/prebuild": "1.0.1131", "@mintlify/validation": "0.1.769", "adm-zip": "0.5.16", "better-opn": "3.0.2", "chalk": "5.2.0", "chokidar": "3.5.3", "express": "4.22.0", "front-matter": "4.0.2", "fs-extra": "11.1.0", "got": "13.0.0", "ink": "6.3.0", "ink-spinner": "5.0.0", "is-online": "10.0.0", "js-yaml": "4.1.1", "openapi-types": "12.1.3", "react": "19.2.3", "socket.io": "4.8.0", "tar": "7.5.15", "unist-util-visit": "4.1.2", "yargs": "17.7.1" } }, "sha512-q4TunK8KjE1k9ve5nOAQTvBpsE7bX+RJN/ozx7RdIyQSCSexpkywmSM6nE3ECJyjUWFy8pg6yrYYbUQLHN/oww=="], + + "@mintlify/scraping": ["@mintlify/scraping@4.0.849", "", { "dependencies": { "@mintlify/common": "1.0.985", "@mintlify/openapi-parser": "0.0.8", "fs-extra": "11.1.1", "hast-util-to-mdast": "10.1.0", "js-yaml": "4.1.1", "mdast-util-mdx-jsx": "3.1.3", "neotraverse": "0.6.18", "puppeteer": "22.14.0", "rehype-parse": "9.0.1", "remark-gfm": "4.0.0", "remark-mdx": "3.0.1", "remark-parse": "11.0.0", "remark-stringify": "11.0.0", "unified": "11.0.5", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "3.24.0" }, "bin": { "mintlify-scrape": "bin/cli.js" } }, "sha512-4aMltLtfSU5rkUJt2SCVaYIbuggiEnx7lMWKM8NW93SaZUeoL0iKNbo0K24SmOqS7kYATRRH8zI7gefPX2ZLDw=="], + + "@mintlify/validation": ["@mintlify/validation@0.1.769", "", { "dependencies": { "@mintlify/mdx": "3.0.4", "@mintlify/models": "0.0.333", "arktype": "2.1.27", "fractional-indexing": "3.2.0", "js-yaml": "4.1.1", "lcm": "0.0.3", "lodash": "4.18.1", "neotraverse": "0.6.18", "object-hash": "3.0.0", "openapi-types": "12.1.3", "uuid": "11.1.1", "zod": "3.24.0", "zod-to-json-schema": "3.20.4" } }, "sha512-8Sg6DCdQ7RSc3NIyaSSWy7G6KaAuw0NfUSBCYLwGhtp6dYPh1jEsPn6YU8hqUMNZn1OQHsA52rA6lZQxjnyLHQ=="], + "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], @@ -1858,6 +2067,30 @@ "@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/canvas": ["@napi-rs/canvas@1.0.2", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "1.0.2", "@napi-rs/canvas-darwin-arm64": "1.0.2", "@napi-rs/canvas-darwin-x64": "1.0.2", "@napi-rs/canvas-linux-arm-gnueabihf": "1.0.2", "@napi-rs/canvas-linux-arm64-gnu": "1.0.2", "@napi-rs/canvas-linux-arm64-musl": "1.0.2", "@napi-rs/canvas-linux-riscv64-gnu": "1.0.2", "@napi-rs/canvas-linux-x64-gnu": "1.0.2", "@napi-rs/canvas-linux-x64-musl": "1.0.2", "@napi-rs/canvas-win32-arm64-msvc": "1.0.2", "@napi-rs/canvas-win32-x64-msvc": "1.0.2" } }, "sha512-EYEqlMYaCbpZDz+IgDH5xp9MTd3ui4dmGqbQYryhMLnSRxrhHKq5KQWHHKxFUcEP4Hp8/BWgvqXocX4j7iSbOQ=="], + + "@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@1.0.2", "", { "os": "android", "cpu": "arm64" }, "sha512-IMXKVQod0ol4vt3gmClUfXz4JAgHYESGPCUqmH3lQxBoL0K/2greJaQE1HVBVxWWFKfLc4OLZVdxg7kXVyXv+g=="], + + "@napi-rs/canvas-darwin-arm64": ["@napi-rs/canvas-darwin-arm64@1.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sc8tPi6cF+5lqOzCCKFALJHhDiRwyMzTPYm3bbhdXsOunU0lQO5f05ucyOzN2r55I23Hg5bsjH63uSCvWp3EgQ=="], + + "@napi-rs/canvas-darwin-x64": ["@napi-rs/canvas-darwin-x64@1.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-niDXZ9LhKB1zLrUdYB64RHQFDGz9rr0eGx061qtJJU3U20EMMIx28ADF5fVYbhtOgkWQrBjFicfaye1yM0U62A=="], + + "@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/canvas-linux-arm-gnueabihf@1.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-sgatQL9JxGRH/Amzcvu0P3t8Am3duou74CisfuJ41Dwt8cWy723z/9KZ8LlgmxfypEwEZxSTNFJtU8d281lmhQ=="], + + "@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/canvas-linux-arm64-gnu@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgKuX0peF3xwY6ZF5QxGS4wbfDqpoFAJYXiLSp+guZKARQUKMkRqZSDrXKj7nfrec3UCMzC0PFCPte0ES98AiA=="], + + "@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/canvas-linux-arm64-musl@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-qwROoDIC9upfvDoRLuPn2aNg9CGW1x0Ygr4k2Or+8paA9d0qBLwk87U+g8KQpoOviKoPoiwl97kvBYuYD7qZoA=="], + + "@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/canvas-linux-riscv64-gnu@1.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-fXRjnPihdnbO6qy1QQOgxAonb68A0TCEG7rj1x7v7rxNElsE8EVIKIEUTvyDtU+sthYSbX+8e7g3oZiLGnOmxw=="], + + "@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/canvas-linux-x64-gnu@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-nPR97DXhbWIAy7yazF3jc06kEPMqYMLmPzFOVNlwKPfIoSChnI+x7dc0hTLaihz3jxrjL6j4BbA7earxfx4X3g=="], + + "@napi-rs/canvas-linux-x64-musl": ["@napi-rs/canvas-linux-x64-musl@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-l7zZY5+jL5qnBZtDz7CoBtY6p7EkHu422g/0zWwrOrzIwWyWxZFRfZZORY1UG7YApymPLx+UbOkN206xXn/c1Q=="], + + "@napi-rs/canvas-win32-arm64-msvc": ["@napi-rs/canvas-win32-arm64-msvc@1.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-yE0koHCFF4PIbMc2o2SEALhnipz7WBISh5glLvQiomtIoCcW0np3H4Lw93ceJAfJttTTeIIWFbwH84F7EVzjMQ=="], + + "@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@1.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-okU8/t2foV6C31n0GtvEMbfD5rOFc70+/6xUNME9Guld29sgSOIGUEDScAWFlcP3k5TYQRl9TNkwJEEjh15w8A=="], + "@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=="], "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], @@ -1944,8 +2177,20 @@ "@one-ini/wasm": ["@one-ini/wasm@0.1.1", "", {}, "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="], + "@oozcitak/dom": ["@oozcitak/dom@2.0.2", "", { "dependencies": { "@oozcitak/infra": "^2.0.2", "@oozcitak/url": "^3.0.0", "@oozcitak/util": "^10.0.0" } }, "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w=="], + + "@oozcitak/infra": ["@oozcitak/infra@2.0.2", "", { "dependencies": { "@oozcitak/util": "^10.0.0" } }, "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA=="], + + "@oozcitak/url": ["@oozcitak/url@3.0.0", "", { "dependencies": { "@oozcitak/infra": "^2.0.2", "@oozcitak/util": "^10.0.0" } }, "sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ=="], + + "@oozcitak/util": ["@oozcitak/util@10.0.0", "", {}, "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA=="], + + "@openapi-contrib/openapi-schema-to-json-schema": ["@openapi-contrib/openapi-schema-to-json-schema@3.2.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" } }, "sha512-Gj6C0JwCr8arj0sYuslWXUBSP/KnUlEGnPW4qxlXvAl543oaNQgMgIgkQUA6vs5BCCvwTEiL8m/wdWzfl4UvSw=="], + "@openauthjs/openauth": ["@openauthjs/openauth@0.0.0-20250322224806", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-p5IWSRXvABcwocH2dNI0w8c1QJelIOFulwhKk+aLLFfUbs8u1pr7kQbYe8yCSM2+bcLHiwbogpUQc2ovrGwCuw=="], + "@opencode-ai/ai": ["@opencode-ai/ai@workspace:packages/ai"], + "@opencode-ai/app": ["@opencode-ai/app@workspace:packages/app"], "@opencode-ai/cli": ["@opencode-ai/cli@workspace:packages/cli"], @@ -1970,6 +2215,8 @@ "@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"], + "@opencode-ai/docs": ["@opencode-ai/docs@workspace:packages/docs"], + "@opencode-ai/effect-drizzle-sqlite": ["@opencode-ai/effect-drizzle-sqlite@workspace:packages/effect-drizzle-sqlite"], "@opencode-ai/effect-sqlite-node": ["@opencode-ai/effect-sqlite-node@workspace:packages/effect-sqlite-node"], @@ -1982,8 +2229,6 @@ "@opencode-ai/httpapi-codegen": ["@opencode-ai/httpapi-codegen@workspace:packages/httpapi-codegen"], - "@opencode-ai/llm": ["@opencode-ai/llm@workspace:packages/llm"], - "@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"], "@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"], @@ -2000,6 +2245,8 @@ "@opencode-ai/session-ui": ["@opencode-ai/session-ui@workspace:packages/session-ui"], + "@opencode-ai/simulation": ["@opencode-ai/simulation@workspace:packages/simulation"], + "@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"], "@opencode-ai/stats-app": ["@opencode-ai/stats-app@workspace:packages/stats/app"], @@ -2016,6 +2263,8 @@ "@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"], + "@opencode-ai/www": ["@opencode-ai/www@workspace:packages/www"], + "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.9.0", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Seva+NCa0WUQnJIUE5GzHsUv1WTIeyqwz0ELl2VtS6NP+eF+77yCXGFVOMbvoCM7QMjlnhv7931e89R+8pJdcQ=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], @@ -2066,6 +2315,8 @@ "@opentui/solid": ["@opentui/solid@0.4.5", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.4.5", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-B0RSkXnrtPVfEJOX+Hj+axjLJ3lzbG1BZw5I7Pvb9OPp48Vzg2cW2a3cSa86/q48ndLt647i/XwFPIw/jqnI5g=="], + "@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="], + "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], "@oslojs/binary": ["@oslojs/binary@1.0.0", "", {}, "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ=="], @@ -2146,7 +2397,7 @@ "@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-project/types": ["@oxc-project/types@0.138.0", "", {}, "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA=="], "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.20.0", "", { "os": "android", "cpu": "arm" }, "sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg=="], @@ -2340,6 +2591,8 @@ "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], + "@posthog/core": ["@posthog/core@1.7.1", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-kjK0eFMIpKo9GXIbts8VtAknsoZ18oZorANdtuTj1CbgS28t4ZVq//HAWhnxEuXRTrtkd+SUJ6Ux3j2Af8NCuA=="], + "@preact/signals-core": ["@preact/signals-core@1.14.3", "", {}, "sha512-m0K3vnbSLC5rHs2ZVfeAMvBtT1zIyq4mxx5OlNncSgMj5Iz6W5Rn3kPrDxAC+iIKmiVe0lSl6U37t5ZkEWoVAw=="], "@protobuf-ts/plugin": ["@protobuf-ts/plugin@2.11.1", "", { "dependencies": { "@bufbuild/protobuf": "^2.4.0", "@bufbuild/protoplugin": "^2.4.0", "@protobuf-ts/protoc": "^2.11.1", "@protobuf-ts/runtime": "^2.11.1", "@protobuf-ts/runtime-rpc": "^2.11.1", "typescript": "^3.9" }, "bin": { "protoc-gen-ts": "bin/protoc-gen-ts", "protoc-gen-dump": "bin/protoc-gen-dump" } }, "sha512-HyuprDcw0bEEJqkOWe1rnXUP0gwYLij8YhPuZyZk6cJbIgc/Q0IFgoHQxOXNIXAcXM4Sbehh6kjVnCzasElw1A=="], @@ -2370,21 +2623,29 @@ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="], + "@puppeteer/browsers": ["@puppeteer/browsers@2.3.0", "", { "dependencies": { "debug": "^4.3.5", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.4.0", "semver": "^7.6.3", "tar-fs": "^3.0.6", "unbzip2-stream": "^1.4.3", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA=="], + "@radix-ui/colors": ["@radix-ui/colors@1.0.1", "", {}, "sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg=="], + "@radix-ui/number": ["@radix-ui/number@1.1.2", "", {}, "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig=="], + "@radix-ui/primitive": ["@radix-ui/primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw=="], + "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collapsible": "1.1.16", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-BpZJNmetujnGgUI6OX0jEhEmlA46WPqgub8Rv09Kyquwd0cc1ndMKpiPYCjmBU6KSSRPAMtgLpEoZSG/tdNIWQ=="], + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA=="], "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-id": "1.0.1", "@radix-ui/react-presence": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-controllable-state": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg=="], - "@radix-ui/react-collection": ["@radix-ui/react-collection@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-slot": "1.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.12", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw=="], "@radix-ui/react-context": ["@radix-ui/react-context@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg=="], - "@radix-ui/react-direction": ["@radix-ui/react-direction@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA=="], + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.12", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+HhbN2+YtkRgVirjZ2afMeutQRuGOrdkWR5+EFC58SJojGmtyNQwYzgi6tHBpOxvFHefMtPeHdgtjz0BOGxFQg=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA=="], "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.0.4", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-callback-ref": "1.0.1", "@radix-ui/react-use-escape-keydown": "1.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg=="], @@ -2394,34 +2655,46 @@ "@radix-ui/react-id": ["@radix-ui/react-id@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ=="], + "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.18", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-K9HiuxZ6xCwSaHcIuUpxyhy4w5gpwzWjh9dHTSbMN3Ix4qAyVObS9RlU3zMycb0PO3v9Tpk0BXMwWvXOUbVXew=="], + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.0.6", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-dismissable-layer": "1.0.4", "@radix-ui/react-focus-guards": "1.0.1", "@radix-ui/react-focus-scope": "1.0.3", "@radix-ui/react-id": "1.0.1", "@radix-ui/react-popper": "1.1.2", "@radix-ui/react-portal": "1.0.3", "@radix-ui/react-presence": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-slot": "1.0.2", "@radix-ui/react-use-controllable-state": "1.0.1", "aria-hidden": "^1.1.1", "react-remove-scroll": "2.5.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cZ4defGpkZ0qTRtlIBzJLSzL6ht7ofhhW4i1+pkemjV1IKXm0wgCRnee154qlV6r9Ttunmh2TNZhMfV2bavUyA=="], "@radix-ui/react-popper": ["@radix-ui/react-popper@1.1.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.0.3", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-callback-ref": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1", "@radix-ui/react-use-rect": "1.0.1", "@radix-ui/react-use-size": "1.0.1", "@radix-ui/rect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg=="], "@radix-ui/react-portal": ["@radix-ui/react-portal@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA=="], - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg=="], + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.7", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA=="], "@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g=="], "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.0.4", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-collection": "1.0.3", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-direction": "1.0.1", "@radix-ui/react-id": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-callback-ref": "1.0.1", "@radix-ui/react-use-controllable-state": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ=="], + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.14", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bBODCWZK7JTbQLHs0uIP4f73wIWatakK4OS33UzkR1x897wu0PuO658a3f+6P2GEGyDzGYMuHRatMVoAk9WZTw=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg=="], + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-roving-focus": "1.1.15", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-nRyXnrAVCwjeXcHbvEbLS6ndbTeKHG1RqCP4A8Gw5L4cemDzPXdD8rAmr6wet0v57R69wGvuIIsFjHSVkZiMzQ=="], + "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-controllable-state": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg=="], "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.0.4", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-direction": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-roving-focus": "1.0.4", "@radix-ui/react-toggle": "1.0.3", "@radix-ui/react-use-controllable-state": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A=="], "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.0.6", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-dismissable-layer": "1.0.4", "@radix-ui/react-id": "1.0.1", "@radix-ui/react-popper": "1.1.2", "@radix-ui/react-portal": "1.0.3", "@radix-ui/react-presence": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-slot": "1.0.2", "@radix-ui/react-use-controllable-state": "1.0.1", "@radix-ui/react-visually-hidden": "1.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-DmNFOiwEc2UDigsYj6clJENma58OelxD24O4IODoZ+3sQc3Zb+L8w1EP+y9laTuKCLAysPw4fD6/v0j4KNV8rg=="], - "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA=="], + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.3", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA=="], + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg=="], + "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A=="], + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ=="], + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw=="], + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/rect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw=="], "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g=="], @@ -2434,7 +2707,37 @@ "@remix-run/router": ["@remix-run/router@1.9.0", "", {}, "sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.4", "", { "os": "android", "cpu": "arm64" }, "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.4", "", { "os": "linux", "cpu": "arm" }, "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.4", "", { "os": "none", "cpu": "arm64" }, "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.4", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], "@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=="], @@ -2488,6 +2791,8 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw=="], + "@scarf/scarf": ["@scarf/scarf@1.4.0", "", {}, "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="], + "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], "@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.36.0", "", { "dependencies": { "@sentry/core": "10.36.0" } }, "sha512-WILVR8HQBWOxbqLRuTxjzRCMIACGsDTo6jXvzA8rz6ezElElLmIrn3CFAswrESLqEEUa4CQHl5bLgSVJCRNweA=="], @@ -2544,6 +2849,8 @@ "@shikijs/transformers": ["@shikijs/transformers@3.9.2", "", { "dependencies": { "@shikijs/core": "3.9.2", "@shikijs/types": "3.9.2" } }, "sha512-MW5hT4TyUp6bNAgTExRYLk1NNasVQMTCw1kgbxHcEC0O5cbepPWaB+1k+JzW9r3SP2/R8kiens8/3E6hGKfgsA=="], + "@shikijs/twoslash": ["@shikijs/twoslash@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0", "twoslash": "^0.3.6" }, "peerDependencies": { "typescript": ">=5.5.0" } }, "sha512-pNaLJWMA3LU7PhT8tm9OQBZ1epy0jmdgeJzntBtr1EVXLbHxGzTj3mnf9vOdcl84l96qnlJXkJ/NGXZYBpXl5g=="], + "@shikijs/types": ["@shikijs/types@3.9.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw=="], "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], @@ -2564,6 +2871,10 @@ "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], + "@sindresorhus/slugify": ["@sindresorhus/slugify@2.2.0", "", { "dependencies": { "@sindresorhus/transliterate": "^1.0.0", "escape-string-regexp": "^5.0.0" } }, "sha512-9Vybc/qX8Kj6pxJaapjkFbiUJPk7MAkCh/GFCxIBnnsuYCFPIXKvnLidG8xlepht3i24L5XemUmGtrJ3UWrl6w=="], + + "@sindresorhus/transliterate": ["@sindresorhus/transliterate@1.6.0", "", { "dependencies": { "escape-string-regexp": "^5.0.0" } }, "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ=="], + "@slack/bolt": ["@slack/bolt@3.22.0", "", { "dependencies": { "@slack/logger": "^4.0.0", "@slack/oauth": "^2.6.3", "@slack/socket-mode": "^1.3.6", "@slack/types": "^2.13.0", "@slack/web-api": "^6.13.0", "@types/express": "^4.16.1", "@types/promise.allsettled": "^1.0.3", "@types/tsscmp": "^1.0.0", "axios": "^1.7.4", "express": "^4.21.0", "path-to-regexp": "^8.1.0", "promise.allsettled": "^1.0.2", "raw-body": "^2.3.3", "tsscmp": "^1.0.6" } }, "sha512-iKDqGPEJDnrVwxSVlFW6OKTkijd7s4qLBeSufoBsTM0reTyfdp/5izIQVkxNfzjHi3o6qjdYbRXkYad5HBsBog=="], "@slack/logger": ["@slack/logger@4.0.1", "", { "dependencies": { "@types/node": ">=18" } }, "sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ=="], @@ -2582,7 +2893,7 @@ "@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-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="], "@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=="], @@ -2656,7 +2967,7 @@ "@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-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-waiter": ["@smithy/util-waiter@4.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-EYviebytZE6vplW0AGwZ2Rc3sNuVR83lfUCNZu11VchUiKhMwJqrRWy7iVDTNEwG/vEwItno591Iad6/prj6Bw=="], @@ -2680,7 +2991,7 @@ "@solid-primitives/media": ["@solid-primitives/media@2.3.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-hQ4hLOGvfbugQi5Eu1BFWAIJGIAzztq9x0h02xgBGl2l0Jaa3h7tg6bz5tV1NSuNYVGio4rPoa7zVQQLkkx9dA=="], - "@solid-primitives/memo": ["@solid-primitives/memo@1.5.0", "", { "dependencies": { "@solid-primitives/scheduled": "^1.5.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-nSMHpFdnOMP88t7lqtktUXJlhQdJk0BQs2v3jUqcn+OtFwUm27Oa/coKl1BHDxL/65jR5drjqxxvuhaIpiUi0w=="], + "@solid-primitives/memo": ["@solid-primitives/memo@1.5.1", "", { "dependencies": { "@solid-primitives/scheduled": "^1.5.3", "@solid-primitives/utils": "^6.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-VDPrkl9epp0tbby9MvsqphGFCYCtDRC5J8FKzTqHbQiG5hhR8n6xv4MfjhTW231IaBxxPHLxS43EE8c5Q23mSQ=="], "@solid-primitives/props": ["@solid-primitives/props@3.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-XzG6en9gSFwmvbKcATm2BxL63HegZ+BAG5fmHi8jyBppQHcaths7ffz+6vYvwYy3nlgLa20ufJLj7tst+PcHFA=="], @@ -2718,7 +3029,37 @@ "@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.9", "", { "peerDependencies": { "@standard-community/standard-json": "^0.3.5", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "effect": "^3.17.14", "openapi-types": "^12.1.3", "sury": "^10.0.0", "typebox": "^1.0.0", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-openapi": "^4" }, "optionalPeers": ["arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-openapi"] }, "sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg=="], - "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@stoplight/better-ajv-errors": ["@stoplight/better-ajv-errors@1.0.3", "", { "dependencies": { "jsonpointer": "^5.0.0", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA=="], + + "@stoplight/json": ["@stoplight/json@3.21.0", "", { "dependencies": { "@stoplight/ordered-object-literal": "^1.0.3", "@stoplight/path": "^1.3.2", "@stoplight/types": "^13.6.0", "jsonc-parser": "~2.2.1", "lodash": "^4.17.21", "safe-stable-stringify": "^1.1" } }, "sha512-5O0apqJ/t4sIevXCO3SBN9AHCEKKR/Zb4gaj7wYe5863jme9g02Q0n/GhM7ZCALkL+vGPTe4ZzTETP8TFtsw3g=="], + + "@stoplight/json-ref-readers": ["@stoplight/json-ref-readers@1.2.2", "", { "dependencies": { "node-fetch": "^2.6.0", "tslib": "^1.14.1" } }, "sha512-nty0tHUq2f1IKuFYsLM4CXLZGHdMn+X/IwEUIpeSOXt0QjMUbL0Em57iJUDzz+2MkWG83smIigNZ3fauGjqgdQ=="], + + "@stoplight/json-ref-resolver": ["@stoplight/json-ref-resolver@3.1.6", "", { "dependencies": { "@stoplight/json": "^3.21.0", "@stoplight/path": "^1.3.2", "@stoplight/types": "^12.3.0 || ^13.0.0", "@types/urijs": "^1.19.19", "dependency-graph": "~0.11.0", "fast-memoize": "^2.5.2", "immer": "^9.0.6", "lodash": "^4.17.21", "tslib": "^2.6.0", "urijs": "^1.19.11" } }, "sha512-YNcWv3R3n3U6iQYBsFOiWSuRGE5su1tJSiX6pAPRVk7dP0L7lqCteXGzuVRQ0gMZqUl8v1P0+fAKxF6PLo9B5A=="], + + "@stoplight/ordered-object-literal": ["@stoplight/ordered-object-literal@1.0.5", "", {}, "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg=="], + + "@stoplight/path": ["@stoplight/path@1.3.2", "", {}, "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ=="], + + "@stoplight/spectral-core": ["@stoplight/spectral-core@1.23.1", "", { "dependencies": { "@scarf/scarf": "^1.4.0", "@stoplight/better-ajv-errors": "1.0.3", "@stoplight/json": "~3.21.0", "@stoplight/path": "1.3.2", "@stoplight/spectral-parsers": "^1.0.0", "@stoplight/spectral-ref-resolver": "^1.0.4", "@stoplight/spectral-runtime": "^1.1.2", "@stoplight/types": "~13.6.0", "@types/es-aggregate-error": "^1.0.2", "@types/json-schema": "^7.0.11", "ajv": "^8.18.0", "ajv-errors": "~3.0.0", "ajv-formats": "~2.1.1", "es-aggregate-error": "^1.0.7", "expr-eval-fork": "^3.0.1", "jsonpath-plus": "^10.3.0", "lodash": "^4.18.1", "lodash.topath": "^4.5.2", "minimatch": "^3.1.4", "nimma": "0.2.3", "pony-cause": "^1.1.1", "tslib": "^2.8.1" } }, "sha512-VLC8OhpO/pMJKb6IHhurxJjXO1qB56Ng1unIb8b+hNxdw0+SEcASvmR+RpjfHYX/jv/DfSaA1x8QhFBJBmqBOQ=="], + + "@stoplight/spectral-formats": ["@stoplight/spectral-formats@1.8.5", "", { "dependencies": { "@scarf/scarf": "^1.4.0", "@stoplight/json": "^3.17.0", "@stoplight/spectral-core": "^1.23.0", "@types/json-schema": "^7.0.7", "tslib": "^2.8.1" } }, "sha512-xaC0rCH0p7/bzNJsz+JgLSj+Cp6uwYGWpePQxdLkF2G6a8Zyp3OyS7umkGYNiimEwKrOjvCNNTFJpeuiENZSBA=="], + + "@stoplight/spectral-functions": ["@stoplight/spectral-functions@1.10.5", "", { "dependencies": { "@scarf/scarf": "^1.4.0", "@stoplight/better-ajv-errors": "1.0.3", "@stoplight/json": "^3.17.1", "@stoplight/spectral-core": "^1.23.0", "@stoplight/spectral-formats": "^1.8.1", "@stoplight/spectral-runtime": "^1.1.2", "ajv": "^8.18.0", "ajv-draft-04": "~1.0.0", "ajv-errors": "~3.0.0", "ajv-formats": "~2.1.1", "lodash": "^4.18.1", "tslib": "^2.8.1" } }, "sha512-vDCd0NJ93715bcUpZZ5vNHiyxd4cgHF6tuXsDiXOXKAByg+I1fR5/dMijEo6Ce1Lz95a+RZ22JKYhF1YuzVvuA=="], + + "@stoplight/spectral-parsers": ["@stoplight/spectral-parsers@1.0.5", "", { "dependencies": { "@stoplight/json": "~3.21.0", "@stoplight/types": "^14.1.1", "@stoplight/yaml": "~4.3.0", "tslib": "^2.8.1" } }, "sha512-ANDTp2IHWGvsQDAY85/jQi9ZrF4mRrA5bciNHX+PUxPr4DwS6iv4h+FVWJMVwcEYdpyoIdyL+SRmHdJfQEPmwQ=="], + + "@stoplight/spectral-ref-resolver": ["@stoplight/spectral-ref-resolver@1.0.5", "", { "dependencies": { "@stoplight/json-ref-readers": "1.2.2", "@stoplight/json-ref-resolver": "~3.1.6", "@stoplight/spectral-runtime": "^1.1.2", "dependency-graph": "0.11.0", "tslib": "^2.8.1" } }, "sha512-gj3TieX5a9zMW29z3mBlAtDOCgN3GEc1VgZnCVlr5irmR4Qi5LuECuFItAq4pTn5Zu+sW5bqutsCH7D4PkpyAA=="], + + "@stoplight/spectral-runtime": ["@stoplight/spectral-runtime@1.1.6", "", { "dependencies": { "@stoplight/json": "^3.20.1", "@stoplight/path": "^1.3.2", "@stoplight/types": "^13.6.0", "lodash": "^4.18.1", "node-fetch": "^2.7.0", "tslib": "^2.8.1" } }, "sha512-Y8rEDyMN4bSMJCrDs2shdcVHYyCnH3FvXRP4dBhha4Z8iJv+JPp7KqOV/hwVB/hWFC209upiwj2oDmLfR0qCDg=="], + + "@stoplight/types": ["@stoplight/types@13.20.0", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA=="], + + "@stoplight/yaml": ["@stoplight/yaml@4.3.0", "", { "dependencies": { "@stoplight/ordered-object-literal": "^1.0.5", "@stoplight/types": "^14.1.1", "@stoplight/yaml-ast-parser": "0.0.50", "tslib": "^2.2.0" } }, "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w=="], + + "@stoplight/yaml-ast-parser": ["@stoplight/yaml-ast-parser@0.0.50", "", {}, "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ=="], "@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=="], @@ -2778,9 +3119,29 @@ "@tanstack/directive-functions-plugin": ["@tanstack/directive-functions-plugin@1.134.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/router-utils": "1.133.19", "babel-dead-code-elimination": "^1.0.10", "pathe": "^2.0.3", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "vite": ">=6.0.0 || >=7.0.0" } }, "sha512-J3oawV8uBRBbPoLgMdyHt+LxzTNuWRKNJJuCLWsm/yq6v0IQSvIVCgfD2+liIiSnDPxGZ8ExduPXy8IzS70eXw=="], + "@tanstack/history": ["@tanstack/history@1.162.0", "", {}, "sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA=="], + "@tanstack/query-core": ["@tanstack/query-core@5.91.2", "", {}, "sha512-Uz2pTgPC1mhqrrSGg18RKCWT/pkduAYtxbcyIyKBhw7dTWjXZIzqmpzO2lBkyWr4hlImQgpu1m1pei3UnkFRWw=="], - "@tanstack/router-utils": ["@tanstack/router-utils@1.133.19", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.5", "@babel/preset-typescript": "^7.27.1", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-WEp5D2gPxvlLDRXwD/fV7RXjYtqaqJNXKB/L6OyZEbT+9BG/Ib2d7oG9GSUZNNMGPGYAlhBUOi3xutySsk6rxA=="], + "@tanstack/react-router": ["@tanstack/react-router@1.170.17", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.171.14", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-ppLkjCfSMaeug9rmFRYzOd4TIqWV+yTE7tzIny7alJsSnM7w4lzEZm6eqCehG0SPetpZ0R3K+UnanSmBgOAVcQ=="], + + "@tanstack/react-start": ["@tanstack/react-start@1.168.27", "", { "dependencies": { "@tanstack/react-router": "1.170.17", "@tanstack/react-start-client": "1.168.15", "@tanstack/react-start-rsc": "0.1.26", "@tanstack/react-start-server": "1.167.21", "@tanstack/router-utils": "1.162.2", "@tanstack/start-client-core": "1.170.13", "@tanstack/start-plugin-core": "1.171.19", "@tanstack/start-server-core": "1.169.16", "pathe": "^2.0.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "vite"] }, "sha512-rdGFDqfCW71gyofyAxaYxhelNKmeVjpmbpm0uFYbNHORCa///4aBxi7B7ecShibKv9O4GfJ66MPX5F0ozbm+ig=="], + + "@tanstack/react-start-client": ["@tanstack/react-start-client@1.168.15", "", { "dependencies": { "@tanstack/react-router": "1.170.17", "@tanstack/router-core": "1.171.14", "@tanstack/start-client-core": "1.170.13" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-pW50PHvadgi50iNCw6deUOvqc9rzs30SstyFZY2tcS9z1XlqTlELSvGowjxdu2m0ymtqm1emj1jau1iP7+3+PQ=="], + + "@tanstack/react-start-rsc": ["@tanstack/react-start-rsc@0.1.26", "", { "dependencies": { "@tanstack/react-router": "1.170.17", "@tanstack/router-core": "1.171.14", "@tanstack/router-utils": "1.162.2", "@tanstack/start-client-core": "1.170.13", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-plugin-core": "1.171.19", "@tanstack/start-server-core": "1.169.16", "@tanstack/start-storage-context": "1.167.16", "pathe": "^2.0.3" }, "peerDependencies": { "@rspack/core": ">=2.0.0-0", "@vitejs/plugin-rsc": ">=0.5.20", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "react-server-dom-rspack": ">=0.0.2" }, "optionalPeers": ["@rspack/core", "@vitejs/plugin-rsc", "react-server-dom-rspack"] }, "sha512-+FMm3qtT1gWsl0i5sG/Q70mh1k7tzZUsPBaqbg4v34zVJZ+XGn1mJb34x2w7z1M0/e7co6GkZfV8BRpczrs8UA=="], + + "@tanstack/react-start-server": ["@tanstack/react-start-server@1.167.21", "", { "dependencies": { "@tanstack/react-router": "1.170.17", "@tanstack/router-core": "1.171.14", "@tanstack/start-server-core": "1.169.16" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-puJ7eFxaLuDzeM/tiLDJaXCA4uK+PZnEOoIC73zipsFqx865MGzrRS/GSZxeVxjavC5iHU+ZwC+rgI0qYSol1A=="], + + "@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "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-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="], + + "@tanstack/router-core": ["@tanstack/router-core@1.171.14", "", { "dependencies": { "@tanstack/history": "1.162.0", "cookie-es": "^3.0.0", "seroval": "^1.5.4", "seroval-plugins": "^1.5.4" } }, "sha512-Mo3hwx0qB0cJsVYGDjG0+Ouf7VV74h/vsoDMGztdlyzDanp4gBA2s7IVvm6hFrmQM6GpD9F0Z7SqD7OldfLE7g=="], + + "@tanstack/router-generator": ["@tanstack/router-generator@1.167.18", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.14", "@tanstack/router-utils": "1.162.2", "@tanstack/virtual-file-routes": "1.162.0", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.5.0", "zod": "^4.4.3" } }, "sha512-kFvM4caRds9Q3EXg64bZubJ6rbDxyV0YDSBSGvOGzmKspQPdz5Xrh0uj5T1Ov8avUUg+c761u04VQAaEzSBXRw=="], + + "@tanstack/router-plugin": ["@tanstack/router-plugin@1.168.19", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.14", "@tanstack/router-generator": "1.167.18", "@tanstack/router-utils": "1.162.2", "chokidar": "^5.0.0", "unplugin": "^3.0.0", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.170.17", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-aFglwLc+bbPTgZlkXn3PvOwpjJAfgUyPGSuql4MP3XrqTTh6WkBiy2RYb6oaG5h0s7EKwivEuq85K3Y4V0Mt1g=="], + + "@tanstack/router-utils": ["@tanstack/router-utils@1.162.2", "", { "dependencies": { "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ=="], "@tanstack/server-functions-plugin": ["@tanstack/server-functions-plugin@1.134.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/directive-functions-plugin": "1.134.5", "babel-dead-code-elimination": "^1.0.9", "tiny-invariant": "^1.3.3" } }, "sha512-2sWxq70T+dOEUlE3sHlXjEPhaFZfdPYlWTSkHchWXrFGw2YOAa+hzD6L9wHMjGDQezYd03ue8tQlHG+9Jzbzgw=="], @@ -2788,8 +3149,22 @@ "@tanstack/solid-virtual": ["@tanstack/solid-virtual@3.13.32", "", { "dependencies": { "@tanstack/virtual-core": "3.17.3" }, "peerDependencies": { "solid-js": "^1.3.0" } }, "sha512-yhX4A4Kgn+wyTg6Mmu8+zwoMTwjz4K1ucvLfRJ8f0rPGDDAIqSaf0v6oU0yT9+SvrjmUaZQ0VX7g4byexbhNng=="], + "@tanstack/start-client-core": ["@tanstack/start-client-core@1.170.13", "", { "dependencies": { "@tanstack/router-core": "1.171.14", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-storage-context": "1.167.16", "seroval": "^1.5.4" } }, "sha512-o37M3msIK5ec87kPrIYJWXb1XPnjIe5/jrkGLXiXpFuVL99z7mhoBCzftKtVPtzqI8EElnRE/VGFYT9BHNnWcw=="], + + "@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.162.0", "", {}, "sha512-QWfUZ3Yo923tdQn38LyKMU8rcTw69zc+T4dAvgTWV4O56SqFRsGfS0lSWIMhJRwXIx/bvdi7nTUBDdZtTHtpTQ=="], + + "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.171.19", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.14", "@tanstack/router-generator": "1.167.18", "@tanstack/router-plugin": "1.168.19", "@tanstack/router-utils": "1.162.2", "@tanstack/start-server-core": "1.169.16", "exsolve": "^1.0.7", "lightningcss": "^1.32.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "seroval": "^1.5.4", "source-map": "^0.7.6", "srvx": "^0.11.9", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "vite"] }, "sha512-+fpW3Z/2vPT8HDV1c5p2WC6/g2k/AV/ujdJVDcn/VFd+gXRtzSX1D/LfozlaDbhDoEsqOnAk/mGwjg60JkUA2Q=="], + + "@tanstack/start-server-core": ["@tanstack/start-server-core@1.169.16", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/router-core": "1.171.14", "@tanstack/start-client-core": "1.170.13", "@tanstack/start-storage-context": "1.167.16", "fetchdts": "^0.1.6", "h3-v2": "npm:h3@2.0.1-rc.20", "seroval": "^1.5.4" } }, "sha512-lvAjQpH3nHJtd4xy0iHIaWbsTbyN9EBxuYCxbtXH0EpeBQPg+TCPhu9GQC9WbbA1rE//s82CpE55oYDQMqkU5A=="], + + "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.167.16", "", { "dependencies": { "@tanstack/router-core": "1.171.14" } }, "sha512-zTegxlij4BC1DbCrC6rsVlMOQVMzOuG5IllacZEkrUdhiFwMIMYpk0VWGH+d0ucx5RBkmv8e8GNX3AOVBWclfg=="], + + "@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="], + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.3", "", {}, "sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw=="], + "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.162.0", "", {}, "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA=="], + "@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=="], @@ -2798,6 +3173,8 @@ "@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="], + "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], + "@tsconfig/bun": ["@tsconfig/bun@1.0.9", "", {}, "sha512-4M0/Ivfwcpz325z6CwSifOBZYji3DFOEpY6zEUt0+Xi2qRhzwvmqQN9XAHJh3OVvRJuAqVTLU2abdCplvp6mwQ=="], "@tsconfig/node22": ["@tsconfig/node22@22.0.2", "", {}, "sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA=="], @@ -2820,6 +3197,8 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + "@types/acorn": ["@types/acorn@4.0.6", "", { "dependencies": { "@types/estree": "*" } }, "sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ=="], + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -2844,6 +3223,8 @@ "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], + "@types/cross-spawn": ["@types/cross-spawn@6.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA=="], "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], @@ -2856,6 +3237,8 @@ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/es-aggregate-error": ["@types/es-aggregate-error@1.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], @@ -2892,7 +3275,7 @@ "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], - "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + "@types/mdx": ["@types/mdx@2.0.14", "", {}, "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg=="], "@types/micromatch": ["@types/micromatch@4.0.10", "", { "dependencies": { "@types/braces": "*" } }, "sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ=="], @@ -2930,7 +3313,9 @@ "@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/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], @@ -2962,6 +3347,8 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/urijs": ["@types/urijs@1.19.26", "", {}, "sha512-wkXrVzX5yoqLnndOwFsieJA7oKM8cNkOKJtf/3vVGSUFkWDKZvFHpIl9Pvqb/T9UsawBBFMTTD8xu7sK5MWuvg=="], + "@types/verror": ["@types/verror@1.10.11", "", {}, "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg=="], "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], @@ -3002,9 +3389,15 @@ "@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="], + "@vercel/cli-config": ["@vercel/cli-config@0.2.0", "", { "dependencies": { "xdg-app-paths": "5", "zod": "4.1.11" } }, "sha512-fJRRRB7734BDuXZ89yBEaA2ncYhH7bWX30mk04W80J6VAfQc+4iB8lyzAdaGpFV3/vNlkt9VZt+/uoQoWX6UsQ=="], + + "@vercel/cli-exec": ["@vercel/cli-exec@1.0.0", "", { "dependencies": { "execa": "5.1.1" } }, "sha512-kQF8LGie/Hbdq9/psJxLE7owRTcqMQMhgybU04gCeR7cbQAr5t8OrjefDNColJv1QSSucFt4pLwRiARVmlOnug=="], + + "@vercel/functions": ["@vercel/functions@3.7.5", "", { "dependencies": { "@vercel/oidc": "3.8.0" }, "peerDependencies": { "@aws-sdk/credential-provider-web-identity": "*", "ws": ">=8" }, "optionalPeers": ["@aws-sdk/credential-provider-web-identity", "ws"] }, "sha512-ESf8BbeDebqRUyMi09JwRbQqpLn4g6fjcVVHPsHB56j2dSqRrSHO4h3X4aaxJf6iQQjzhAtDGI2xCWQ27JE8PA=="], + "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.3", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg=="], "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.8", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.8", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.8", "vitest": "4.1.8" }, "optionalPeers": ["@vitest/browser"] }, "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw=="], @@ -3052,7 +3445,7 @@ "abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="], - "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -3060,10 +3453,16 @@ "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], + "address": ["address@1.2.2", "", {}, "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA=="], + + "adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + "aggregate-error": ["aggregate-error@4.0.1", "", { "dependencies": { "clean-stack": "^4.0.0", "indent-string": "^5.0.0" } }, "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w=="], + "ai": ["ai@6.0.168", "", { "dependencies": { "@ai-sdk/gateway": "3.0.104", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ=="], "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=="], @@ -3072,6 +3471,8 @@ "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], + "ajv-errors": ["ajv-errors@3.0.0", "", { "peerDependencies": { "ajv": "^8.0.1" } }, "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ=="], + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], @@ -3080,6 +3481,8 @@ "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -3108,6 +3511,10 @@ "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + "arkregex": ["arkregex@0.0.3", "", { "dependencies": { "@ark/util": "0.55.0" } }, "sha512-bU21QJOJEFJK+BPNgv+5bVXkvRxyAvgnon75D92newgHxkBJTgiFwQxusyViYyJkETsddPlHyspshDQcCzmkNg=="], + + "arktype": ["arktype@2.1.27", "", { "dependencies": { "@ark/schema": "0.55.0", "@ark/util": "0.55.0", "arkregex": "0.0.3" } }, "sha512-enctOHxI4SULBv/TDtCVi5M8oLd4J5SVlPUblXDzSsOYQNMzmVbUosGBnJuZDKmFlN5Ie0/QVEuTE+Z5X1UhsQ=="], + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], @@ -3152,10 +3559,14 @@ "atomically": ["atomically@2.1.1", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ=="], + "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], + "autoprefixer": ["autoprefixer@10.5.0", "", { "dependencies": { "browserslist": "^4.28.2", "caniuse-lite": "^1.0.30001787", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + "avsc": ["avsc@5.7.9", "", {}, "sha512-yOA4wFeI7ET3v32Di/sUybQ+ttP20JHSW3mxLuNGeO0uD6PPcvLrIQXSvy/rhJOWU5JrYh7U4OHplWMmtAtjMg=="], + "avvio": ["avvio@9.2.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ=="], "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], @@ -3200,14 +3611,20 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.33", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw=="], + "basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="], + "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=="], "bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="], "before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="], + "better-opn": ["better-opn@3.0.2", "", { "dependencies": { "open": "^8.0.4" } }, "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ=="], + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], "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=="], @@ -3216,13 +3633,15 @@ "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "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=="], "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], - "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=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="], @@ -3286,6 +3705,8 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "camel-case": ["camel-case@4.1.2", "", { "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw=="], "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], @@ -3310,6 +3731,8 @@ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + "chardet": ["chardet@2.2.0", "", {}, "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA=="], + "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], @@ -3322,22 +3745,32 @@ "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + "chromium-bidi": ["chromium-bidi@0.6.2", "", { "dependencies": { "mitt": "3.0.1", "urlpattern-polyfill": "10.0.0", "zod": "3.23.8" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-4WVBa6ijmUTVr9cZD4eicQD8Mdy/HCX3bzEIYYpmk0glqYLoWH+LqQEvV9RpDRzoQSbY1KJHloYXbDMXMbDPhg=="], + "chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="], "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + "classnames": ["classnames@2.3.2", "", {}, "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="], "clean-css": ["clean-css@5.3.3", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="], + "clean-stack": ["clean-stack@4.2.0", "", { "dependencies": { "escape-string-regexp": "5.0.0" } }, "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg=="], + "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], + "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], + "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=="], + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + "clipboardy": ["clipboardy@4.0.0", "", { "dependencies": { "execa": "^8.0.1", "is-wsl": "^3.1.0", "is64bit": "^2.0.0" } }, "sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w=="], "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], @@ -3354,10 +3787,16 @@ "cmd-shim": ["cmd-shim@8.0.0", "", {}, "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="], + "cnfast": ["cnfast@0.0.8", "", { "bin": { "cnfast": "bin/cli.js" } }, "sha512-EjXKMfGfdwtV4AcNSQ6AwQaVzpC1B7IxeiwA3FlhTXz+YFlMKVi4c1JX9tgD2QOlahQXjB8KUXrBaYG+3v871Q=="], + + "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], + "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + "color-blend": ["color-blend@4.0.0", "", {}, "sha512-fYODTHhI/NG+B5GnzvuL3kiFrK/UnkUezWFTgEPBTY5V+kpyfAn95Vn9sJeeCX6omrCOdxnqCL3CvH+6sXtIbw=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], @@ -3378,6 +3817,8 @@ "compress-commons": ["compress-commons@6.0.2", "", { "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^6.0.0", "is-stream": "^2.0.1", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg=="], + "compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "condense-newlines": ["condense-newlines@0.2.1", "", { "dependencies": { "extend-shallow": "^2.0.1", "is-whitespace": "^0.3.0", "kind-of": "^3.0.2" } }, "sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg=="], @@ -3390,22 +3831,26 @@ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "cookie-es": ["cookie-es@2.0.1", "", {}, "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA=="], - "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + "cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="], + "crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="], "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], @@ -3432,6 +3877,8 @@ "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + "cssfilter": ["cssfilter@0.0.10", "", {}, "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], @@ -3466,12 +3913,18 @@ "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], + "decode-bmp": ["decode-bmp@0.2.1", "", { "dependencies": { "@canvas/image-data": "^1.0.0", "to-data-view": "^1.1.0" } }, "sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA=="], + + "decode-ico": ["decode-ico@0.4.1", "", { "dependencies": { "@canvas/image-data": "^1.0.0", "decode-bmp": "^0.2.0", "to-data-view": "^1.1.0" } }, "sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA=="], + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], @@ -3488,12 +3941,16 @@ "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + "degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "dependency-graph": ["dependency-graph@0.11.0", "", {}, "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg=="], + "deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -3502,18 +3959,22 @@ "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], - "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "detect-port": ["detect-port@1.5.1", "", { "dependencies": { "address": "^1.0.1", "debug": "4" }, "bin": { "detect": "bin/detect-port.js", "detect-port": "bin/detect-port.js" } }, "sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ=="], + "deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="], "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=="], + "devtools-protocol": ["devtools-protocol@0.0.1312386", "", {}, "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA=="], + "dfa": ["dfa@1.2.0", "", {}, "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="], "diacritics": ["diacritics@1.3.0", "", {}, "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA=="], @@ -3536,6 +3997,8 @@ "dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="], + "dns-socket": ["dns-socket@4.2.2", "", { "dependencies": { "dns-packet": "^5.2.4" } }, "sha512-BDeBd8najI4/lS00HSKpdFia+OvUMytaVjfzR9n5Lq8MlZRSvtbI+uLtx1+XmQFls5wFU9dssccTmQQ6nfpjdg=="], + "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], @@ -3574,7 +4037,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "effect": ["effect@4.0.0-beta.83", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w=="], + "effect": ["effect@4.0.0-beta.98", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-oz+bsG5h+6RNrw4t5GMfQrk/xBS8ROoqkYsuvRhBr5O7mCOrpvH/hbw+QrDzvKIpX4HJClwm86F94c87W0sJxg=="], "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], @@ -3618,6 +4081,8 @@ "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "engine.io": ["engine.io@6.6.9", "", { "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "@types/ws": "^8.5.12", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.21.0" } }, "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg=="], + "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=="], @@ -3628,14 +4093,20 @@ "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], "es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="], + "es-aggregate-error": ["es-aggregate-error@1.0.14", "", { "dependencies": { "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "globalthis": "^1.0.4", "has-property-descriptors": "^1.0.2", "set-function-name": "^2.0.2" } }, "sha512-3YxX6rVb07B5TV11AV5wsL7nQCHXNwoHPsQC8S4AmBiqYhyNCJ5BRKXkXyDJvs8QzXN20NgRtxe3dEEQD9NLHA=="], + "es-array-method-boxes-properly": ["es-array-method-boxes-properly@1.0.0", "", {}, "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -3652,6 +4123,8 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + "es-toolkit": ["es-toolkit@1.49.0", "", {}, "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g=="], + "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], @@ -3670,8 +4143,12 @@ "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], @@ -3682,10 +4159,14 @@ "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], + "estree-util-value-to-estree": ["estree-util-value-to-estree@3.5.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ=="], + "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], @@ -3704,11 +4185,15 @@ "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], - "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=="], + "expr-eval-fork": ["expr-eval-fork@3.0.3", "", {}, "sha512-BhC+hbc5lIVjygr840n5DEkW3MQq7H9o+mc1/N7Z5uIiCFVyESLL5DIE7LNq4CYUNxy+XjA+3jRrL/h0Kt2xcg=="], + + "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=="], "express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="], @@ -3728,7 +4213,7 @@ "extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="], - "fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="], + "fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="], "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], @@ -3742,6 +4227,8 @@ "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-memoize": ["fast-memoize@2.5.2", "", {}, "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw=="], + "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.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], @@ -3756,17 +4243,23 @@ "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + "fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="], + + "favicons": ["favicons@7.2.0", "", { "dependencies": { "escape-html": "^1.0.3", "sharp": "^0.33.1", "xml2js": "^0.6.1" } }, "sha512-k/2rVBRIRzOeom3wI9jBPaSEvoTSQEW4iM0EveBmBBKFxO8mSyyRWtDlfC3VnEfu0avmjrMzy8/ZFPSe6F71Hw=="], + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "fetchdts": ["fetchdts@0.1.7", "", {}, "sha512-YoZjBdafyLIop9lSxXVI33oLD5kN31q4Td+CasofLLYeLXRFeOsuOw0Uo+XNRi9PZlbfdlN2GmRtm4tCEQ9/KA=="], + "filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="], @@ -3794,6 +4287,8 @@ "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], + "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], + "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], @@ -3802,9 +4297,15 @@ "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], + "fractional-indexing": ["fractional-indexing@3.2.0", "", {}, "sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ=="], + "framer-motion": ["framer-motion@8.5.5", "", { "dependencies": { "@motionone/dom": "^10.15.3", "hey-listen": "^1.0.8", "tslib": "^2.4.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-5IDx5bxkjWHWUF3CVJoSyUVOtrbAxtzYBBowRE2uYI/6VYhkEBD+rbTHEGuUmbGHRj6YqqSfoG7Aa1cLyWCrBA=="], - "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "front-matter": ["front-matter@4.0.2", "", { "dependencies": { "js-yaml": "^3.13.1" } }, "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg=="], + + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], @@ -3814,6 +4315,12 @@ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "fumadocs-core": ["fumadocs-core@16.11.1", "", { "dependencies": { "@orama/orama": "^3.1.18", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "js-yaml": "^5.2.1", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "remark": "^15.0.1", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.3.1", "tinyglobby": "^0.2.17", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@mdx-js/mdx": "*", "@mixedbread/sdk": "0.x.x", "@orama/core": "1.x.x", "@oramacloud/client": "2.x.x", "@tanstack/react-router": "1.x.x", "@types/estree-jsx": "*", "@types/hast": "*", "@types/mdast": "*", "@types/react": "*", "algoliasearch": "5.x.x", "flexsearch": "*", "lucide-react": "*", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router": "7.x.x || 8.x.x", "waku": "*", "zod": "4.x.x" }, "optionalPeers": ["@mdx-js/mdx", "@mixedbread/sdk", "@orama/core", "@oramacloud/client", "@tanstack/react-router", "@types/estree-jsx", "@types/hast", "@types/mdast", "@types/react", "algoliasearch", "flexsearch", "lucide-react", "next", "react", "react-dom", "react-router", "waku", "zod"] }, "sha512-tKuh1AKoVTb+f7IoAOM2cfz5djd3YhePeqA95q6mf422gEvDTeJms23OJ+icYRWZ6ryNQ5W/ZsgKEe87M5HVYg=="], + + "fumadocs-mdx": ["fumadocs-mdx@15.1.0", "", { "dependencies": { "@mdx-js/mdx": "^3.1.1", "@standard-schema/spec": "^1.1.0", "chokidar": "^5.0.0", "esbuild": "^0.28.1", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "js-yaml": "^5.2.1", "mdast-util-mdx": "^3.0.0", "picocolors": "^1.1.1", "picomatch": "^4.0.5", "tinyexec": "^1.2.4", "tinyglobby": "^0.2.17", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3", "zod": "^4.4.3" }, "peerDependencies": { "@fumadocs/satteri": "0.x.x", "@types/mdast": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "^16.7.0", "mdast-util-directive": "*", "next": "^15.3.0 || ^16.0.0", "react": "^19.2.0", "rolldown": "*", "satteri": "^0.9.4", "vite": "7.x.x || 8.x.x" }, "optionalPeers": ["@fumadocs/satteri", "@types/mdast", "@types/mdx", "@types/react", "mdast-util-directive", "next", "react", "rolldown", "satteri", "vite"], "bin": { "fumadocs-mdx": "./bin.js" } }, "sha512-2nDusSlYFuNVcyB51jgY3tA3r01ALTwoURrMDNoc7cbJKZ2sac/PW+CDq6SHTArkgRMmFiKYQGfspJdjgTtPTg=="], + + "fumadocs-ui": ["fumadocs-ui@16.11.1", "", { "dependencies": { "@fuma-translate/react": "^1.0.2", "@fumadocs/tailwind": "0.1.0", "@radix-ui/react-accordion": "^1.2.15", "@radix-ui/react-collapsible": "^1.1.15", "@radix-ui/react-dialog": "^1.1.18", "@radix-ui/react-direction": "^1.1.2", "@radix-ui/react-navigation-menu": "^1.2.17", "@radix-ui/react-popover": "^1.1.18", "@radix-ui/react-presence": "^1.1.6", "@radix-ui/react-scroll-area": "^1.2.13", "@radix-ui/react-slot": "^1.3.0", "@radix-ui/react-tabs": "^1.1.16", "class-variance-authority": "^0.7.1", "cnfast": "^0.0.8", "lucide-react": "^1.23.0", "motion": "^12.42.2", "next-themes": "^0.4.6", "react-remove-scroll": "^2.7.2", "rehype-raw": "^7.0.0", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.3.1", "unist-util-visit": "^5.1.0" }, "peerDependencies": { "@takumi-rs/image-response": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "16.11.1", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0" }, "optionalPeers": ["@takumi-rs/image-response", "@types/mdx", "@types/react", "next"] }, "sha512-Dq819PFV4RGhAI9Wd4erSCiRlEDLVOZae+kgE5LeOKFH8mbKX49U8N17ldFOhdkC9EZpxMZdEKul77RDgFHQww=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], @@ -3824,6 +4331,8 @@ "gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="], + "gcd": ["gcd@0.0.1", "", {}, "sha512-VNx3UEGr+ILJTiMs1+xc5SX1cMgJCrXezKPa003APUWNqQqaF6n25W8VcR7nHN6yRWbvvUTwCpZCFJeWC2kXlw=="], + "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], @@ -3850,13 +4359,17 @@ "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#83c0a07", {}, "anomalyco-ghostty-web-83c0a07", "sha512-Lf2v1agHkVUpMpHBWWuCZrhOEmcwwin5/Hboc9rZwQ7/CKkIh5rU1r1CvfLlhkMoFv+ed8z52RZ8hkzGZZj3MQ=="], + "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="], + + "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#513463a", {}, "anomalyco-ghostty-web-513463a", "sha512-GZR8LSmgGzViWnBJrqRI8MpAZRCJxhcr1Hi9Tyeh7YRooHZQjK9J97FQRD3tbBaM2wjq05gzGY2UEsG+JtZeBw=="], "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "gitlab-ai-provider": ["gitlab-ai-provider@6.12.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-Qn5iHqvjG8yktI5MWaUgdRR94l7O4WtYW0CAbhsCh1Tj0Fei/DeprOYPVyf4Nht1Ix6U2PXSYM32QOHI6Z2TDw=="], + "gitlab-ai-provider": ["gitlab-ai-provider@6.11.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-SJ6f5qa7P8md6lPrserryER3zerLkrezlnqqYQ2AbvDPpHLbwtbyk0FYJ5kNRcmbI80i/VMcsMBP0YIRdc3ucQ=="], "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], @@ -3890,6 +4403,8 @@ "h3": ["h3@2.0.1-rc.4", "", { "dependencies": { "rou3": "^0.7.8", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw=="], + "h3-v2": ["h3@2.0.1-rc.20", "", { "dependencies": { "rou3": "^0.8.1", "srvx": "^0.11.13" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg=="], + "happy-dom": ["happy-dom@20.9.0", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ=="], "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], @@ -3910,8 +4425,12 @@ "hast-util-format": ["hast-util-format@1.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-whitespace": "^3.0.0", "html-whitespace-sensitive-tag-names": "^3.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA=="], + "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="], + "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], + "hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="], + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], "hast-util-has-property": ["hast-util-has-property@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA=="], @@ -3938,6 +4457,8 @@ "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + "hast-util-to-mdast": ["hast-util-to-mdast@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-to-html": "^9.0.0", "hast-util-to-text": "^4.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "mdast-util-to-string": "^4.0.0", "rehype-minify-whitespace": "^6.0.0", "trim-trailing-lines": "^2.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-DsL/SvCK9V7+vfc6SLQ+vKIyBDXTk2KLSbfBYkH4zeF/uR1yBajHRhkzuaUSGOB1WJSTieJBdHwxlC+HLKvZZw=="], + "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], @@ -3952,6 +4473,8 @@ "heap-snapshot-toolkit": ["heap-snapshot-toolkit@1.1.3", "", {}, "sha512-joThu2rEsDu8/l4arupRDI1qP4CZXNG+J6Wr348vnbLGSiBkwRdqZ6aOHl5BzEiC+Dc8OTbMlmWjD0lbXD5K2Q=="], + "hex-rgb": ["hex-rgb@5.0.0", "", {}, "sha512-NQO+lgVUCtHxZ792FodgW0zflK+ozS9X9dwGp9XvvmPlH7pyxd588cn24TD3rmPm/N0AIRXF10Otah8yKqGw4w=="], + "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="], "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], @@ -3996,6 +4519,8 @@ "i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="], + "ico-endec": ["ico-endec@0.1.6", "", {}, "sha512-ZdLU38ZoED3g1j3iEyzcQj+wAkY2xfWNkymszfJPoxucIUhK7NayQ+/C4Kv0nDFMIsbtbEHldv3V8PU494/ueQ=="], + "iconv-corefoundation": ["iconv-corefoundation@1.1.7", "", { "dependencies": { "cli-truncate": "^2.1.0", "node-addon-api": "^1.6.3" }, "os": "darwin" }, "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ=="], "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], @@ -4008,6 +4533,8 @@ "immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + "import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="], "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], @@ -4020,8 +4547,14 @@ "ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="], + "ink": ["ink@6.3.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.0", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.32.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^7.2.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^4.19.1" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-2CbJAa7XeziZYe6pDS5RVLirRY28iSGMQuEV8jRU5NQsONQNfcR/BZHHc9vkMg2lGYTHTM2pskxC1YmY28p6bQ=="], + + "ink-spinner": ["ink-spinner@5.0.0", "", { "dependencies": { "cli-spinners": "^2.7.0" }, "peerDependencies": { "ink": ">=4.0.0", "react": ">=18.0.0" } }, "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA=="], + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + "inquirer": ["inquirer@12.3.0", "", { "dependencies": { "@inquirer/core": "^10.1.2", "@inquirer/prompts": "^7.2.1", "@inquirer/type": "^3.0.2", "ansi-escapes": "^4.3.2", "mute-stream": "^2.0.0", "run-async": "^3.0.0", "rxjs": "^7.8.1" }, "peerDependencies": { "@types/node": ">=18" } }, "sha512-3NixUXq+hM8ezj2wc7wC37b32/rHq1MwNZDYdvx+d6jokOD+r+i8Q4Pkylh9tISYP114A128LCX8RKhopC5RfQ=="], + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], @@ -4030,6 +4563,8 @@ "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], + "ip-regex": ["ip-regex@4.3.0", "", {}, "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q=="], + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], @@ -4082,10 +4617,14 @@ "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + "is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="], + "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], "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-ip": ["is-ip@3.1.0", "", { "dependencies": { "ip-regex": "^4.0.0" } }, "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q=="], + "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=="], @@ -4094,6 +4633,8 @@ "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + "is-online": ["is-online@10.0.0", "", { "dependencies": { "got": "^12.1.0", "p-any": "^4.0.0", "p-timeout": "^5.1.0", "public-ip": "^5.0.0" } }, "sha512-WCPdKwNDjXJJmUubf2VHLMDBkUZEtuOvpXUfUnUFbEnM6In9ByiScL4f4jKACz/fsb2qDkesFerW3snf/AYz3A=="], + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], @@ -4132,6 +4673,8 @@ "isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="], + "isbot": ["isbot@5.2.0", "", {}, "sha512-gbZiGCb4B5xaoxg9mS7koAyRdvJnArk10VLSHOgz6rtBG93/pi1xOFaVvXMKZ7JXgyZ8zAbNRK5uIBdIUTFSqw=="], + "isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], @@ -4166,6 +4709,8 @@ "jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="], + "jsep": ["jsep@1.4.0", "", {}, "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], @@ -4198,6 +4743,10 @@ "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], + "jsonpath-plus": ["jsonpath-plus@10.4.0", "", { "dependencies": { "@jsep-plugin/assignment": "^1.3.0", "@jsep-plugin/regex": "^1.0.4", "jsep": "^1.4.0" }, "bin": { "jsonpath": "bin/jsonpath-cli.js", "jsonpath-plus": "bin/jsonpath-cli.js" } }, "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA=="], + + "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], + "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], "just-diff": ["just-diff@6.0.2", "", {}, "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA=="], @@ -4212,6 +4761,8 @@ "katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="], + "keytar": ["keytar@7.9.0", "", { "dependencies": { "node-addon-api": "^4.3.0", "prebuild-install": "^7.0.1" } }, "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], @@ -4230,12 +4781,18 @@ "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], + "lcm": ["lcm@0.0.3", "", { "dependencies": { "gcd": "^0.0.1" } }, "sha512-TB+ZjoillV6B26Vspf9l2L/vKaRY/4ep3hahcyVkCGFgsTNRUQdc24bQeNFiZeoxH0vr5+7SfNRMQuPHv/1IrQ=="], + "leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="], + "leven": ["leven@4.1.0", "", {}, "sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew=="], + "light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="], "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], @@ -4282,6 +4839,8 @@ "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + "lodash.topath": ["lodash.topath@4.5.2", "", {}, "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg=="], + "loglevelnext": ["loglevelnext@6.0.0", "", {}, "sha512-FDl1AI2sJGjHHG3XKJd6sG3/6ncgiGCQ0YkW46nxe7SfqQq6hujd9CvFXIXtkGBUN83KPZ2KSOJK8q5P0bSSRQ=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -4302,6 +4861,8 @@ "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], + "lucide-react": ["lucide-react@1.24.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA=="], + "luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], @@ -4338,6 +4899,8 @@ "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + "mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="], + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], @@ -4350,6 +4913,8 @@ "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="], + "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], @@ -4368,11 +4933,11 @@ "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], - "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="], - "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], @@ -4386,6 +4951,8 @@ "micromark-extension-directive": ["micromark-extension-directive@3.0.2", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA=="], + "micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="], + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], @@ -4400,9 +4967,11 @@ "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], + "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], - "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], + "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.1", "", { "dependencies": { "@types/acorn": "^4.0.0", "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg=="], "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], @@ -4488,8 +5057,14 @@ "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + "mint": ["mint@4.2.666", "", { "dependencies": { "@mintlify/cli": "4.0.1269" }, "bin": { "mint": "index.js" } }, "sha512-FsdL35EH++MiVDoKxN8M6/obOsrgx0Ko7P/1Y2lBXh/jEPx1UD1Y8msmAOW6cuwaqGIzAxuC7ySmr7D3G2bmBg=="], + + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], + "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="], "motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "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-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="], @@ -4502,7 +5077,7 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msgpackr": ["msgpackr@2.0.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ=="], + "msgpackr": ["msgpackr@2.0.4", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA=="], "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=="], @@ -4510,10 +5085,12 @@ "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=="], - "multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="], + "multipasta": ["multipasta@0.2.8", "", {}, "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q=="], "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], + "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], + "mysql2": ["mysql2@3.14.4", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w=="], "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], @@ -4524,12 +5101,22 @@ "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], + "netmask": ["netmask@2.1.1", "", {}, "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA=="], + + "next-mdx-remote-client": ["next-mdx-remote-client@1.1.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@types/mdx": "^2.0.13", "remark-mdx-remove-esm": "^1.3.2", "serialize-error": "^13.0.1", "vfile": "^6.0.3", "vfile-matter": "^5.0.1" }, "peerDependencies": { "react": ">= 18.3.0 < 19.0.0", "react-dom": ">= 18.3.0 < 19.0.0" } }, "sha512-IElOrn02JjGQZxx+re7wMx/1AUG+Arte9aDImAtxjAfMw6xuSCaH5mTCunKelkWzFyFdRb565jO8jRICvvh96g=="], + + "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], + "nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="], + "nimma": ["nimma@0.2.3", "", { "dependencies": { "@jsep-plugin/regex": "^1.0.1", "@jsep-plugin/ternary": "^1.0.2", "astring": "^1.8.1", "jsep": "^1.2.0" }, "optionalDependencies": { "jsonpath-plus": "^6.0.1 || ^10.1.0", "lodash.topath": "^4.5.2" } }, "sha512-1ZOI8J+1PKKGceo/5CT5GfQOG6H8I2BencSK06YarZ2wXwH37BSSUWldqJmMJYA5JfqDqffxDXynt6f11AyKcA=="], + "nitro": ["nitro@3.0.1-alpha.1", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.1", "db0": "^0.3.4", "h3": "2.0.1-rc.5", "jiti": "^2.6.1", "nf3": "^0.1.10", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.96.0", "oxc-transform": "^0.96.0", "srvx": "^0.9.5", "undici": "^7.16.0", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.4" }, "peerDependencies": { "rolldown": "*", "rollup": "^4", "vite": "^7", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-U4AxIsXxdkxzkFrK0XAw0e5Qbojk8jQ50MjjRBtBakC4HurTtQoiZvF+lSe382jhuQZCfAyywGWOFa9QzXLFaw=="], "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], @@ -4562,6 +5149,8 @@ "node-releases": ["node-releases@2.0.46", "", {}, "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ=="], + "non-error": ["non-error@0.1.0", "", {}, "sha512-TMB1uHiGsHRGv1uYclfhivcnf0/PdFp2pNqRxXjncaAsjYMoisaQJI+SSZCqRq+VliwRTC8tsMQfmrWjDMhkPQ=="], + "nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -4588,6 +5177,8 @@ "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=="], + "oauth4webapi": ["oauth4webapi@3.8.6", "", {}, "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], @@ -4634,6 +5225,8 @@ "opentui-spinner": ["opentui-spinner@0.0.7", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.3.4", "@opentui/react": "^0.3.4", "@opentui/solid": "^0.3.4", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-nPzwAvJG+y9rVEwwHLHqbsMzLnIk2zw+F9LqwA7aYJvpM5gsrKC2rrGi36A+tZpA+1RnWxXeWEgVZMchnaH18Q=="], + "os-paths": ["os-paths@4.4.0", "", {}, "sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg=="], + "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=="], @@ -4648,6 +5241,8 @@ "oxlint-tsgolint": ["oxlint-tsgolint@0.21.0", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.21.0", "@oxlint-tsgolint/darwin-x64": "0.21.0", "@oxlint-tsgolint/linux-arm64": "0.21.0", "@oxlint-tsgolint/linux-x64": "0.21.0", "@oxlint-tsgolint/win32-arm64": "0.21.0", "@oxlint-tsgolint/win32-x64": "0.21.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-HiWPhANwRnN1pZJQ2SgNB3WRR+1etLJHmRzQ/MJhyINsEIaOUCjxhlXJKbEaVUwdnyXwRWqo/P9Fx21lz0/mSg=="], + "p-any": ["p-any@4.0.0", "", { "dependencies": { "p-cancelable": "^3.0.0", "p-some": "^6.0.0" } }, "sha512-S/B50s+pAVe0wmEZHmBs/9yJXeZ5KhHzOsgKzt0hRdgkoR3DxW9ts46fcsWi/r3VnzsnkKS7q4uimze+zjdryw=="], + "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], "p-defer": ["p-defer@3.0.0", "", {}, "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw=="], @@ -4664,10 +5259,16 @@ "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], + "p-some": ["p-some@6.0.0", "", { "dependencies": { "aggregate-error": "^4.0.0", "p-cancelable": "^3.0.0" } }, "sha512-CJbQCKdfSX3fIh8/QKgS+9rjm7OBNUTmwWswAFQAhc8j1NR1dsEDETUEuVUtQHZpV+J03LqWBEwvu0g1Yn+TYg=="], + "p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="], + + "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="], + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], @@ -4680,10 +5281,14 @@ "param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parse-conflict-json": ["parse-conflict-json@5.0.1", "", { "dependencies": { "json-parse-even-better-errors": "^5.0.0", "just-diff": "^6.0.0", "just-diff-apply": "^5.2.0" } }, "sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ=="], "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="], "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], @@ -4698,6 +5303,8 @@ "pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="], + "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], @@ -4762,6 +5369,8 @@ "poe-oauth": ["poe-oauth@0.0.8", "", {}, "sha512-zlaRVLR6vuxBIYUkZoTIVo3f8h3qd27gv9Ms+kmGiYEiiV4TdccddTdNcGyI0DnuJ9tVi+5LP3Bvzez59IFbjw=="], + "pony-cause": ["pony-cause@1.1.1", "", {}, "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g=="], + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], "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=="], @@ -4782,6 +5391,8 @@ "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], + "posthog-node": ["posthog-node@5.17.2", "", { "dependencies": { "@posthog/core": "1.7.1" } }, "sha512-lz3YJOr0Nmiz0yHASaINEDHqoV+0bC3eD8aZAG+Ky292dAnVYul+ga/dMX8KCBXg8hHfKdxw0SztYD5j6dgUqQ=="], + "postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="], "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], @@ -4790,6 +5401,8 @@ "preact-render-to-string": ["preact-render-to-string@6.6.5", "", { "peerDependencies": { "preact": ">=10 || >= 11.0.0-0" } }, "sha512-O6MHzYNIKYaiSX3bOw0gGZfEbOmlIDtDfWwN1JJdc/T3ihzRT6tGGSEWE088dWrEDGa1u7101q+6fzQnO9XCPA=="], + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], "pretty": ["pretty@2.0.0", "", { "dependencies": { "condense-newlines": "^0.2.1", "extend-shallow": "^2.0.1", "js-beautify": "^1.6.12" } }, "sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w=="], @@ -4830,14 +5443,22 @@ "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=="], + "proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="], + "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], + "public-ip": ["public-ip@5.0.0", "", { "dependencies": { "dns-socket": "^4.2.2", "got": "^12.0.0", "is-ip": "^3.1.0" } }, "sha512-xaH3pZMni/R2BG7ZXXaWS9Wc9wFlhyDVJF47IJ+3ali0TGv+2PsckKxbmo+rnx3ZxiV2wblVhtdS3bohAP6GGw=="], + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "pupa": ["pupa@3.3.0", "", { "dependencies": { "escape-goat": "^4.0.0" } }, "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA=="], + "puppeteer": ["puppeteer@22.14.0", "", { "dependencies": { "@puppeteer/browsers": "2.3.0", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1312386", "puppeteer-core": "22.14.0" }, "bin": { "puppeteer": "lib/esm/puppeteer/node/cli.js" } }, "sha512-MGTR6/pM8zmWbTdazb6FKnwIihzsSEXBPH49mFFU96DNZpQOevCAZMnjBZGlZRGRzRK6aADCavR6SQtrbv5dQw=="], + + "puppeteer-core": ["puppeteer-core@22.14.0", "", { "dependencies": { "@puppeteer/browsers": "2.3.0", "chromium-bidi": "0.6.2", "debug": "^4.3.5", "devtools-protocol": "0.0.1312386", "ws": "^8.18.0" } }, "sha512-rl4tOY5LcA3e374GAlsGGHc05HL3eGNf5rZ+uxkl6id9zVZKcwcp1Z+Nd6byb6WPiPeecT/dwz8f/iUm+AZQSw=="], + "pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="], "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], @@ -4858,7 +5479,9 @@ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + "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=="], + + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], @@ -4866,13 +5489,15 @@ "react-docgen-typescript": ["react-docgen-typescript@2.4.0", "", { "peerDependencies": { "typescript": ">= 4.3.x" } }, "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg=="], - "react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + "react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="], + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], - "react-remove-scroll": ["react-remove-scroll@2.5.5", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.3", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", "use-sidecar": "^1.1.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw=="], + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], @@ -4930,6 +5555,10 @@ "rehype-format": ["rehype-format@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-format": "^1.0.0" } }, "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ=="], + "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="], + + "rehype-minify-whitespace": ["rehype-minify-whitespace@6.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0" } }, "sha512-Zk0pyQ06A3Lyxhe9vGtOtzz3Z0+qZ5+7icZ/PL/2x1SHPbKao5oB/g/rlc6BCTajqBb33JcOe71Ye1oFsuYbnw=="], + "rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="], "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], @@ -4940,12 +5569,20 @@ "relateurl": ["relateurl@0.2.7", "", {}, "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog=="], + "remark": ["remark@15.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A=="], + "remark-directive": ["remark-directive@3.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", "micromark-extension-directive": "^3.0.0", "unified": "^11.0.0" } }, "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A=="], + "remark-frontmatter": ["remark-frontmatter@5.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0", "unified": "^11.0.0" } }, "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ=="], + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + "remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="], + "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], + "remark-mdx-remove-esm": ["remark-mdx-remove-esm@1.3.2", "", { "dependencies": { "@types/mdast": "^4.0.4", "unist-util-remove": "^4.0.0" }, "peerDependencies": { "unified": "^11" } }, "sha512-BvL8VSdVXy9S7NlHP56nUJAHFc45h5E9HnHiLUGHe5tw3Yvm/3cVZvAzlkEEh2i+fkq2uKrf2xn5VmItBhMypA=="], + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], @@ -4978,8 +5615,12 @@ "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="], + "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], + "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], + "restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="], "ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="], @@ -5002,6 +5643,8 @@ "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=="], + "rolldown": ["rolldown@1.1.4", "", { "dependencies": { "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.4", "@rolldown/binding-darwin-arm64": "1.1.4", "@rolldown/binding-darwin-x64": "1.1.4", "@rolldown/binding-freebsd-x64": "1.1.4", "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", "@rolldown/binding-linux-arm64-gnu": "1.1.4", "@rolldown/binding-linux-arm64-musl": "1.1.4", "@rolldown/binding-linux-ppc64-gnu": "1.1.4", "@rolldown/binding-linux-s390x-gnu": "1.1.4", "@rolldown/binding-linux-x64-gnu": "1.1.4", "@rolldown/binding-linux-x64-musl": "1.1.4", "@rolldown/binding-openharmony-arm64": "1.1.4", "@rolldown/binding-wasm32-wasi": "1.1.4", "@rolldown/binding-win32-arm64-msvc": "1.1.4", "@rolldown/binding-win32-x64-msvc": "1.1.4" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA=="], + "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=="], @@ -5010,8 +5653,12 @@ "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], + "run-async": ["run-async@3.0.0", "", {}, "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + "s-js": ["s-js@0.4.9", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="], "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=="], @@ -5032,7 +5679,9 @@ "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], - "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="], "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="], @@ -5044,7 +5693,7 @@ "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], - "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], @@ -5054,7 +5703,7 @@ "seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="], - "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + "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=="], "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], @@ -5068,6 +5717,8 @@ "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], + "sharp-ico": ["sharp-ico@0.1.5", "", { "dependencies": { "decode-ico": "*", "ico-endec": "*", "sharp": "*" } }, "sha512-a3jODQl82NPp1d5OYb0wY+oFaPk7AvyxipIowCHk7pBsZCWgbe0yAkU2OOXdoH0ENyANhyOQbs9xkAiRHcF02Q=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], @@ -5090,6 +5741,10 @@ "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-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="], @@ -5106,6 +5761,10 @@ "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], + "socket.io": ["socket.io@4.8.0", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA=="], + + "socket.io-adapter": ["socket.io-adapter@2.5.8", "", { "dependencies": { "debug": "~4.4.1", "ws": "~8.21.0" } }, "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw=="], + "socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="], "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=="], @@ -5128,8 +5787,6 @@ "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], - "solid-sonner": ["solid-sonner@0.3.1", "", { "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-F/+zi9yKJTHh5hX1UGJfkDvyC+F34Vi3jgy44NJwOKCgic1QtAon0b1iT9OsDO77RTgR+PCil+3Y5B8T2Owy1Q=="], - "solid-stripe": ["solid-stripe@0.8.1", "", { "peerDependencies": { "@stripe/stripe-js": ">=1.44.1 <8.0.0", "solid-js": "^1.6.0" } }, "sha512-l2SkWoe51rsvk9u1ILBRWyCHODZebChSGMR6zHYJTivTRC0XWrRnNNKs5x1PYXsaIU71KYI6ov5CZB5cOtGLWw=="], "solid-transition-size": ["solid-transition-size@0.1.4", "", { "dependencies": { "@corvu/utils": "~0.3.2" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-ocHVnbfy23CgfaH4cEUR/AFg0Y3CEL8Oh3n9Qv8OHFJgPh+zkmERKZQfi/xH5XvxDCizg8VjPrVUhiHB1Gza8g=="], @@ -5184,6 +5841,8 @@ "sst-win32-x86": ["sst-win32-x86@4.13.1", "", { "os": "win32", "cpu": "none" }, "sha512-YPxBVdac/MsrzwlC6pF0NrrvMcmfdBLYjv7MbzHc5jNh1FQ1WPh6bdWQqgv0KD9EQTNLLEkej0beydgUvcCWJg=="], + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], @@ -5232,6 +5891,8 @@ "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "stripe": ["stripe@18.0.0", "", { "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.11.0" } }, "sha512-3Fs33IzKUby//9kCkCa1uRpinAoTvj6rJgQ2jrBEysoxEvfsclvXdna1amyEYbA2EKkjynuB4+L/kleCCaWTpA=="], "strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="], @@ -5266,6 +5927,8 @@ "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-fs": ["tar-fs@2.1.5", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw=="], + "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=="], @@ -5286,6 +5949,8 @@ "thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="], + "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], + "thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="], "tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="], @@ -5312,13 +5977,15 @@ "tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="], + "to-data-view": ["to-data-view@1.1.0", "", {}, "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ=="], + "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.1", "", {}, "sha512-5DXWzE4Vz7xNHsv+xQ+MGfJYyC78Aok3tEr0MNwHoRf7vZnga1mQXZ4/Nsodld4VR6Wd+VhfmqnNrsRJyYPfrQ=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="], + "toml": ["toml@4.3.0", "", {}, "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A=="], "toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.8", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-b+5ynEFp4Woe5a22hzNQm42lD23t13ZMihVxHbzjA50zdcM9aOSJTIjdJ0PDSd4/50HbBXcpHiQsz6rM4N88ww=="], @@ -5336,6 +6003,8 @@ "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + "trim-trailing-lines": ["trim-trailing-lines@2.1.0", "", {}, "sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg=="], + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="], @@ -5356,6 +6025,8 @@ "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + "turbo": ["turbo@2.10.2", "", { "optionalDependencies": { "@turbo/darwin-64": "2.10.2", "@turbo/darwin-arm64": "2.10.2", "@turbo/linux-64": "2.10.2", "@turbo/linux-arm64": "2.10.2", "@turbo/windows-64": "2.10.2", "@turbo/windows-arm64": "2.10.2" }, "bin": { "turbo": "bin/turbo" } }, "sha512-wTExrNrRjB8qzIcg+ZLm0A3GFNLDsWNwdS/RBXB0FPrBDyzk3i96Yx+TxWZC7a0k1SIreFB8ciUbxjmEqTH8IQ=="], "turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="], @@ -5364,9 +6035,13 @@ "tw-to-css": ["tw-to-css@0.0.12", "", { "dependencies": { "postcss": "8.4.31", "postcss-css-variables": "0.18.0", "tailwindcss": "3.3.2" } }, "sha512-rQAsQvOtV1lBkyCw+iypMygNHrShYAItES5r8fMsrhhaj5qrV2LkZyXc8ccEH+u5bFjHjQ9iuxe90I7Kykf6pw=="], + "twoslash": ["twoslash@0.3.9", "", { "dependencies": { "@typescript/vfs": "^1.6.4", "twoslash-protocol": "0.3.9" }, "peerDependencies": { "typescript": "^5.5.0 || ^6.0.0" } }, "sha512-rDclk+OtzuTX+tnea7DYLCkqGQ3eP0IyfD+kzUJ7t46X/NzlaxwrhecmEBNuSCuEn3V+n1PhcjUUQQ7gUJzX5Q=="], + + "twoslash-protocol": ["twoslash-protocol@0.3.9", "", {}, "sha512-9/iwp+CXOnjFMPQuPL5PkuRbZnDoNpBvtJCLs9t8kDYkL3YHujbvnHfZA1i5fApDftVEdBw+T/4F+dH5kIzpYQ=="], + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + "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=="], "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], @@ -5392,9 +6067,11 @@ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + "unbzip2-stream": ["unbzip2-stream@1.4.3", "", { "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg=="], + "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], - "undici": ["undici@8.3.0", "", {}, "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q=="], + "undici": ["undici@8.7.0", "", {}, "sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], @@ -5408,16 +6085,22 @@ "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=="], + "unist-builder": ["unist-builder@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg=="], + "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=="], + "unist-util-map": ["unist-util-map@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-HJs1tpkSmRJUzj6fskQrS5oYhBYlmtcvy4SepdDEEsL04FjBrgF0Mgggvxc1/qGBGgW7hRh9+UBK1aqTEnBpIA=="], + "unist-util-modify-children": ["unist-util-modify-children@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="], "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], + "unist-util-remove": ["unist-util-remove@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg=="], + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], @@ -5448,8 +6131,14 @@ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "uqr": ["uqr@0.1.3", "", {}, "sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "urijs": ["urijs@1.19.11", "", {}, "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ=="], + + "urlpattern-polyfill": ["urlpattern-polyfill@10.0.0", "", {}, "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg=="], + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], @@ -5460,9 +6149,11 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="], + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - "uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="], + "uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="], "valibot": ["valibot@1.4.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g=="], @@ -5478,6 +6169,8 @@ "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], + "vfile-matter": ["vfile-matter@5.0.1", "", { "dependencies": { "vfile": "^6.0.0", "yaml": "^2.0.0" } }, "sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw=="], + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], "vite": ["vite@7.1.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "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-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw=="], @@ -5582,16 +6275,24 @@ "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + "xdg-app-paths": ["xdg-app-paths@5.5.1", "", { "dependencies": { "os-paths": "^4.0.1", "xdg-portable": "^7.2.0" } }, "sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ=="], + "xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="], + "xdg-portable": ["xdg-portable@7.3.0", "", { "dependencies": { "os-paths": "^4.0.1" } }, "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw=="], + "xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="], "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=="], + "xmlbuilder2": ["xmlbuilder2@4.0.3", "", { "dependencies": { "@oozcitak/dom": "^2.0.2", "@oozcitak/infra": "^2.0.2", "@oozcitak/util": "^10.0.0", "js-yaml": "^4.1.1" } }, "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA=="], + "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="], + "xss": ["xss@1.0.15", "", { "dependencies": { "commander": "^2.20.3", "cssfilter": "0.0.10" }, "bin": { "xss": "bin/xss" } }, "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg=="], + "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -5614,6 +6315,10 @@ "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], + + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], @@ -5654,10 +6359,6 @@ "@ai-sdk/amazon-bedrock/@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/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="], - - "@ai-sdk/amazon-bedrock/@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=="], - "@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-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=="], @@ -5706,9 +6407,7 @@ "@ai-sdk/groq/@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/mistral/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="], - - "@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.40", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OL5IrpUm9Y8Dwy+w/vvFwPotS6m52O9W0op2oXgXdCROMJIBalBI0oro6OIBYkPxvm5Xg02GSkoQN25RlR0bnw=="], + "@ai-sdk/mistral/@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/openai/@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=="], @@ -5716,8 +6415,6 @@ "@ai-sdk/perplexity/@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/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/togetherai/@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/vercel/@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=="], @@ -5728,6 +6425,10 @@ "@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bjYld/2KGPLt78kpqbya+fD4LYS7BqVQJyUjE3qAHrYB0FR2Q90BaWEVIBZaguTWXf/A8L6uG1zO1v9TxVlGWg=="], + "@alcalzone/ansi-tokenize/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "@alcalzone/ansi-tokenize/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + "@astrojs/check/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=="], "@astrojs/cloudflare/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=="], @@ -5750,6 +6451,12 @@ "@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=="], + "@asyncapi/parser/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + + "@asyncapi/parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "@asyncapi/parser/node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="], + "@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.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], @@ -5768,26 +6475,18 @@ "@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/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.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.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/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], - "@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.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.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.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], - "@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=="], - "@aws-sdk/client-sts/@aws-sdk/core": ["@aws-sdk/core@3.775.0", "", { "dependencies": { "@aws-sdk/types": "3.775.0", "@smithy/core": "^3.2.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/signature-v4": "^5.0.2", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/util-middleware": "^4.0.2", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" } }, "sha512-8vpW4WihVfz0DX+7WnnLGm3GuQER++b0IwQG35JlQMlgqnc44M//KbJPsIHA0aJUJVwJAEShgfr5dUbY8WUzaA=="], "@aws-sdk/client-sts/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.782.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.775.0", "@aws-sdk/credential-provider-http": "3.775.0", "@aws-sdk/credential-provider-ini": "3.782.0", "@aws-sdk/credential-provider-process": "3.775.0", "@aws-sdk/credential-provider-sso": "3.782.0", "@aws-sdk/credential-provider-web-identity": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-HZiAF+TCEyKjju9dgysjiPIWgt/+VerGaeEp18mvKLNfgKz1d+/82A2USEpNKTze7v3cMFASx3CvL8yYyF7mJw=="], @@ -5810,10 +6509,6 @@ "@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/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.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=="], @@ -5862,10 +6557,6 @@ "@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.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/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=="], @@ -5932,40 +6623,166 @@ "@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.4.0", "", {}, "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ=="], + "@fuma-translate/react/react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + "@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=="], "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], "@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@hono/standard-validator/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "@jsx-email/cli/@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + "@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=="], + "@jsx-email/cli/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + "@jsx-email/cli/tailwindcss": ["tailwindcss@3.3.3", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.18.2", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w=="], "@jsx-email/cli/vite": ["vite@4.5.14", "", { "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", "rollup": "^3.27.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@types/node": ">= 14", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g=="], "@jsx-email/doiuse-email/htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="], + "@jsx-email/tailwind/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + "@kobalte/core/solid-presence": ["solid-presence@0.1.8", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA=="], "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + "@mdx-js/mdx/@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + "@mdx-js/mdx/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "@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=="], + "@mdx-js/react/@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + + "@mintlify/cli/chalk": ["chalk@5.2.0", "", {}, "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA=="], + + "@mintlify/cli/fs-extra": ["fs-extra@11.2.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw=="], + + "@mintlify/cli/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "@mintlify/cli/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + + "@mintlify/cli/openid-client": ["openid-client@6.8.2", "", { "dependencies": { "jose": "^6.1.3", "oauth4webapi": "^3.8.4" } }, "sha512-uOvTCndr4udZsKihJ68H9bUICrriHdUVJ6Az+4Ns6cW55rwM5h0bjVIzDz2SxgOI84LKjFyjOFvERLzdTUROGA=="], + + "@mintlify/cli/react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], + + "@mintlify/cli/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@mintlify/cli/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + + "@mintlify/cli/yargs": ["yargs@17.7.1", "", { "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-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw=="], + + "@mintlify/cli/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@mintlify/common/acorn": ["acorn@8.11.2", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w=="], + + "@mintlify/common/hast-util-to-html": ["hast-util-to-html@9.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA=="], + + "@mintlify/common/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "@mintlify/common/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], + + "@mintlify/common/mdast-util-gfm": ["mdast-util-gfm@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw=="], + + "@mintlify/common/mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.1.3", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ=="], + + "@mintlify/common/postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], + + "@mintlify/common/remark-gfm": ["remark-gfm@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA=="], + + "@mintlify/common/remark-mdx": ["remark-mdx@3.1.0", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA=="], + + "@mintlify/common/remark-rehype": ["remark-rehype@11.1.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ=="], + + "@mintlify/common/sucrase": ["sucrase@3.34.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "7.1.6", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw=="], + + "@mintlify/common/tailwindcss": ["tailwindcss@3.4.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.6", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og=="], + + "@mintlify/common/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + + "@mintlify/common/unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], + + "@mintlify/link-rot/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="], + + "@mintlify/link-rot/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], + + "@mintlify/mdx/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], + + "@mintlify/mdx/react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], + + "@mintlify/mdx/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + + "@mintlify/mdx/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=="], + + "@mintlify/prebuild/chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="], + + "@mintlify/prebuild/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="], + + "@mintlify/prebuild/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "@mintlify/prebuild/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], + + "@mintlify/prebuild/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="], + + "@mintlify/previewing/chalk": ["chalk@5.2.0", "", {}, "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA=="], + + "@mintlify/previewing/chokidar": ["chokidar@3.5.3", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw=="], + + "@mintlify/previewing/express": ["express@4.22.0", "", { "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-c2iPh3xp5vvCLgaHK03+mWLFPhox7j1LwyxcZwFVApEv5i0X+IjPpbT50SJJwwLpdBVfp45AkK/v+AFgv/XlfQ=="], + + "@mintlify/previewing/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="], + + "@mintlify/previewing/got": ["got@13.0.0", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA=="], + + "@mintlify/previewing/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "@mintlify/previewing/react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], + + "@mintlify/previewing/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], + + "@mintlify/previewing/yargs": ["yargs@17.7.1", "", { "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-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw=="], + + "@mintlify/scraping/fs-extra": ["fs-extra@11.1.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ=="], + + "@mintlify/scraping/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "@mintlify/scraping/mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.1.3", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ=="], + + "@mintlify/scraping/remark-gfm": ["remark-gfm@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA=="], + + "@mintlify/scraping/remark-mdx": ["remark-mdx@3.0.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-3Pz3yPQ5Rht2pM5R+0J2MrGoBSrzf+tJG94N+t/ilfdh8YLyyKYtidAYwTveB20BoHAcwIopOUqhcmh2F7hGYA=="], + + "@mintlify/scraping/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + + "@mintlify/scraping/yargs": ["yargs@17.7.1", "", { "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-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw=="], + + "@mintlify/scraping/zod": ["zod@3.24.0", "", {}, "sha512-Hz+wiY8yD0VLA2k/+nsg2Abez674dDGTai33SwNvMPuf9uIrBC9eFgIMQxBBbHFxVXi8W+5nX9DcAh9YNSQm/w=="], + + "@mintlify/validation/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "@mintlify/validation/object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "@mintlify/validation/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="], + + "@mintlify/validation/zod": ["zod@3.24.0", "", {}, "sha512-Hz+wiY8yD0VLA2k/+nsg2Abez674dDGTai33SwNvMPuf9uIrBC9eFgIMQxBBbHFxVXi8W+5nX9DcAh9YNSQm/w=="], + + "@mintlify/validation/zod-to-json-schema": ["zod-to-json-schema@3.20.4", "", { "peerDependencies": { "zod": "^3.20.0" } }, "sha512-Un9+kInJ2Zt63n6Z7mLqBifzzPcOyX+b+Exuzf7L1+xqck9Q2EPByyTRduV3kmSPaXaRer1JCsucubpgL1fipg=="], "@modelcontextprotocol/sdk/hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="], "@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": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "@npmcli/config/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], @@ -6034,7 +6851,13 @@ "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], - "@opencode-ai/app/@opencode-ai/client": ["@opencode-ai/client@vendor/opencode-ai-client-1.17.13-v2.tgz", {}, "sha512-332kgNifvpQOF9e3UA+pIa5xPrMhLaQkUiNiO+meS0Ba9HjSE6hfsWnEojMkD0DPSLqPP6rCF1dDoF7U0Y0OCQ=="], + "@opencode-ai/cli/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@opencode-ai/console-app/@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=="], + + "@opencode-ai/console-app/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], + + "@opencode-ai/console-mail/@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="], "@opencode-ai/core/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="], @@ -6044,22 +6867,32 @@ "@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=="], + "@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], - "@opencode-ai/llm/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="], - - "@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.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "@opencode-ai/session-ui/@opencode-ai/client": ["@opencode-ai/client@../app/vendor/opencode-ai-client-1.17.13-v2.tgz", {}, "sha512-332kgNifvpQOF9e3UA+pIa5xPrMhLaQkUiNiO+meS0Ba9HjSE6hfsWnEojMkD0DPSLqPP6rCF1dDoF7U0Y0OCQ=="], - "@opencode-ai/session-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/storybook/@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="], + "@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=="], + "@opencode-ai/www/@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.44.0", "", { "dependencies": { "@cloudflare/unenv-preset": "2.16.1", "miniflare": "4.20260708.1", "unenv": "2.0.0-rc.24", "wrangler": "4.110.0", "ws": "8.21.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0 || ^8.0.0" }, "bin": { "cf-vite": "bin/cf-vite" } }, "sha512-8wGGunqRcs34o4GRq0Rurp7GZg30xtLJeRGUU81a49r9zQRjlp3xIlsWr3nFlSCso4eE3cjZfiKC/2y116M4TQ=="], + + "@opencode-ai/www/@tailwindcss/vite": ["@tailwindcss/vite@4.3.2", "", { "dependencies": { "@tailwindcss/node": "4.3.2", "@tailwindcss/oxide": "4.3.2", "tailwindcss": "4.3.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA=="], + + "@opencode-ai/www/react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + + "@opencode-ai/www/tailwindcss": ["tailwindcss@4.3.2", "", {}, "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA=="], + + "@opencode-ai/www/vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.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", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="], + + "@opencode-ai/www/wrangler": ["wrangler@4.110.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260708.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260708.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260708.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "cf-wrangler": "bin/cf-wrangler.js", "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg=="], + "@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], "@opentui/core/marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], @@ -6068,10 +6901,16 @@ "@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="], + "@oxc-parser/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], + + "@oxc-parser/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + "@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=="], + "@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], + "@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=="], @@ -6080,6 +6919,154 @@ "@protobuf-ts/plugin/typescript": ["typescript@3.9.10", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q=="], + "@puppeteer/browsers/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "@puppeteer/browsers/tar-fs": ["tar-fs@3.1.3", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ=="], + + "@puppeteer/browsers/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=="], + + "@radix-ui/react-accordion/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="], + + "@radix-ui/react-accordion/@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-opfXRe6nnzyGmCDPx+l1Aqo/RbqWtQal2FnsBqF9hhePp6j0LsRoBaRxcMOlTv+uYTJVtWYZKg9t9wTe+BA/ZA=="], + + "@radix-ui/react-accordion/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + + "@radix-ui/react-accordion/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + + "@radix-ui/react-accordion/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + + "@radix-ui/react-accordion/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + + "@radix-ui/react-accordion/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + + "@radix-ui/react-arrow/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + + "@radix-ui/react-collapsible/@radix-ui/react-presence": ["@radix-ui/react-presence@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg=="], + + "@radix-ui/react-collapsible/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + + "@radix-ui/react-collection/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + + "@radix-ui/react-collection/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + + "@radix-ui/react-collection/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="], + + "@radix-ui/react-dialog/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + + "@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + + "@radix-ui/react-dialog/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-effect-event": "0.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA=="], + + "@radix-ui/react-dialog/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q=="], + + "@radix-ui/react-dialog/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.12", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A=="], + + "@radix-ui/react-dialog/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + + "@radix-ui/react-dialog/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.13", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA=="], + + "@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + + "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-dialog/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], + + "@radix-ui/react-dismissable-layer/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + + "@radix-ui/react-focus-scope/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], + + "@radix-ui/react-focus-scope/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + + "@radix-ui/react-navigation-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="], + + "@radix-ui/react-navigation-menu/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + + "@radix-ui/react-navigation-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + + "@radix-ui/react-navigation-menu/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-effect-event": "0.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA=="], + + "@radix-ui/react-navigation-menu/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + + "@radix-ui/react-navigation-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + + "@radix-ui/react-navigation-menu/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + + "@radix-ui/react-navigation-menu/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "@radix-ui/react-navigation-menu/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1wNZBggTDK3GRuuQ6nP4k2yi7a6l7I5qbMPbZcRsrGsGVead/f/d5FhEzUvqFs0bcrDLx7n1zKQ3JvLR6whaaw=="], + + "@radix-ui/react-popover/@radix-ui/react-presence": ["@radix-ui/react-presence@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg=="], + + "@radix-ui/react-popover/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + + "@radix-ui/react-popover/react-remove-scroll": ["react-remove-scroll@2.5.5", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.3", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", "use-sidecar": "^1.1.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw=="], + + "@radix-ui/react-popper/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], + + "@radix-ui/react-popper/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + + "@radix-ui/react-portal/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + + "@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "@radix-ui/react-primitive/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-collection": ["@radix-ui/react-collection@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-slot": "1.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-direction": ["@radix-ui/react-direction@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], + + "@radix-ui/react-roving-focus/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + + "@radix-ui/react-scroll-area/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="], + + "@radix-ui/react-scroll-area/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + + "@radix-ui/react-scroll-area/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + + "@radix-ui/react-scroll-area/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + + "@radix-ui/react-scroll-area/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "@radix-ui/react-tabs/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="], + + "@radix-ui/react-tabs/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + + "@radix-ui/react-tabs/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + + "@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + + "@radix-ui/react-tabs/@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-is-hydrated": "0.1.1", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-40svmmugfM3mUN7VUDGVE1tQGOhyi8enlGD0CNJEcMM36C1f71PKM21DFgNHUfem0XnA+d8H8oN3Z9ZpJjSslg=="], + + "@radix-ui/react-tabs/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + + "@radix-ui/react-toggle/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + + "@radix-ui/react-toggle-group/@radix-ui/react-direction": ["@radix-ui/react-direction@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA=="], + + "@radix-ui/react-toggle-group/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + + "@radix-ui/react-tooltip/@radix-ui/react-presence": ["@radix-ui/react-presence@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg=="], + + "@radix-ui/react-tooltip/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + + "@radix-ui/react-use-controllable-state/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], + + "@radix-ui/react-use-effect-event/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "@radix-ui/react-use-escape-keydown/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], + + "@radix-ui/react-visually-hidden/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + + "@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@sentry/bundler-plugin-core/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=="], @@ -6104,8 +7091,16 @@ "@shikijs/themes/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], + "@shikijs/twoslash/@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=="], + + "@shikijs/twoslash/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + + "@slack/bolt/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=="], + "@slack/bolt/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + "@slack/bolt/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + "@slack/oauth/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], "@slack/socket-mode/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], @@ -6122,6 +7117,8 @@ "@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=="], + "@solid-primitives/memo/@solid-primitives/utils": ["@solid-primitives/utils@6.4.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ISSB5QX1qP2ynrheIpYwc4oKR5Ny4siNuUyf1qZniy+Il+p/PtDB0QK1Dnle8noiHpwRD3gpPdubOC3qI/Zamg=="], + "@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=="], @@ -6132,9 +7129,31 @@ "@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="], - "@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=="], + "@stoplight/better-ajv-errors/leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], - "@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "@stoplight/json/jsonc-parser": ["jsonc-parser@2.2.1", "", {}, "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w=="], + + "@stoplight/json/safe-stable-stringify": ["safe-stable-stringify@1.1.1", "", {}, "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw=="], + + "@stoplight/json-ref-readers/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "@stoplight/json-ref-resolver/immer": ["immer@9.0.21", "", {}, "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA=="], + + "@stoplight/spectral-core/@stoplight/types": ["@stoplight/types@13.6.0", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ=="], + + "@stoplight/spectral-core/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + + "@stoplight/spectral-core/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "@stoplight/spectral-functions/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + + "@stoplight/spectral-parsers/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="], + + "@stoplight/yaml/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="], + + "@storybook/addon-docs/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + + "@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-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=="], @@ -6150,16 +7169,62 @@ "@tanstack/directive-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + "@tanstack/directive-functions-plugin/@tanstack/router-utils": ["@tanstack/router-utils@1.133.19", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.5", "@babel/preset-typescript": "^7.27.1", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-WEp5D2gPxvlLDRXwD/fV7RXjYtqaqJNXKB/L6OyZEbT+9BG/Ib2d7oG9GSUZNNMGPGYAlhBUOi3xutySsk6rxA=="], + + "@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], + + "@tanstack/router-core/seroval": ["seroval@1.5.5", "", {}, "sha512-bSjOuPcwPKLSJNhr9+bZxA20nQxVle5J5MNsYRVE6cIg7KpRLXGupymePavu0jrxlPiPsr4xGZSB8yUY2sH2sw=="], + + "@tanstack/router-core/seroval-plugins": ["seroval-plugins@1.5.5", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-+BDhqYM6CEn3x09v44dpa9p6974FuUB2dxk+Ctn04k0cO1Zt6QODTXfmEZK0eBaTe/fJBvP4NMGuNJ+R8T+QMg=="], + + "@tanstack/router-generator/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "@tanstack/router-plugin/@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@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-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@tanstack/router-plugin/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "@tanstack/router-plugin/unplugin": ["unplugin@3.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.4", "webpack-virtual-modules": "^0.6.2" }, "peerDependencies": { "@farmfe/core": "*", "@rspack/core": "*", "bun-types-no-globals": "*", "esbuild": "*", "rolldown": "*", "rollup": "*", "unloader": "*", "vite": "*", "webpack": "*" }, "optionalPeers": ["@farmfe/core", "@rspack/core", "bun-types-no-globals", "esbuild", "rolldown", "rollup", "unloader", "vite", "webpack"] }, "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg=="], + + "@tanstack/router-plugin/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@tanstack/router-utils/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], "@tanstack/server-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + "@tanstack/start-client-core/seroval": ["seroval@1.5.5", "", {}, "sha512-bSjOuPcwPKLSJNhr9+bZxA20nQxVle5J5MNsYRVE6cIg7KpRLXGupymePavu0jrxlPiPsr4xGZSB8yUY2sH2sw=="], + + "@tanstack/start-plugin-core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@tanstack/start-plugin-core/@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@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-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@tanstack/start-plugin-core/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "@tanstack/start-plugin-core/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "@tanstack/start-plugin-core/seroval": ["seroval@1.5.5", "", {}, "sha512-bSjOuPcwPKLSJNhr9+bZxA20nQxVle5J5MNsYRVE6cIg7KpRLXGupymePavu0jrxlPiPsr4xGZSB8yUY2sH2sw=="], + + "@tanstack/start-plugin-core/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "@tanstack/start-plugin-core/srvx": ["srvx@0.11.21", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-GWTHjKMeekX8CwJf4VU9Oo6mJpSGaflGMddbCvR+Cmmh9sslRMiGbAoqqZacE0r1ncARh6buCEETr2W52F8b1w=="], + + "@tanstack/start-plugin-core/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "@tanstack/start-server-core/seroval": ["seroval@1.5.5", "", {}, "sha512-bSjOuPcwPKLSJNhr9+bZxA20nQxVle5J5MNsYRVE6cIg7KpRLXGupymePavu0jrxlPiPsr4xGZSB8yUY2sH2sw=="], + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], "@types/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + "@vercel/cli-config/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], + + "@vercel/cli-exec/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "@vercel/functions/@vercel/oidc": ["@vercel/oidc@3.8.0", "", { "dependencies": { "@vercel/cli-config": "0.2.0", "@vercel/cli-exec": "1.0.0", "jose": "^5.9.6" } }, "sha512-r00laGW6Pv778RoR6M2NxX91ycSj+PBwVo+fOb9Bif+F0IyUKt25zrvBzfEzQpeAzbqOgPZyQibEWDdDFApd+A=="], + + "@vitejs/plugin-react/vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.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", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="], + "@vitest/coverage-v8/@vitest/utils": ["@vitest/utils@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg=="], "@vitest/coverage-v8/magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="], @@ -6172,9 +7237,7 @@ "@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="], - "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "aggregate-error/indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], "ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.107", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.78", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@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-8nT08pGPy25rleJNk56ep00UHK6kCtCmu+ZNqVVSSPDieADlIZqcaN1iRXAFBoCH0Fb9F6C2EjFDaySdsargfQ=="], @@ -6184,8 +7247,6 @@ "ai-gateway-provider/@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-gateway-provider/@ai-sdk/mistral": ["@ai-sdk/mistral@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-ZXe7nZQgliDdjz5ufH5RKpHWxbN72AzmzzKGbF/z+0K9GN5tUCnftrQRvTRFHA5jAzTapcm2BEevmGLVbMkW+A=="], - "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/xai": ["@ai-sdk/xai@3.0.82", "", { "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-A0VFMufnVf4wODcT3SPQUUzvYXiIO1VhFuXj9r6z/vP4rlo+QRDPw3WSTchcz93ROQWSfBE3I6Szqz342OHi5w=="], @@ -6240,9 +7301,11 @@ "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=="], - "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], - "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "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=="], "builder-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -6252,6 +7315,8 @@ "c12/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], + "chromium-bidi/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="], + "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], "compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], @@ -6262,10 +7327,16 @@ "config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "cosmiconfig/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "cosmiconfig/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "crc/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "degenerator/ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], + "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=="], @@ -6284,8 +7355,6 @@ "editorconfig/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - "effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "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=="], @@ -6308,8 +7377,14 @@ "encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "engine.io/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "engine.io/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "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=="], + "error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + "esast-util-from-js/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "esbuild-plugin-copy/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -6324,22 +7399,50 @@ "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], + "favicons/xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], "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=="], - "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "framer-motion/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + "fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + "fumadocs-core/js-yaml": ["js-yaml@5.2.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.mjs" } }, "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw=="], + + "fumadocs-core/shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="], + + "fumadocs-mdx/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "fumadocs-mdx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + + "fumadocs-mdx/js-yaml": ["js-yaml@5.2.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.mjs" } }, "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw=="], + + "fumadocs-mdx/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "fumadocs-mdx/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], + + "fumadocs-mdx/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "fumadocs-ui/@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-opfXRe6nnzyGmCDPx+l1Aqo/RbqWtQal2FnsBqF9hhePp6j0LsRoBaRxcMOlTv+uYTJVtWYZKg9t9wTe+BA/ZA=="], + + "fumadocs-ui/@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.12", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.3", "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jkrTdQVxnIB8fpn0NyyxW9CTB5aCXZZelVz5z+Xmii6g5WxMqS3fInNslZ63puP39+Puu4jYohUK31y3dT87gQ=="], + + "fumadocs-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "fumadocs-ui/motion": ["motion@12.42.2", "", { "dependencies": { "framer-motion": "^12.42.2", "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-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q=="], + + "fumadocs-ui/react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + + "fumadocs-ui/shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="], + "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=="], + "get-uri/data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], + "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=="], @@ -6348,6 +7451,10 @@ "got/@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], + "h3-v2/rou3": ["rou3@0.8.1", "", {}, "sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA=="], + + "h3-v2/srvx": ["srvx@0.11.21", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-GWTHjKMeekX8CwJf4VU9Oo6mJpSGaflGMddbCvR+Cmmh9sslRMiGbAoqqZacE0r1ncARh6buCEETr2W52F8b1w=="], + "html-minifier-terser/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], "html-minifier-terser/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], @@ -6358,6 +7465,26 @@ "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "ink/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "ink/indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + + "ink/react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], + + "ink/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "ink/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + + "ink-spinner/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "inquirer/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "is-online/got": ["got@12.6.1", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ=="], + + "is-online/p-timeout": ["p-timeout@5.1.0", "", {}, "sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew=="], + "istanbul-reports/html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], "js-beautify/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=="], @@ -6366,18 +7493,20 @@ "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + "keytar/node-addon-api": ["node-addon-api@4.3.0", "", {}, "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="], + "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], - "lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "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=="], "micromark-extension-mdxjs/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + "micromark-extension-mdxjs/micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], @@ -6394,6 +7523,14 @@ "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=="], + "next-mdx-remote-client/@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + + "next-mdx-remote-client/react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], + + "next-mdx-remote-client/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + + "next-mdx-remote-client/serialize-error": ["serialize-error@13.0.1", "", { "dependencies": { "non-error": "^0.1.0", "type-fest": "^5.4.1" } }, "sha512-bBZaRwLH9PN5HbLCjPId4dP5bNGEtumcErgOX952IsvOhVPrm3/AeK1y0UHA/QaPG701eg0yEnOKsCOC6X/kaA=="], + "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.26.0", "", {}, "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg=="], @@ -6402,8 +7539,6 @@ "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=="], - "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], @@ -6418,6 +7553,8 @@ "opencode/@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA=="], + "opencode/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "opencode/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], "opencode/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], @@ -6428,12 +7565,20 @@ "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + "oxc-parser/@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], + + "p-any/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], + "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=="], + "p-some/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "parse-json/json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], @@ -6454,6 +7599,8 @@ "postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], + "prebuild-install/node-abi": ["node-abi@3.94.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g=="], + "pretty-format/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -6462,24 +7609,34 @@ "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "proxy-agent/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "public-ip/got": ["got@12.6.1", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ=="], + + "rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "react-dom/react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + + "react-reconciler/react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], + + "react-reconciler/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], "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=="], - - "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], - "sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "shiki/@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="], "shiki/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], @@ -6488,6 +7645,10 @@ "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], + "socket.io/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "socket.io/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + "solid-transition-size/@corvu/utils": ["@corvu/utils@0.3.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.7" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-ZWlyWEE8qV9+CB9OAyo2bTrZGXQN9ZeM+JfYv89zoR+lRACKTDuoOZEdiyL8Uc7U5dUSH1uTqKhTTnaHWb+wZA=="], "sort-keys/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], @@ -6496,6 +7657,8 @@ "sst/jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], + "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + "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=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -6508,6 +7671,10 @@ "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "terser/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], @@ -6524,7 +7691,9 @@ "tw-to-css/tailwindcss": ["tailwindcss@3.3.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.18.2", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w=="], - "type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + + "unbzip2-stream/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "unifont/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=="], @@ -6570,6 +7739,10 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "xmlbuilder2/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "xss/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="], "yaml-language-server/yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], @@ -6588,46 +7761,6 @@ "@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], - "@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/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=="], - - "@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@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/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@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=="], - - "@ai-sdk/openai-compatible/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/perplexity/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/togetherai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/vercel/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/xai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@astrojs/check/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=="], "@astrojs/check/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=="], @@ -6656,6 +7789,8 @@ "@astrojs/starlight/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@asyncapi/parser/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@aws-crypto/sha1-browser/@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-crypto/sha256-browser/@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=="], @@ -6738,6 +7873,12 @@ "@hey-api/json-schema-ref-parser/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@inquirer/core/wrap-ansi/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=="], + + "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@jsx-email/cli/@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], + "@jsx-email/cli/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], "@jsx-email/cli/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], @@ -6784,6 +7925,8 @@ "@jsx-email/cli/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], + "@jsx-email/cli/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + "@jsx-email/cli/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "@jsx-email/cli/tailwindcss/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -6798,29 +7941,167 @@ "@jsx-email/doiuse-email/htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "@jsx-email/tailwind/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + "@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=="], + "@mintlify/cli/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/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + "@mintlify/cli/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], + "@mintlify/cli/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], - "@modelcontextprotocol/sdk/express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "@mintlify/cli/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], - "@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "@mintlify/cli/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - "@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "@mintlify/cli/openid-client/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], - "@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "@mintlify/cli/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=="], - "@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "@mintlify/cli/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=="], - "@modelcontextprotocol/sdk/express/send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + "@mintlify/common/hast-util-to-html/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], - "@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=="], + "@mintlify/common/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "@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=="], + "@mintlify/common/mdast-util-gfm/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "@mintlify/common/mdast-util-mdx-jsx/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "@mintlify/common/remark-gfm/mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "@mintlify/common/sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "@mintlify/common/sucrase/glob": ["glob@7.1.6", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA=="], + + "@mintlify/common/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "@mintlify/common/tailwindcss/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "@mintlify/common/tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "@mintlify/common/tailwindcss/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "@mintlify/common/tailwindcss/object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "@mintlify/common/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=="], + + "@mintlify/common/tailwindcss/sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + + "@mintlify/common/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + + "@mintlify/link-rot/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "@mintlify/link-rot/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "@mintlify/link-rot/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], + + "@mintlify/link-rot/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], + + "@mintlify/mdx/@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=="], + + "@mintlify/mdx/@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=="], + + "@mintlify/mdx/react-dom/react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="], + + "@mintlify/mdx/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "@mintlify/mdx/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=="], + + "@mintlify/mdx/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], + + "@mintlify/mdx/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], + + "@mintlify/mdx/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], + + "@mintlify/mdx/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + + "@mintlify/mdx/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=="], + + "@mintlify/prebuild/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "@mintlify/prebuild/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "@mintlify/prebuild/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "@mintlify/prebuild/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], + + "@mintlify/prebuild/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], + + "@mintlify/previewing/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "@mintlify/previewing/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "@mintlify/previewing/express/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=="], + + "@mintlify/previewing/express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + + "@mintlify/previewing/express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "@mintlify/previewing/express/cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + + "@mintlify/previewing/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "@mintlify/previewing/express/finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + + "@mintlify/previewing/express/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "@mintlify/previewing/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "@mintlify/previewing/express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], + + "@mintlify/previewing/express/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], + + "@mintlify/previewing/express/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + + "@mintlify/previewing/express/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + + "@mintlify/previewing/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + + "@mintlify/previewing/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "@mintlify/previewing/got/@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="], + + "@mintlify/previewing/got/@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="], + + "@mintlify/previewing/got/cacheable-lookup": ["cacheable-lookup@7.0.0", "", {}, "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="], + + "@mintlify/previewing/got/cacheable-request": ["cacheable-request@10.2.14", "", { "dependencies": { "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", "http-cache-semantics": "^4.1.1", "keyv": "^4.5.3", "mimic-response": "^4.0.0", "normalize-url": "^8.0.0", "responselike": "^3.0.0" } }, "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ=="], + + "@mintlify/previewing/got/form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="], + + "@mintlify/previewing/got/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "@mintlify/previewing/got/http2-wrapper": ["http2-wrapper@2.2.1", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ=="], + + "@mintlify/previewing/got/lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="], + + "@mintlify/previewing/got/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], + + "@mintlify/previewing/got/responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="], + + "@mintlify/previewing/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "@mintlify/previewing/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "@mintlify/previewing/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], + + "@mintlify/previewing/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], + + "@mintlify/previewing/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=="], + + "@mintlify/previewing/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=="], + + "@mintlify/scraping/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "@mintlify/scraping/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "@mintlify/scraping/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=="], + + "@mintlify/scraping/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=="], + + "@mintlify/validation/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "@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=="], @@ -6930,12 +8211,100 @@ "@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=="], + "@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare": ["miniflare@4.20260708.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260708.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-c94O9zRDISdqO18EHt6l0iF/fWgWt8p18PJvRsA/L/NJZ9Cfke3s/F5Blg1XXF7WDutVRzWVWy8Vy4LaT5ifsA=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/node": ["@tailwindcss/node@4.3.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "5.21.6", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.2" } }, "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.2", "@tailwindcss/oxide-darwin-arm64": "4.3.2", "@tailwindcss/oxide-darwin-x64": "4.3.2", "@tailwindcss/oxide-freebsd-x64": "4.3.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", "@tailwindcss/oxide-linux-x64-musl": "4.3.2", "@tailwindcss/oxide-wasm32-wasi": "4.3.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" } }, "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag=="], + + "@opencode-ai/www/vite/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "@opencode-ai/www/vite/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "@opencode-ai/www/vite/postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="], + + "@opencode-ai/www/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], + + "@opencode-ai/www/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], + + "@opencode-ai/www/wrangler/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + + "@opencode-ai/www/wrangler/miniflare": ["miniflare@4.20260708.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260708.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-c94O9zRDISdqO18EHt6l0iF/fWgWt8p18PJvRsA/L/NJZ9Cfke3s/F5Blg1XXF7WDutVRzWVWy8Vy4LaT5ifsA=="], + + "@opencode-ai/www/wrangler/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="], + "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@oxc-parser/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@pierre/diffs/@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=="], "@pierre/diffs/@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=="], + "@puppeteer/browsers/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=="], + + "@puppeteer/browsers/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=="], + + "@radix-ui/react-accordion/@radix-ui/react-collapsible/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "@radix-ui/react-accordion/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "@radix-ui/react-accordion/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-accordion/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "@radix-ui/react-arrow/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "@radix-ui/react-collapsible/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "@radix-ui/react-dialog/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "@radix-ui/react-dialog/@radix-ui/react-portal/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "@radix-ui/react-dialog/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "@radix-ui/react-dismissable-layer/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "@radix-ui/react-focus-scope/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "@radix-ui/react-navigation-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-popover/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "@radix-ui/react-popper/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "@radix-ui/react-portal/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "@radix-ui/react-primitive/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "@radix-ui/react-roving-focus/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "@radix-ui/react-scroll-area/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-tabs/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-tabs/@radix-ui/react-roving-focus/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + + "@radix-ui/react-tabs/@radix-ui/react-roving-focus/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "@radix-ui/react-tabs/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "@radix-ui/react-toggle-group/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "@radix-ui/react-toggle/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "@radix-ui/react-tooltip/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "@radix-ui/react-visually-hidden/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], + "@sentry/bundler-plugin-core/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], "@sentry/bundler-plugin-core/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], @@ -6948,6 +8317,34 @@ "@shikijs/stream/@shikijs/core/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], + "@slack/bolt/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "@slack/bolt/express/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=="], + + "@slack/bolt/express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + + "@slack/bolt/express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "@slack/bolt/express/cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + + "@slack/bolt/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "@slack/bolt/express/finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + + "@slack/bolt/express/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "@slack/bolt/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "@slack/bolt/express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], + + "@slack/bolt/express/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + + "@slack/bolt/express/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + + "@slack/bolt/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + + "@slack/bolt/raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "@slack/web-api/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "@slack/web-api/p-queue/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], @@ -6966,9 +8363,29 @@ "@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.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="], - "@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@standard-community/standard-json/effect/msgpackr": ["msgpackr@2.0.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ=="], + + "@standard-community/standard-json/effect/multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="], + + "@standard-community/standard-json/effect/toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="], + + "@standard-community/standard-json/effect/uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="], + + "@standard-community/standard-openapi/effect/fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="], + + "@standard-community/standard-openapi/effect/msgpackr": ["msgpackr@2.0.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ=="], + + "@standard-community/standard-openapi/effect/multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="], + + "@standard-community/standard-openapi/effect/toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="], + + "@standard-community/standard-openapi/effect/uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="], + + "@stoplight/spectral-core/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], + + "@storybook/addon-docs/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], "@storybook/csf-plugin/unplugin/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], @@ -6976,20 +8393,70 @@ "@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=="], + "@tanstack/directive-functions-plugin/@tanstack/router-utils/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + + "@tanstack/router-plugin/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@tanstack/router-plugin/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "@tanstack/router-plugin/unplugin/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "@tanstack/router-plugin/unplugin/webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + + "@tanstack/start-plugin-core/@babel/core/@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=="], + + "@tanstack/start-plugin-core/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@tanstack/start-plugin-core/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "@tanstack/start-plugin-core/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "@tanstack/start-plugin-core/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "@tanstack/start-plugin-core/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "@tanstack/start-plugin-core/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "@tanstack/start-plugin-core/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "@tanstack/start-plugin-core/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "@tanstack/start-plugin-core/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "@tanstack/start-plugin-core/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "@tanstack/start-plugin-core/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "@vercel/cli-exec/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "@vercel/cli-exec/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "@vercel/cli-exec/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "@vercel/cli-exec/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "@vercel/cli-exec/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "@vercel/cli-exec/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "@vercel/cli-exec/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "@vercel/functions/@vercel/oidc/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], + + "@vitejs/plugin-react/vite/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "@vitejs/plugin-react/vite/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "@vitejs/plugin-react/vite/postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="], + "@vitest/coverage-v8/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.8", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA=="], "@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/amazon-bedrock/@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/amazon-bedrock/@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-gateway-provider/@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="], - - "ai-gateway-provider/@ai-sdk/amazon-bedrock/@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=="], - "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/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=="], @@ -7002,8 +8469,6 @@ "ai-gateway-provider/@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-gateway-provider/@ai-sdk/mistral/@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/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=="], "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], @@ -7058,7 +8523,11 @@ "babel-plugin-module-resolver/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=="], - "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "better-opn/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], + + "better-opn/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + + "better-opn/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], "builder-util/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], @@ -7066,6 +8535,8 @@ "conf/dot-prop/type-fest": ["type-fest@5.7.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg=="], + "cosmiconfig/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "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=="], @@ -7116,20 +8587,168 @@ "electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], - "esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "engine.io/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "engine.io/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "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=="], - "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "framer-motion/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "fumadocs-core/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "fumadocs-core/shiki/@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="], + + "fumadocs-core/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ=="], + + "fumadocs-core/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg=="], + + "fumadocs-core/shiki/@shikijs/langs": ["@shikijs/langs@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ=="], + + "fumadocs-core/shiki/@shikijs/themes": ["@shikijs/themes@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="], + + "fumadocs-core/shiki/@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="], + + "fumadocs-mdx/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "fumadocs-mdx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "fumadocs-mdx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "fumadocs-mdx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "fumadocs-mdx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "fumadocs-mdx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "fumadocs-mdx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "fumadocs-mdx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "fumadocs-mdx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "fumadocs-mdx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "fumadocs-mdx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "fumadocs-mdx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "fumadocs-mdx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "fumadocs-mdx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "fumadocs-mdx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "fumadocs-mdx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "fumadocs-mdx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "fumadocs-mdx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "fumadocs-mdx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "fumadocs-mdx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "fumadocs-mdx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "fumadocs-mdx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "fumadocs-mdx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "fumadocs-mdx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "fumadocs-mdx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "fumadocs-mdx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "fumadocs-mdx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + + "fumadocs-mdx/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "fumadocs-ui/@radix-ui/react-collapsible/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="], + + "fumadocs-ui/@radix-ui/react-collapsible/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + + "fumadocs-ui/@radix-ui/react-collapsible/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + + "fumadocs-ui/@radix-ui/react-collapsible/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + + "fumadocs-ui/@radix-ui/react-collapsible/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + + "fumadocs-ui/@radix-ui/react-collapsible/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + + "fumadocs-ui/@radix-ui/react-collapsible/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-effect-event": "0.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.12", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-popper": ["@radix-ui/react-popper@1.3.3", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.11", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-rect": "1.1.2", "@radix-ui/react-use-size": "1.1.2", "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mS7dGpyjv6b+gsDjLF7e0ia1W4Im1B1hSCy2yuXlHuvnZxHKagfDaobt/KAKt27EpZMit2pss8eJBVyVjEWM+g=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.13", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + + "fumadocs-ui/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + + "fumadocs-ui/motion/framer-motion": ["framer-motion@12.42.2", "", { "dependencies": { "motion-dom": "^12.42.2", "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-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="], + + "fumadocs-ui/shiki/@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="], + + "fumadocs-ui/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ=="], + + "fumadocs-ui/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg=="], + + "fumadocs-ui/shiki/@shikijs/langs": ["@shikijs/langs@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ=="], + + "fumadocs-ui/shiki/@shikijs/themes": ["@shikijs/themes@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="], + + "fumadocs-ui/shiki/@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="], + "iconv-corefoundation/cli-truncate/slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="], "iconv-corefoundation/cli-truncate/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=="], + "ink/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "inquirer/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + + "is-online/got/@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="], + + "is-online/got/@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="], + + "is-online/got/cacheable-lookup": ["cacheable-lookup@7.0.0", "", {}, "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="], + + "is-online/got/cacheable-request": ["cacheable-request@10.2.14", "", { "dependencies": { "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", "http-cache-semantics": "^4.1.1", "keyv": "^4.5.3", "mimic-response": "^4.0.0", "normalize-url": "^8.0.0", "responselike": "^3.0.0" } }, "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ=="], + + "is-online/got/form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="], + + "is-online/got/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "is-online/got/http2-wrapper": ["http2-wrapper@2.2.1", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ=="], + + "is-online/got/lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="], + + "is-online/got/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], + + "is-online/got/responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="], + "js-beautify/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "js-beautify/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -7148,6 +8767,12 @@ "motion/framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], + "next-mdx-remote-client/react-dom/react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="], + + "next-mdx-remote-client/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "next-mdx-remote-client/serialize-error/type-fest": ["type-fest@5.7.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg=="], + "opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], "opencode/@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.54", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OyXt0zK8y2/ZIyWlbxTv2r1M7AK227S+Gl4BYOEF42q0wz1n5m4fwR8L4Fy/MQ4Ho6xje47MPsFcRdIqIyP6Rw=="], @@ -7166,16 +8791,44 @@ "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=="], + "prebuild-install/node-abi/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "public-ip/got/@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="], + + "public-ip/got/@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="], + + "public-ip/got/cacheable-lookup": ["cacheable-lookup@7.0.0", "", {}, "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="], + + "public-ip/got/cacheable-request": ["cacheable-request@10.2.14", "", { "dependencies": { "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", "http-cache-semantics": "^4.1.1", "keyv": "^4.5.3", "mimic-response": "^4.0.0", "normalize-url": "^8.0.0", "responselike": "^3.0.0" } }, "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ=="], + + "public-ip/got/form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="], + + "public-ip/got/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "public-ip/got/http2-wrapper": ["http2-wrapper@2.2.1", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ=="], + + "public-ip/got/lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="], + + "public-ip/got/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], + + "public-ip/got/responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="], + "readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], + "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "socket.io/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "socket.io/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "storybook/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "tar-fs/tar-stream/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=="], + "tw-to-css/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "tw-to-css/tailwindcss/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -7186,8 +8839,6 @@ "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=="], - "unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "unzipper/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], @@ -7196,10 +8847,6 @@ "venice-ai-sdk-provider/@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="], - "venice-ai-sdk-provider/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "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=="], "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], @@ -7256,6 +8903,8 @@ "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "xmlbuilder2/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@actions/artifact/@actions/core/@actions/exec/@actions/io": ["@actions/io@2.0.0", "", {}, "sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg=="], "@actions/github/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], @@ -7310,12 +8959,8 @@ "@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=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@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-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.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.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=="], @@ -7332,6 +8977,10 @@ "@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@jsx-email/cli/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "@jsx-email/cli/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -7380,9 +9029,63 @@ "@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=="], + "@mintlify/cli/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + "@mintlify/cli/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=="], + + "@mintlify/cli/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@mintlify/cli/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@mintlify/common/remark-gfm/mdast-util-gfm/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "@mintlify/common/sucrase/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "@mintlify/common/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "@mintlify/common/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "@mintlify/common/tailwindcss/sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "@mintlify/previewing/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "@mintlify/previewing/express/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "@mintlify/previewing/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "@mintlify/previewing/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "@mintlify/previewing/express/body-parser/qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], + + "@mintlify/previewing/express/body-parser/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + + "@mintlify/previewing/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "@mintlify/previewing/express/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "@mintlify/previewing/express/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "@mintlify/previewing/express/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "@mintlify/previewing/got/cacheable-request/mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="], + + "@mintlify/previewing/got/cacheable-request/normalize-url": ["normalize-url@8.1.1", "", {}, "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ=="], + + "@mintlify/previewing/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@mintlify/previewing/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=="], + + "@mintlify/previewing/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@mintlify/previewing/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@mintlify/scraping/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@mintlify/scraping/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=="], + + "@mintlify/scraping/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@mintlify/scraping/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "@octokit/auth-app/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], @@ -7408,29 +9111,187 @@ "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], - "@opencode-ai/core/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@opencode-ai/desktop/@actions/artifact/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/node/enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.2", "", { "os": "android", "cpu": "arm64" }, "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.2", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ=="], + + "@opencode-ai/www/vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "@opencode-ai/www/vite/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "@opencode-ai/www/vite/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "@opencode-ai/www/vite/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "@opencode-ai/www/vite/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "@opencode-ai/www/vite/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "@opencode-ai/www/vite/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "@opencode-ai/www/vite/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "@opencode-ai/www/vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "@opencode-ai/www/vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "@opencode-ai/www/wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + + "@opencode-ai/www/wrangler/miniflare/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + + "@opencode-ai/www/wrangler/miniflare/undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], + + "@opencode-ai/www/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260708.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg=="], + + "@opencode-ai/www/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260708.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA=="], + + "@opencode-ai/www/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260708.1", "", { "os": "linux", "cpu": "x64" }, "sha512-3daE60YdD7YX0Jtuzc9DE/r/qMkmx8ZvHTkF8Mzmp3F5tbzlV0DAzmu5PFUPF2WuvtKbAhZKbvC2cHmWpQYxnA=="], + + "@opencode-ai/www/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260708.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-VLdNYOx5Hj+9C6isy0ACWZsbMtSxex2DIJWEe7cZxUdlphZ58ZT8zxNXK8yunFiowd34hn3VwGMopdvdj8lvmA=="], + + "@opencode-ai/www/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260708.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA=="], + + "@puppeteer/browsers/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@puppeteer/browsers/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=="], + + "@puppeteer/browsers/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@puppeteer/browsers/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + "@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=="], "@sentry/bundler-plugin-core/glob/path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "@slack/bolt/express/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "@slack/bolt/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "@slack/bolt/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "@slack/bolt/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "@slack/bolt/express/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "@slack/bolt/express/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "@slack/bolt/express/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "@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=="], - "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@stoplight/spectral-core/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "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=="], + "@vercel/cli-exec/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "ai-gateway-provider/@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@vitejs/plugin-react/vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], - "ai-gateway-provider/@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@vitejs/plugin-react/vite/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], - "ai-gateway-provider/@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@vitejs/plugin-react/vite/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "@vitejs/plugin-react/vite/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "@vitejs/plugin-react/vite/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "@vitejs/plugin-react/vite/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "@vitejs/plugin-react/vite/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "@vitejs/plugin-react/vite/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "@vitejs/plugin-react/vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "@vitejs/plugin-react/vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -7486,14 +9347,44 @@ "electron-builder/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "engine.io/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "esbuild-plugin-copy/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "fumadocs-core/shiki/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.11", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-popper/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-popper/@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.2", "", { "dependencies": { "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-popper/@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-popper/@radix-ui/rect": ["@radix-ui/rect@1.1.2", "", {}, "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-portal/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "fumadocs-ui/motion/framer-motion/motion-dom": ["motion-dom@12.42.2", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA=="], + + "fumadocs-ui/motion/framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], + + "fumadocs-ui/shiki/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="], + "iconv-corefoundation/cli-truncate/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "iconv-corefoundation/cli-truncate/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "is-online/got/cacheable-request/mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="], + + "is-online/got/cacheable-request/normalize-url": ["normalize-url@8.1.1", "", {}, "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ=="], + "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.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], @@ -7510,10 +9401,16 @@ "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + "public-ip/got/cacheable-request/mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="], + + "public-ip/got/cacheable-request/normalize-url": ["normalize-url@8.1.1", "", {}, "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ=="], + "readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "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=="], + "socket.io/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "tw-to-css/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "tw-to-css/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -7538,8 +9435,6 @@ "@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.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], @@ -7554,8 +9449,168 @@ "@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "@mintlify/cli/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@mintlify/cli/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@mintlify/common/sucrase/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=="], + + "@mintlify/common/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "@mintlify/previewing/express/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "@mintlify/previewing/express/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "@mintlify/previewing/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@mintlify/previewing/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@mintlify/scraping/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@mintlify/scraping/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260708.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260708.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260708.1", "", { "os": "linux", "cpu": "x64" }, "sha512-3daE60YdD7YX0Jtuzc9DE/r/qMkmx8ZvHTkF8Mzmp3F5tbzlV0DAzmu5PFUPF2WuvtKbAhZKbvC2cHmWpQYxnA=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260708.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-VLdNYOx5Hj+9C6isy0ACWZsbMtSxex2DIJWEe7cZxUdlphZ58ZT8zxNXK8yunFiowd34hn3VwGMopdvdj8lvmA=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260708.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260708.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260708.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260708.1", "", { "os": "linux", "cpu": "x64" }, "sha512-3daE60YdD7YX0Jtuzc9DE/r/qMkmx8ZvHTkF8Mzmp3F5tbzlV0DAzmu5PFUPF2WuvtKbAhZKbvC2cHmWpQYxnA=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260708.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-VLdNYOx5Hj+9C6isy0ACWZsbMtSxex2DIJWEe7cZxUdlphZ58ZT8zxNXK8yunFiowd34hn3VwGMopdvdj8lvmA=="], + + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260708.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], + + "@opencode-ai/www/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "@opencode-ai/www/wrangler/miniflare/sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "@puppeteer/browsers/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@puppeteer/browsers/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@sentry/bundler-plugin-core/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "@slack/bolt/express/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "@slack/bolt/express/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="], "@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=="], @@ -7590,6 +9645,8 @@ "tw-to-css/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "@mintlify/common/sucrase/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "archiver-utils/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "archiver-utils/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], diff --git a/docs/design/service-lifecycle.md b/docs/design/service-lifecycle.md new file mode 100644 index 0000000000..e32a6cb441 --- /dev/null +++ b/docs/design/service-lifecycle.md @@ -0,0 +1,685 @@ +# Service Lifecycle: Election, Restart, and Reconnect + +Status: in progress + +Incident: [#36688](https://github.com/anomalyco/opencode/issues/36688) + +## Summary + +The managed V2 service keeps its current update policy: the background updater +may install a new package, but only a freshly launched TUI activates that update +after finding an older running service. Existing TUIs never replace a service; +they only reconnect. + +The restart path changes in three places: + +1. A process-held OS lock, not the HTTP port or registration file, elects + exactly one server owner for its lifetime. +2. The elected process binds and registers a minimal lifecycle surface before + it initializes the application, so clients can distinguish a slow winner + from an absent server. +3. TUIs rediscover and reconnect indefinitely. Transport loss is never a + terminal error by itself. + +Several clients may spawn small contenders during a restart. This is safe and +intentional: one contender acquires the lock and initializes, while every loser +exits before expensive server boot. The design does not require clients to +agree on a single initiator. + +This proposal does not introduce a supervisor process, warm candidate server, +protocol negotiation, idle background restart, or general execution-recovery +framework. + +## Architecture at a Glance + +```text + ╭───────────────────╮ + │ CLI ServiceConfig │ + ╰─────────┬─────────╯ + │ + ▼ + ╭──────────────────────╮ + │ CLI ServerConnection │ + ╰───────────┬──────────╯ + ╭──────────────────╰───────────────────╮ + ▼ ▼ +╭──────────────────────────╮ ╭─────────────────────────╮ +│ Client Service lifecycle │ │ CLI runPromiseWith seam │ +╰─────────────┬────────────╯ ╰─────────────┬───────────╯ + ╰─────╮ │ + ▼ ▼ + ╭────────────────────────────╮ ╭─────────────╮ + │ Background service process │ │ TUI / Solid │ + ╰──────────────┬─────────────╯ ╰──────┬──────╯ + │ │ + ╰────────────◀────────────────────╯ + ╭───────────────────────╮ + │ Server HTTP transport │ + ╰───────────┬───────────╯ + │ + ▼ + ╭──────────────────╮ + │ Core application │ + ╰──────────────────╯ +``` + +| Owner | Responsibility | +| ------------------------------------------------ | --------------------------------------------------------------------------------------------------- | +| `packages/client/src/effect/service.ts` | Effect-native discovery, start, and stop lifecycle operations | +| `packages/cli/src/services/service-config.ts` | CLI registration path, installed version, and daemon command | +| `packages/cli/src/services/server-connection.ts` | Resolve an endpoint and, only for the shared service, grouped reconnect and restart Effects | +| `packages/cli/src/server-process.ts` | Daemon election, registration, and server process boot | +| `packages/server/src/process.ts` | HTTP lifecycle shell and application transport | +| `packages/core` | Application behavior behind the transport | +| CLI default handler | Convert lifecycle Effects with the outer `FileSystem` context and pass grouped Promise capabilities | +| `packages/tui` Solid client context | Own event-stream reconnect, endpoint replacement, status, and user-triggered restart UI | + +## Implementation Status + +| Area | State | +| ------------------------- | --------------------------------------------------------------------- | +| Lifetime ownership | Implemented on this branch with a scoped OS lock | +| Contender behavior | Implemented; losers exit before the server module is imported | +| Registration repair | Implemented; the owner reasserts deleted or corrupt discovery | +| Channel isolation | Implemented with no-clobber migration for legacy preview discovery | +| Client startup waiting | Implemented; slow winners are not killed and waiting is indefinite | +| Lifecycle shell | Implemented; the owner binds and registers before application boot | +| Failed-state latching | Implemented; deterministic boot failure stays bound and actionable | +| Recovery diagnostics | Implemented; the TUI shows status instead of transport internals | +| Cross-platform validation | macOS runtime verified; Linux and Windows run in the unit-test matrix | + +## Context + +The V2 CLI runs a shared managed service that owns Sessions, location graphs, +plugins, permissions, and tool execution. The service updater can replace the +installed package while the current process continues running the old image. +A later TUI launch then detects the version mismatch and replaces the service. + +Incident #36688 showed four failures in that replacement path: + +- Multiple TUIs spawned heavyweight server contenders. +- A winner remained unobservable while it cold-booted, so another wave treated + it as absent and displaced it. +- A fresh TUI exhausted its reconnect budget and crashed with an unhandled + transport defect. +- A losing contender remained alive and consumed about 1 GB of RSS. + +The `origin/v2` baseline serializes service startup with `EffectFlock`. A +contender acquires a three-second heartbeat lease, checks whether another +service became discoverable, and only the winner crosses the application-boot +boundary. This already prevents simultaneous heavy boots and makes startup +losers exit. + +The lease is released immediately after registration, however, so it is not +lifetime ownership. Registration then reverts to last-writer-wins authority: a +deleted or corrupt registration can admit a second boot, a displaced server +terminates itself through its 10-second registration self-check, and a stalled +lease holder can be displaced after the three-second service staleness timeout. + +`Flock` and `EffectFlock` live in `packages/core/src/util` and are also used for +config writes, MCP auth, npm installs, and repository caching. Despite the +name, the primitive is an atomic-mkdir lease with heartbeat and staleness +takeover, not an OS-held lock. It remains appropriate for bounded critical +sections, including today's startup fence, but is not lifetime service +ownership. + +The current implementation also mixes three different concepts: + +- **Ownership:** which process is allowed to be the managed server. +- **Discovery:** where clients can reach that process. +- **Lifecycle:** whether that process is starting, ready, stopping, or failed. + +This design gives each concept one authority. + +```definitions +[ + { + "term": "Owner", + "definition": "The one process holding the process-held OS service lock." + }, + { + "term": "Contender", + "definition": "A small serve process attempting to acquire the service lock. It must not initialize the application before winning." + }, + { + "term": "Registration", + "definition": "An atomic discovery record containing the elected owner's identity and endpoint. Registration never grants ownership." + }, + { + "term": "Lifecycle shell", + "definition": "The minimal HTTP surface bound by the elected process before application initialization. It serves health and retryable startup responses." + }, + { + "term": "Application", + "definition": "The full server routes and global or location-scoped modules used for normal OpenCode work." + } +] +``` + +## Goals + +- At most one process initializes and serves the managed application. +- Losing contenders exit before database, route, plugin, MCP, or location boot. +- A slow winner becomes observable before expensive initialization. +- Existing and freshly launched TUIs survive retryable service unavailability. +- Reconnect follows service state instead of displaying retry counts or raw + transport failures. +- Version-mismatch replacement remains triggered by a fresh TUI launch. +- A stale or malformed registration cannot create a second owner. +- An unresponsive owner is never killed automatically by an arbitrary TUI. +- Every spawned contender has a bounded path to ownership or exit. + +## Non-goals + +- Restarting automatically when a background update finds an idle window. +- Running old and candidate application servers concurrently. +- Adding a permanent steward, proxy, or supervisor process. +- Zero-downtime worker handoff or automatic rollback. +- Application protocol negotiation or automatic TUI self-restart. +- General hard-crash recovery for active Sessions. +- Defining recovery semantics for provider attempts, tools, shells, sub-agents, + permissions, questions, or background jobs. +- Automatically killing a frozen owner. +- Bounding concurrent location cold boots after clients reconnect. +- Multi-machine or clustered service placement. + +## Invariants + +1. **The service lock is ownership.** Exactly one process may hold the OS lock + for one installation channel and service profile. +2. **Ownership precedes boot.** A contender performs no expensive application + initialization before it acquires the lock. +3. **Ownership lasts for the process lifetime.** The owner holds an open lock + handle until the managed server exits. The OS releases it on process death + without a cleanup callback. +4. **The port is transport, not election.** The owner may select a dynamic port + after acquiring the lock. +5. **Registration is discovery, not election.** Deleting, corrupting, or + replacing registration does not invalidate a live owner's lock. +6. **Only a fresh launch enforces package version.** Existing TUIs reconnect to + the current owner without initiating version replacement. +7. **Transport loss is retryable.** It never terminates a TUI without a separate + diagnosed, non-retryable cause. +8. **Clients do not kill an unresponsive owner automatically.** Destructive + recovery requires the explicit `service restart` command. +9. **Lifecycle does not promise execution semantics.** Graceful replacement + invokes Session suspension and resumption hooks, but tool-level continuity + belongs to a separate design. + +## System Model + +```text +╭───────────────────────╮ ╭──────────────────────────────╮ +│ Fresh or existing TUI │ │ Process-held OS service lock │ +╰───────────┬───────────╯ ╰───────────────┬──────────────╯ + ╰─────┬ normal requests observe ───────────────────────╮ │ + │ discover │ ├──╯ authorizes one owner + ▼ │ ▼ + ╭───────────────────╮ │ ╭─────────────────╮ + │ Registration file │ │ │ Lifecycle shell │ + ╰───────────────────╯ │ ╰────────┬────────╯ + │ │ + ├────────────────────────╯ + ▼ + ╭──────────────────────╮ + │ OpenCode application │ + ╰──────────────────────╯ +``` + +The lifecycle shell and application run in the same process. The distinction is +initialization order and responsibility, not process topology. + +## Service Status + +The server reports one small status value: + +```typescript +type ServiceStatus = + | { + type: "starting" + } + | { + type: "ready" + } + | { + type: "stopping" + targetVersion?: string + } + | { + type: "failed" + message: string + action: string + } +``` + +The client adds only the discovery states needed by callers: + +```typescript +type Status = { type: "missing" } | { type: "unreachable" } | { type: "unresponsive" } | ServiceStatus +``` + +The health response retains the existing fields for old clients and adds the +status discriminant: + +```typescript +type ServiceHealth = { + healthy: true + version: string + pid: number + instanceID: string + status: ServiceStatus +} +``` + +`healthy: true` means the registered lifecycle shell is responding and its +identity matches registration. New clients use `status.type === "ready"` as +the application-readiness signal. + +During `starting` or `stopping`, application requests are not held in memory. +They receive an immediate retryable response: + +```http +HTTP/1.1 503 Service Unavailable +Retry-After: 1 +Content-Type: application/json + +{"code":"service_starting"} +``` + +`stopping` uses `service_stopping`. A failed application boot uses +`service_failed` and includes a safe diagnostic message. + +A failed owner remains bound and keeps holding the service lock. Exiting on +failure would let every waiting client's `ensureRunning` loop elect a new +contender that repeats the same heavy failing boot, so staying bound turns a +deterministic boot failure into one observable `failed` state instead of a +client-driven respawn loop. Recovery still works: a fresh launch observes the +failed instance through the stop path, and explicit `service restart` replaces +it. + +## Registration Contract + +Registration contains only discovery identity: + +```typescript +type ServiceRegistration = { + schema: 1 + instanceID: string + version: string + url: string + pid: number +} +``` + +Authentication continues to use the existing private service credential +storage. The registration schema does not change that policy. + +The owner writes registration only after the lifecycle shell has bound: + +1. Bind the lifecycle shell. +2. Write a temporary registration file with mode `0600`. +3. Atomically rename it over the old registration. +4. Serve lifecycle health as `starting`. + +On shutdown, the owner removes registration only if the current file still has +its `instanceID`. An old finalizer can never remove a successor's registration. + +While running, the owner periodically asserts its registration. Because the +lock guarantees exactly one live owner, any registration that does not name the +owner is stale or corrupt, and the owner rewrites it. A deleted or clobbered +registration therefore heals within one assertion interval instead of leaving +clients waiting on absent discovery. This inverts today's self-check loop, +which terminates the displaced process instead of repairing discovery. + +Legacy registration shapes are decoded by a compatibility adapter. The new +domain type does not make fields optional to represent old formats. + +## Election + +This design promotes today's startup fence into lifetime ownership. +Last-writer-wins registration is replaced by a process-held OS lock that is +acquired before any expensive boot work and held for the entire service +lifetime. + +A heartbeat-and-staleness lease, including the existing `Flock` utility, is not +sufficient for service ownership: the service configures a three-second stale +timeout, after which its lock can be broken and recreated. An event-loop stall, +a suspended machine, or a debugger pause can therefore make a live owner appear +stale and allow a contender to displace it. Service ownership requires a +process-held OS lock: `flock` on Unix and an exclusively bound named pipe on +Windows. It cannot be broken because a heartbeat exceeded a timeout. Process +death releases the lock through the OS. + +Neither Bun nor Node exposes `flock` directly, the existing `Flock` utility is +an mkdir-plus-heartbeat lease rather than an OS-held lock, and the common +lockfile packages are staleness-based leases as well. The platform layer uses +`bun:ffi` to call `flock` on POSIX and Node's named-pipe server support on +Windows, where Bun FFI is not available on every shipped architecture. It lives +alongside the existing utility in `packages/core/src/util`. This primitive is +the foundation of the design, so the delivery sequence spikes it first. + +```text +Contender Lock Lifecycle Application + │ │ │ │ + ├─ try acquire ───▶ │ │ + │ │ │ │ + ╭─ alt: lock held ────────────────────────────────────────────────╮ + │ │ │ │ │ │ + │ ◀─ busy ──────────┤ │ │ │ + │ │ │ │ │ │ + │ ├─────────╮ │ │ │ │ + │ │ exit │ │ │ │ │ + │ ◀─────────╯ │ │ │ │ + │ │ │ │ │ │ + ├─ else: lock acquired ───────────────────────────────────────────┤ + │ │ │ │ │ │ + │ ◀─ owner ─────────┤ │ │ │ + │ │ │ │ │ │ + │ ├─ bind, register, starting ────────▶ │ │ + │ │ │ │ │ │ + │ ├─ initialize ──────────────────────────────────────────────▶ │ + │ │ │ │ │ │ + │╭─ alt: boot succeeds ──────────────────────────────────────────╮│ + ││ │ │ │ │ ││ + ││ │ │ ◀─ ready ───────────────┤ ││ + ││ │ │ │ │ ││ + │├─ else: boot fails ────────────────────────────────────────────┤│ + ││ │ │ │ │ ││ + ││ │ │ ◀─ failed, stay bound ──┤ ││ + ││ │ │ │ │ ││ + │╰───────────────────────────────────────────────────────────────╯│ + │ │ │ │ │ │ + ╰─────────────────────────────────────────────────────────────────╯ + │ │ │ │ +``` + +Lock acquisition by a contender is nonblocking or tightly bounded. A loser +must exit before constructing application routes or importing startup-heavy +modules. + +Several clients may spawn contenders concurrently. The design guarantees one +heavy winner, not one process spawn. If the winner crashes during startup, the +OS releases the lock and a later client retry starts another election. + +The lock is scoped by installation channel and service profile. Local, preview, +and stable installations cannot displace one another. + +## Update Activation + +Background update behavior remains unchanged: + +1. The running service checks for an update. +2. The updater installs the package in the background. +3. The running process continues using its existing process image. +4. No idle check or automatic restart occurs. + +A fresh TUI launch activates the installed update: + +1. Read registration and authenticate the responding service. +2. If its package version matches the fresh client, attach normally. +3. If the version differs, request graceful stop of that exact registered + instance using the existing authenticated stop path. +4. Re-check instance identity before every signal or escalation in that path. +5. Wait for the old process to exit and release the service lock. +6. Call `ensureRunning` until a compatible service becomes ready. + +Concurrent fresh launchers may all observe the same old instance. Stopping that +exact instance must be idempotent. Once registration names a different instance, +a stale launcher stops signaling and returns to discovery. + +No durable restart-transition record is introduced. The initiating fresh TUI +already knows the source and target versions and can display its update +preflight. Existing TUIs may display `Updating...` if they observed `stopping`; +otherwise `Waiting for background service...` is the honest fallback. + +## Fresh Launch Versus Reconnect + +Fresh launch and reconnect deliberately have different version policies: + +```typescript +type ManagedConnection = + | { + type: "launch" + requiredVersion: string + } + | { + type: "reconnect" + } +``` + +- `launch` requires the installed package version and may activate replacement. +- `reconnect` accepts the current owner and never activates replacement. + +This preserves today's permissive reconnect behavior. Explicit application +protocol negotiation and automatic TUI re-exec remain follow-ups. + +## Client Reconnect + +Fresh and existing TUIs use the same status loop after startup: + +1. Read registration on every attempt. Do not retry a stale URL indefinitely. +2. If registration is absent, call `ensureRunning` and continue waiting. +3. If registration is unreachable, call `ensureRunning`. A live owner prevents + contenders from acquiring the lock; a dead owner does not. +4. If status is `starting` or `stopping`, wait. +5. If status is `failed`, show its actionable message. +6. If status is `ready`, rebuild HTTP and event-stream clients for the new + endpoint and perform authoritative state reconciliation. + +Retry cadence is internal policy. Retry counts are telemetry, not user-facing +state. The TUI waits until the service is ready or the user exits. + +Transport failures are handled at the TUI run boundary. A raw client transport +error or Effect defect must not escape to the terminal. Hard exit is reserved +for diagnosed causes such as invalid local configuration, failed authentication, +or a foreign process occupying an explicitly configured port. + +The UI derives text from status: + +| Status | User-facing state | +| ------------------------ | ----------------------------------- | +| No registration | `Starting background service...` | +| Registration unreachable | `Waiting for background service...` | +| `starting` | `Starting OpenCode vX...` | +| `stopping` | `Updating to vX...` | +| `failed` | Actionable failure message | +| `ready` | Normal TUI | + +## Graceful Session Continuity + +Version-mismatch replacement uses the existing graceful Session suspension and +resumption hooks: + +1. The old server snapshots active Session IDs during graceful teardown. +2. The successor schedules those Sessions for continuation. +3. The runner reloads durable Session history before continuing. + +This lifecycle design does not define what an interrupted physical provider +attempt or tool invocation means. It does not promise that external side effects +did not occur, replay the exact interrupted tool, preserve an in-memory form, or +recover process-local background work. + +Those concerns require a separate execution-continuity design covering tools, +shells, sub-agents, permissions, questions, provider attempts, and hard-crash +recovery. + +## Unresponsive Owner + +An unreachable registration does not prove that the owner is dead. A contender +attempts the service lock: + +- If the lock is free, the contender starts a replacement. +- If the lock is held, the contender exits and the client keeps waiting. + +After a bounded diagnostic threshold, the client may show: + +```text +The background service owns the service lock but is not responding. +Run `opencode service restart` to recover it. +``` + +Only explicit `service restart` may perform destructive recovery. It verifies +the complete registration and process instance before signaling, waits for +graceful exit, re-checks identity before escalation, and refuses to kill a +process it cannot positively identify. + +Automatic frozen-owner recovery is deferred. + +## Failure Walkthroughs + +### Update with open TUIs + +1. The old service installs vNext but keeps running. +2. A fresh vNext TUI finds the healthy vOld service and requests graceful stop. +3. The old service reports `stopping`, suspends active Sessions, and exits. +4. Open TUIs enter their indefinite status loops. +5. One or more clients spawn contenders. +6. One contender acquires the service lock. Losers exit before heavy boot. +7. The winner binds and registers the lifecycle shell as `starting`. +8. Clients stop spawning and wait on the observable winner. +9. The winner initializes the application and reports `ready`. +10. TUIs rebuild clients, reconcile state, and resume. + +### Server crashes while ready + +1. The endpoint becomes unreachable and registration may remain stale. +2. Clients call `ensureRunning`. +3. Process death has released the service lock. +4. One contender wins, replaces registration, and starts normally. +5. Detailed active-execution recovery is outside this design. + +### Winner crashes during startup + +1. Clients observed `starting` and remain alive. +2. Process death releases the service lock. +3. A later reconnect attempt starts another election. +4. One new contender wins; all other contenders exit. + +### Registration is deleted while the owner is healthy + +1. Clients may call `ensureRunning` because discovery is absent. +2. Every contender fails to acquire the owner's lock and exits. +3. No second application initializes. +4. The owner's next registration assertion republishes discovery. + +### Owner is alive but unresponsive + +1. Health fails, but the process still holds the service lock. +2. Contenders fail lock acquisition and exit. +3. Clients wait and eventually show explicit recovery guidance. +4. No TUI kills the owner automatically. + +## TDD Verification + +Implementation should proceed test-first with real subprocesses and real locks. +Mocks cannot establish process death, lock release, loser cleanup, or port +behavior. + +### Election tests + +| Scenario | Required result | +| ----------------------------------------------------- | ------------------------------------------------------- | +| Ten contenders start simultaneously | Exactly one crosses the application-boot boundary | +| Winner pauses after lock acquisition | No loser initializes or remains alive | +| Winner event loop pauses beyond the old stale timeout | Ownership is not displaced | +| Winner crashes before bind | Lock releases; a later attempt wins | +| Winner crashes after bind but before registration | Lock releases; a later attempt replaces stale discovery | +| Registration is deleted while owner runs | No second owner initializes | +| Registration is malformed | Lock still prevents a second owner | +| Registration names a dead PID | New contender can acquire the released lock | +| Two installation channels start | Each elects an independent owner | +| Explicit configured port is foreign-owned | Fail diagnostically; do not kill the foreign process | + +The fixture records a marker immediately before application initialization. The +tests assert that only one process writes that marker and that every loser exits +within a bounded interval. The harness should also assert that a loser's peak +RSS stays an order of magnitude below an application boot, since import weight +was the observed incident cost. + +### Lifecycle tests + +| Scenario | Required result | +| ----------------------------------------------- | ---------------------------------------------------------------- | +| Winner owns lock but application boot is paused | Health reports `starting` | +| Application request arrives during startup | Immediate retryable `503` | +| Application becomes ready | Status changes once from `starting` to `ready` | +| Graceful replacement begins | Status reports `stopping` before disconnect | +| Application initialization fails | Actionable `failed` status; owner stays bound and holds the lock | +| Registration is deleted while owner runs | Owner republishes it within one assertion interval | +| Owner exits | Registration is removed only if it still names that owner | + +### Update tests + +| Scenario | Required result | +| -------------------------------------- | -------------------------------------------------------- | +| Background update installs vNext | Running vOld service does not restart | +| Fresh vNext launch finds vOld | Exact old instance stops; vNext eventually becomes ready | +| Two fresh vNext launches race | One heavy successor; both clients attach | +| Existing vOld TUI reconnects to vNext | It never requests replacement | +| Stale launcher observes a new instance | It does not signal the new instance | + +### Reconnect tests + +| Scenario | Required result | +| --------------------------------------------------- | -------------------------------------------------- | +| Endpoint disappears and changes port | TUI rediscovers and rebuilds clients | +| Service remains unavailable beyond old retry budget | TUI remains alive | +| Event stream reconnects | Client performs authoritative state reconciliation | +| Transport returns an unexpected defect | TUI formats it; no raw stack escapes | +| Owner remains unresponsive | TUI waits and shows explicit restart guidance | + +## Delivery Sequence + +1. **Spike the lock primitive.** Prove a nonblocking, process-held OS lock + under Bun on macOS, Linux, and Windows (`bun:ffi` to `flock` on POSIX and a + named pipe on Windows), including release on hard kill and behavior across + containers and network filesystems used in CI. +2. **Expand the subprocess test harness.** Begin from the baseline + two-contender test and cover ten contenders, lock release on crash, a paused + winner, deleted or corrupt registration, and bounded loser exit before + changing ownership. +3. **Contain client failure.** Make transport loss nonterminal, rediscover on + every cycle, and format unexpected failures at the TUI boundary. +4. **Promote the startup fence to process-held ownership.** Preserve the + existing pre-boot acquisition seam, replace its lease with the OS lock, hold + it until process exit, and invert the registration self-check from + self-termination to reassertion. +5. **Bind the lifecycle shell first.** Publish registration and `starting`, + return retryable `503` for application requests, then initialize the app. + The health contract change is public API: regenerate clients from + `packages/client` with `bun run generate`. +6. **Codify launch versus reconnect.** Fresh launch enforces installed version; + reconnect never activates replacement. +7. **Integrate graceful replacement.** Preserve current background-install and + fresh-launch activation behavior while invoking Session continuity hooks. +8. **Harden explicit recovery.** Verify exact process identity during explicit + `service restart`; never automatically kill an unresponsive owner. +9. **Run the full multi-process suite.** Include repeated restart cycles and + assert that no contender or child process remains afterward. + +## Acceptance Criteria + +- Ten concurrent restart observers produce one application initialization. +- No losing contender survives or builds a location graph. +- A 30-second application boot remains continuously observable as `starting`. +- A TUI remains alive through a service outage longer than the previous retry + budget. +- A service endpoint change does not require restarting an existing TUI. +- Background installation alone does not restart the service. +- A fresh mismatched TUI eventually attaches to the installed service version. +- Existing reconnecting TUIs never replace the current owner. +- Registration corruption cannot produce two owners. +- A deleted registration heals without restarting the owner or any client. +- An unresponsive owner is not killed without an explicit recovery command. +- Raw transport defects never escape to the terminal. + +## Follow-ups + +- Idle background update activation with an admission fence. +- Application protocol compatibility and automatic local TUI re-exec. +- Durable execution recovery for provider attempts and tools. +- Shell, sub-agent, permission, question, and background-job continuity. +- Automatic recovery for a positively identified frozen owner. +- Cold-boot concurrency limits and interaction-prioritized location loading. +- A steward or socket-handoff architecture if zero-downtime replacement becomes + a real requirement. diff --git a/github/index.ts b/github/index.ts index 4e1af9cf55..e8acd9a9ce 100644 --- a/github/index.ts +++ b/github/index.ts @@ -495,7 +495,6 @@ async function subscribeSessionEvents() { console.log("Subscribing to session events...") const TOOL: Record = { - todowrite: ["Todo", "\x1b[33m\x1b[1m"], bash: ["Bash", "\x1b[31m\x1b[1m"], edit: ["Edit", "\x1b[32m\x1b[1m"], glob: ["Glob", "\x1b[34m\x1b[1m"], diff --git a/infra/console.ts b/infra/console.ts index 79556f5e0c..5fbf35de00 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -288,12 +288,8 @@ new sst.cloudflare.x.SolidStart("Console", { server: { placement: { region: "aws:us-east-2" }, transform: { - worker: (args) => { - args.compatibilityFlags = $resolve(args.compatibilityFlags).apply((flags) => [ - ...(flags ?? []), - "global_fetch_strictly_public", - ]) - args.tailConsumers = [{ service: logProcessor.nodes.worker.scriptName }] + worker: { + tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }], }, }, }, diff --git a/infra/stage.ts b/infra/stage.ts index f0db797448..8d80eefed8 100644 --- a/infra/stage.ts +++ b/infra/stage.ts @@ -8,25 +8,6 @@ export const zoneID = "430ba34c138cfb5360826c4909f99be8" export const awsStage = $app.stage === "production" ? "production" : "dev" export const deployAws = $app.stage === awsStage -if ($app.stage === "production") { - new cloudflare.DnsRecord("TrustCenter", { - zoneId: zoneID, - name: "trust.opencode.ai", - type: "CNAME", - content: "3a69a5bb27875189.vercel-dns-016.com", - proxied: false, - ttl: 60, - }) - - new cloudflare.DnsRecord("TrustCenterVerification", { - zoneId: zoneID, - name: "opencode.ai", - type: "TXT", - content: "compai-domain-verification=org_6993a99c6200a2d642bb115d", - ttl: 60, - }) -} - new cloudflare.RegionalHostname("RegionalHostname", { hostname: domain, regionKey: "us", diff --git a/nix/desktop.nix b/nix/desktop.nix index 2df62f7a1c..d0d7fa7eca 100644 --- a/nix/desktop.nix +++ b/nix/desktop.nix @@ -8,8 +8,6 @@ makeWrapper, writableTmpDirAsHomeHook, autoPatchelfHook, - copyDesktopItems, - makeDesktopItem, opencode, }: let @@ -29,12 +27,9 @@ stdenv.mkDerivation (finalAttrs: { nodejs makeWrapper writableTmpDirAsHomeHook - ] - ++ lib.optionals stdenv.hostPlatform.isLinux [ + ] ++ lib.optionals stdenv.hostPlatform.isLinux [ autoPatchelfHook - copyDesktopItems - ] - ++ lib.optionals stdenv.hostPlatform.isDarwin [ + ] ++ lib.optionals stdenv.hostPlatform.isDarwin [ # Ad-hoc sign the .app: --config.mac.identity=null below skips signing. darwin.autoSignDarwinBinariesHook ]; @@ -43,37 +38,20 @@ stdenv.mkDerivation (finalAttrs: { (lib.getLib stdenv.cc.cc) ]; - desktopItems = lib.optional stdenv.hostPlatform.isLinux (makeDesktopItem { - name = "ai.opencode.desktop"; - desktopName = "OpenCode"; - exec = "opencode-desktop %U"; - icon = "ai.opencode.desktop"; - # Electron 41 derives X11 WM_CLASS from app.name. - startupWMClass = "OpenCode"; - categories = [ "Development" ]; - }); - env = opencode.env // { ELECTRON_SKIP_BINARY_DOWNLOAD = "1"; }; - postPatch = - # NOTE: Relax Bun version check to be a warning instead of an error - '' - substituteInPlace packages/script/src/index.ts \ - --replace-fail 'throw new Error(`This script requires bun@''${expectedBunVersionRange}' \ - 'console.warn(`Warning: This script requires bun@''${expectedBunVersionRange}' - '' - # https://github.com/electron/electron/issues/31121 - # mac builds use a .app bundle which doesnt have this issue - + lib.optionalString stdenv.isLinux '' - BASE_PATH=packages/desktop - FILES=(src/main/windows.ts) - for file in "''${FILES[@]}"; do - substituteInPlace $BASE_PATH/$file \ - --replace-fail "process.resourcesPath" "'$out/opt/opencode-desktop/resources'" - done - ''; + # https://github.com/electron/electron/issues/31121 + # mac builds use a .app bundle which doesnt have this issue + postPatch = lib.optionalString stdenv.isLinux '' + BASE_PATH=packages/desktop + FILES=(src/main/windows.ts) + for file in "''${FILES[@]}"; do + substituteInPlace $BASE_PATH/$file \ + --replace-fail "process.resourcesPath" "'$out/opt/opencode-desktop/resources'" + done + ''; preBuild = '' cp -r "${electron.dist}" $HOME/.electron-dist @@ -98,38 +76,27 @@ stdenv.mkDerivation (finalAttrs: { runHook postBuild ''; - installPhase = '' - runHook preInstall - '' - + lib.optionalString stdenv.hostPlatform.isDarwin '' - mkdir -p $out/Applications - mv dist/mac*/*.app $out/Applications - makeWrapper "$out/Applications/OpenCode.app/Contents/MacOS/OpenCode" $out/bin/opencode-desktop - '' - + lib.optionalString stdenv.hostPlatform.isLinux '' - mkdir -p $out/opt/opencode-desktop - cp -r dist/linux*-unpacked/{resources,LICENSE*} $out/opt/opencode-desktop - install -Dm644 resources/icons/32x32.png \ - "$out/share/icons/hicolor/32x32/apps/ai.opencode.desktop.png" - install -Dm644 resources/icons/64x64.png \ - "$out/share/icons/hicolor/64x64/apps/ai.opencode.desktop.png" - install -Dm644 resources/icons/128x128.png \ - "$out/share/icons/hicolor/128x128/apps/ai.opencode.desktop.png" - install -Dm644 resources/icons/128x128@2x.png \ - "$out/share/icons/hicolor/256x256/apps/ai.opencode.desktop.png" - install -Dm644 resources/icons/icon.png \ - "$out/share/icons/hicolor/512x512/apps/ai.opencode.desktop.png" - install -Dm644 resources/ai.opencode.desktop.metainfo.xml \ - "$out/share/metainfo/ai.opencode.desktop.metainfo.xml" - makeWrapper ${lib.getExe electron} $out/bin/opencode-desktop \ - --inherit-argv0 \ - --set ELECTRON_FORCE_IS_PACKAGED 1 \ - --add-flags $out/opt/opencode-desktop/resources/app.asar \ - --add-flags "\''${NIXOS_OZONE_WL:+\''${WAYLAND_DISPLAY:+--ozone-platform-hint=auto --enable-features=WaylandWindowDecorations --enable-wayland-ime=true}}" - '' - + '' - runHook postInstall - ''; + installPhase = + '' + runHook preInstall + '' + + lib.optionalString stdenv.hostPlatform.isDarwin '' + mkdir -p $out/Applications + mv dist/mac*/*.app $out/Applications + makeWrapper "$out/Applications/OpenCode.app/Contents/MacOS/OpenCode" $out/bin/opencode-desktop + '' + + lib.optionalString stdenv.hostPlatform.isLinux '' + mkdir -p $out/opt/opencode-desktop + cp -r dist/linux*-unpacked/{resources,LICENSE*} $out/opt/opencode-desktop + makeWrapper ${lib.getExe electron} $out/bin/opencode-desktop \ + --inherit-argv0 \ + --set ELECTRON_FORCE_IS_PACKAGED 1 \ + --add-flags $out/opt/opencode-desktop/resources/app.asar \ + --add-flags "\''${NIXOS_OZONE_WL:+\''${WAYLAND_DISPLAY:+--ozone-platform-hint=auto --enable-features=WaylandWindowDecorations --enable-wayland-ime=true}}" + '' + + '' + runHook postInstall + ''; autoPatchelfIgnoreMissingDeps = [ "libc.musl-x86_64.so.1" diff --git a/nix/hashes.json b/nix/hashes.json index 25f0e76812..9d85faba7f 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-a7NyYa9vRUEqDfZNDPXXmFO58RDEgioyuGSl5CPBvxo=", - "aarch64-linux": "sha256-l4OJtSEllHvRhktjcaJYwkXBSaJvsIoyoLusbZfYMcM=", - "aarch64-darwin": "sha256-IIl0BQGs1/HLFh0auiQjiwfSQ2nfHcK2G2BAphYW59c=", - "x86_64-darwin": "sha256-vVeuPyd4ZIRYrHouphTuEb4rkRZLKKTAHe840jNh9rU=" + "x86_64-linux": "sha256-F1luclnqCPQk9yxfmeSYGaM/nScf28yBu9K3Fv+Xd24=", + "aarch64-linux": "sha256-XW0XZnsCRkU3MFJH9TjMRYZHffzVy3cQyiNCkec2gl4=", + "aarch64-darwin": "sha256-bf8kvORs3Fs2UYLp3PekF+AJR7NKOcHb+fIQA79RtMk=", + "x86_64-darwin": "sha256-sBdQPkzd7JXNW6Lbi9JHiAsfHwdLwTKWY+uPeXAv2Nw=" } } diff --git a/package.json b/package.json index 2a8eef6356..c8b778f207 100644 --- a/package.json +++ b/package.json @@ -2,18 +2,24 @@ "$schema": "https://json.schemastore.org/package.json", "name": "opencode", "description": "AI-powered development tool", + "version": "0.0.0", "private": true, "type": "module", "packageManager": "bun@1.3.14", "scripts": { - "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", + "dev": "bun run --cwd packages/cli --conditions=browser src/index.ts", "dev:desktop": "bun --cwd packages/desktop dev", "dev:web": "bun --cwd packages/app dev", "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev", "dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev", + "dev:www": "bun run --cwd packages/www dev", "dev:storybook": "bun --cwd packages/storybook storybook", "lint": "oxlint", - "typecheck": "bun turbo typecheck", + "lint:effect-patterns": "ast-grep scan -c script/ast-grep/sgconfig.yml packages/core/src packages/server/src packages/protocol/src packages/cli/src", + "test:lint-rules": "ast-grep test -c script/ast-grep/sgconfig.yml", + "typecheck": "bun turbo typecheck --concurrency=3", + "typecheck:profile": "bun script/profile-typecheck.ts", + "typecheck:profile:packages": "bun script/profile-typecheck-packages.ts", "upgrade-opentui": "bun run script/upgrade-opentui.ts", "postinstall": "bun run --cwd packages/core fix-node-pty", "prepare": "husky", @@ -31,9 +37,9 @@ "packages/slack" ], "catalog": { - "@effect/opentelemetry": "4.0.0-beta.83", - "@effect/platform-node": "4.0.0-beta.83", - "@effect/sql-sqlite-bun": "4.0.0-beta.83", + "@effect/opentelemetry": "4.0.0-beta.98", + "@effect/platform-node": "4.0.0-beta.98", + "@effect/sql-sqlite-bun": "4.0.0-beta.98", "@npmcli/arborist": "9.4.0", "@types/bun": "1.3.13", "@types/cross-spawn": "6.0.6", @@ -63,12 +69,13 @@ "dompurify": "3.3.1", "drizzle-kit": "1.0.0-rc.2", "drizzle-orm": "1.0.0-rc.2", - "effect": "4.0.0-beta.83", + "effect": "4.0.0-beta.98", "ai": "6.0.168", "cross-spawn": "7.0.6", "hono": "4.10.7", "hono-openapi": "1.1.2", "fuzzysort": "3.1.0", + "get-east-asian-width": "1.6.0", "luxon": "3.6.1", "marked": "17.0.6", "marked-shiki": "1.2.1", @@ -79,9 +86,11 @@ "@typescript/native-preview": "7.0.0-dev.20251207.1", "zod": "4.1.8", "remeda": "2.26.0", + "resolve.exports": "2.0.3", "sst": "4.13.1", "shiki": "4.2.0", "solid-list": "0.3.0", + "string-width": "7.2.0", "tailwindcss": "4.1.11", "vite": "7.1.4", "@solidjs/meta": "0.29.4", @@ -90,13 +99,15 @@ "@sentry/solid": "10.36.0", "@sentry/vite-plugin": "4.6.0", "solid-js": "1.9.10", - "solid-sonner": "0.3.1", "vite-plugin-solid": "2.11.10", "@lydell/node-pty": "1.2.0-beta.12" } }, "devDependencies": { "@actions/artifact": "5.0.1", + "@ast-grep/cli": "0.44.0", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.3", "@tsconfig/bun": "catalog:", "@types/mime-types": "3.0.1", "@typescript/native-preview": "catalog:", @@ -144,20 +155,18 @@ "@types/node": "catalog:" }, "patchedDependencies": { - "@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch", "@ff-labs/fff-bun@0.9.3": "patches/@ff-labs%2Ffff-bun@0.9.3.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", - "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", "pacote@21.5.0": "patches/pacote@21.5.0.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", - "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", + "effect@4.0.0-beta.98": "patches/effect@4.0.0-beta.98.patch", "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch" } } diff --git a/packages/llm/AGENTS.md b/packages/ai/AGENTS.md similarity index 91% rename from packages/llm/AGENTS.md rename to packages/ai/AGENTS.md index c4edaba2b4..997615ce0d 100644 --- a/packages/llm/AGENTS.md +++ b/packages/ai/AGENTS.md @@ -1,4 +1,4 @@ -# LLM Package Guide +# AI Package Guide ## Effect @@ -113,10 +113,27 @@ Keep provider facades small and explicit: `Provider.make(...)` remains available for simple static provider definitions, but new built-in providers should prefer plain configured facades unless a helper removes real duplication without adding runtime behavior. +### Provider Package Entrypoints + +Catalog-selected native providers use package-like export paths from `@opencode-ai/ai`. They are internal entrypoints in one npm package, not separately published provider packages. Every entrypoint implements `ProviderPackage.Definition` and exposes `model(modelID, settings)`, where settings are serializable provider configuration plus common `headers`, `body`, and `limits` overlays. + +```ts +import { model } from "@opencode-ai/ai/providers/openai/responses" + +const selected = model("gpt-5", { + apiKey, + transport: "websocket", +}) +``` + +Keep semantic APIs as separate entrypoints, such as OpenAI `chat` and `responses`. Keep transport choices inside the semantic entrypoint settings, so OpenAI Responses HTTP and WebSocket share one entrypoint. Provider facades may still expose named selectors such as `responsesWebSocket` for direct typed call sites; the package-like contract maps its settings to those selectors before returning an executable `Model`. + +Do not expose `Route` in provider package settings. Route composition stays an implementation detail behind `model(...)`. + ### Folder layout ``` -packages/llm/src/ +packages/ai/src/ schema/ canonical Schema model, split by concern ids.ts branded IDs, literal types, ProviderMetadata options.ts Generation/Provider/Http options, Limits, Model, cache policy @@ -126,7 +143,7 @@ packages/llm/src/ index.ts barrel llm.ts request constructors and convenience helpers route/ - index.ts @opencode-ai/llm/route advanced barrel + index.ts @opencode-ai/ai/route advanced barrel client.ts Route.make + LLMClient.prepare/stream/generate executor.ts RequestExecutor service + transport error mapping protocol.ts Protocol type + Protocol.make @@ -147,9 +164,11 @@ packages/llm/src/ bedrock-converse.ts bedrock-event-stream.ts framing for AWS event-stream binary frames openai-compatible-chat.ts route that reuses OpenAIChat.protocol, no canonical URL + openai-compatible-responses.ts route that reuses OpenAIResponses.protocol, no canonical URL utils/ per-protocol helpers (auth, cache, media, tool-stream, ...) providers/ - openai-compatible.ts generic compatible helper + family model helpers + openai-compatible.ts generic Chat helper + family model helpers + openai-compatible-responses.ts generic Responses helper 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 @@ -303,7 +322,7 @@ recorded.effect("streams text", () => ) ``` -Replay is the default. `RECORD=true` records fresh cassettes and requires the listed env vars. Cassettes are written as pretty-printed JSON so multi-interaction diffs stay reviewable. +Replay is the default. `RECORD=true` records fresh cassettes locally and requires the listed env vars; unset `CI` before recording because CI always forces replay. Cassettes are written as pretty-printed JSON so multi-interaction diffs stay reviewable. Pass `provider`, `protocol`, and optional `tags` to `recordedTests(...)` / `recorded.effect.with(...)` so cassettes carry searchable metadata. Use recorded-test filters to replay or record a narrow subset without rewriting a whole file: @@ -316,6 +335,6 @@ Filters apply in replay and record mode. Combine them with `RECORD=true` when re **Binary response bodies.** Most providers stream text (SSE, JSON). The recorder treats known textual media types (`text/*`, JSON/XML structured types, JavaScript, forms, YAML, and SVG) as text and stores every other response as base64 with `bodyEncoding: "base64"`. This preserves binary formats such as AWS event-stream frames without a lossy UTF-8 round trip. -**Matching strategy.** Replay walks the cassette in record order via an internal cursor: the Nth runtime request is served by the Nth recorded interaction, and each one is validated by comparing method, URL, allow-listed headers, and the canonical JSON body. This handles tool loops (each round's request differs as history grows) and retry/polling scenarios (successive byte-identical requests with different responses) uniformly. If a test reorders its requests, re-record the cassette. `scriptedResponses` (in `test/lib/http.ts`) is the deterministic counterpart for tests that don't need a live provider; it scripts response bodies in order without reading from disk. +**Matching strategy.** A runtime request atomically claims the first unused recorded interaction that matches its method, URL, allow-listed headers, and canonical JSON body. Distinct requests may replay in any order or concurrently. Repeated identical requests consume their matching responses in cassette order, preserving deterministic retry and polling behavior. `scriptedResponses` (in `test/lib/http.ts`) is the deterministic counterpart for tests that don't need a live provider; it scripts response bodies in order without reading from disk. Do not blanket re-record an entire test file when adding one cassette. `RECORD=true` rewrites every recorded case that runs, and provider streams contain volatile IDs, timestamps, fingerprints, and obfuscation fields. Prefer deleting the one cassette you intend to refresh, or run a focused test pattern that only registers the scenario you want to record. Keep stable existing cassettes unchanged unless their request shape or expected behavior changed. diff --git a/packages/llm/DESIGN.md b/packages/ai/DESIGN.md similarity index 99% rename from packages/llm/DESIGN.md rename to packages/ai/DESIGN.md index 22e76969a4..2e73360300 100644 --- a/packages/llm/DESIGN.md +++ b/packages/ai/DESIGN.md @@ -1,7 +1,7 @@ # AI Library Design -> Discussion draft. This document describes the intended replacement for the -> current private `@opencode-ai/llm` API. Names and exact TypeScript signatures +> Discussion draft. This document describes an intended clean-break redesign of +> the current private `@opencode-ai/ai` API. Names and exact TypeScript signatures > are illustrative until implementation, but the domain boundaries and defaults > are deliberate. @@ -1074,7 +1074,6 @@ The redesign intentionally removes or changes these current concepts: | Current | Proposed | | --------------------------------------- | ----------------------------------------------------------- | -| `@opencode-ai/llm` | `@opencode-ai/ai` | | Mandatory `LLM.request({ model, ... })` | Inline calls or model-free portable requests | | `LLM.generate` means one turn | `LLM.generate` means complete run | | `LLMClient.generate/stream` | `LLM.generateTurn/streamTurn` for one turn | diff --git a/packages/llm/README.md b/packages/ai/README.md similarity index 60% rename from packages/llm/README.md rename to packages/ai/README.md index 020198dd64..b9139bc3af 100644 --- a/packages/llm/README.md +++ b/packages/ai/README.md @@ -1,11 +1,11 @@ -# @opencode-ai/llm +# @opencode-ai/ai Schema-first LLM core for opencode. One typed request, response, event, and tool language; provider quirks live in adapters, not in calling code. ```ts import { Effect } from "effect" -import { LLM, LLMClient } from "@opencode-ai/llm" -import { OpenAI } from "@opencode-ai/llm/providers" +import { LLM, LLMClient } from "@opencode-ai/ai" +import { OpenAI } from "@opencode-ai/ai/providers" const model = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY }).responses("gpt-4o-mini") @@ -95,7 +95,7 @@ Normalized cache usage is read back into `response.usage.cacheReadInputTokens` a Provider facades configure endpoint/auth/deployment details first, then expose model selectors that take only a model or deployment id. The selected model carries the executable route value used at runtime. ```ts -import { OpenAI, CloudflareAIGateway } from "@opencode-ai/llm/providers" +import { OpenAI, CloudflareAIGateway } from "@opencode-ai/ai/providers" const openai = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY }).responses("gpt-4o-mini") const gateway = CloudflareAIGateway.configure({ @@ -104,7 +104,67 @@ const gateway = CloudflareAIGateway.configure({ }).model("workers-ai/@cf/meta/llama-3.1-8b-instruct") ``` -Included providers: OpenAI, Anthropic, Google (Gemini), Amazon Bedrock, Azure OpenAI, Cloudflare AI Gateway, Cloudflare Workers AI, GitHub Copilot, OpenRouter, xAI, plus generic OpenAI-compatible helpers for DeepSeek, Cerebras, Groq, Fireworks, Together, etc. +Included providers: OpenAI, Anthropic, Google (Gemini), Google Vertex Gemini and Anthropic, Amazon Bedrock, Azure OpenAI, Cloudflare AI Gateway, Cloudflare Workers AI, GitHub Copilot, OpenRouter, xAI, plus generic OpenAI-compatible Chat and Responses entrypoints and an Anthropic Messages-compatible entrypoint. + +### Package-like entrypoints + +Native catalog integrations load provider behavior through package-like entrypoints. These are export paths from the same `@opencode-ai/ai` npm package, not independently published packages. Each entrypoint exports the same `model(modelID, settings)` contract, and `settings` contains serializable provider configuration plus common `headers`, `body`, and `limits` overlays. + +```ts +import { model } from "@opencode-ai/ai/providers/openai/responses" + +const selected = model("gpt-5", { + apiKey: process.env.OPENAI_API_KEY, + transport: "websocket", + headers: { "x-application": "opencode" }, + limits: { context: 200_000, output: 64_000 }, +}) +``` + +OpenAI Chat and OpenAI Responses are separate semantic entrypoints: + +- `@opencode-ai/ai/providers/openai/chat` +- `@opencode-ai/ai/providers/openai/responses` +- `@opencode-ai/ai/providers/openai-compatible/responses` +- `@opencode-ai/ai/providers/anthropic-compatible` +- `@opencode-ai/ai/providers/google-vertex/gemini` +- `@opencode-ai/ai/providers/google-vertex/chat` +- `@opencode-ai/ai/providers/google-vertex/responses` +- `@opencode-ai/ai/providers/google-vertex/messages` + +Responses HTTP versus WebSocket is a scoped `transport` setting on the OpenAI Responses entrypoint, not another entrypoint. Azure follows the same Chat/Responses split at `providers/azure/chat` and `providers/azure/responses`. Generic OpenAI-compatible Chat remains at `providers/openai-compatible`; compatible Responses is separate at `providers/openai-compatible/responses`. Generic Anthropic Messages-compatible providers use `providers/anthropic-compatible`, which the named Anthropic provider composes. Google Gemini and Amazon Bedrock expose their single native API through their existing provider paths. + +Vertex Gemini, Vertex Chat, Vertex Responses, and Vertex Messages are separate API entrypoints. All accept `project`, `location`, and an optional `accessToken`; when no explicit token or auth override is supplied they lazily use Google Application Default Credentials. Vertex Gemini instead selects express mode when `apiKey` or `GOOGLE_VERTEX_API_KEY` is present. Vertex Chat targets MaaS models through the OpenAI-compatible Chat Completions endpoint, while Vertex Responses targets Grok models and defaults `store` to `false` as required by Vertex. `providers/google-vertex` remains the default alias for `providers/google-vertex/gemini`. + +Tuned Vertex Gemini deployments use model ids shaped like `endpoints/1234567890` and require OAuth or ADC; Vertex express-mode API keys support publisher models only. + +```ts +import { model } from "@opencode-ai/ai/providers/google-vertex/gemini" + +model("gemini-3.5-flash", { project: "my-project", location: "global" }) +``` + +```ts +import { model } from "@opencode-ai/ai/providers/google-vertex/chat" + +model("deepseek-ai/deepseek-v3.2-maas", { project: "my-project", location: "global" }) +``` + +```ts +import { model } from "@opencode-ai/ai/providers/google-vertex/responses" + +model("xai/grok-4.20-reasoning", { project: "my-project", location: "global" }) +``` + +```ts +import { model } from "@opencode-ai/ai/providers/google-vertex/messages" + +model("claude-sonnet-4-6", { project: "my-project", location: "global" }) +``` + +Provider facades such as `OpenAI.configure(...).responses(...)` remain the direct application API. Package-like entrypoints are the self-similar loading contract used when a catalog selects behavior by export path. + +Other provider exports listed above remain direct facades until they explicitly implement the package-like contract. Exporting a provider facade does not implicitly make it a catalog-loadable provider package. ## Provider options & HTTP overlays @@ -127,5 +187,6 @@ This package is built on Effect. Public methods return `Effect` or `Stream`; pro ## See also - `AGENTS.md` — architecture, route construction, contributor guide +- `STATUS.md` — native provider parity status and AI SDK migration gaps - `example/tutorial.ts` — runnable end-to-end walkthrough - `test/provider/*.test.ts` — fixture-first protocol tests; `*.recorded.test.ts` files cover live cassettes diff --git a/packages/ai/STATUS.md b/packages/ai/STATUS.md new file mode 100644 index 0000000000..24c98a426a --- /dev/null +++ b/packages/ai/STATUS.md @@ -0,0 +1,108 @@ +# LLM Provider Parity Status + +Last reviewed: 2026-07-17 + +This file tracks the gap between the native `@opencode-ai/ai` package and the AI SDK provider packages that opencode still depends on for many catalog/runtime paths. + +## Existing Status Sources + +| File | What it tracks | Limitation | +| ----------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------- | +| `packages/ai/DESIGN.md` | Future clean-break API proposal for `@opencode-ai/ai`. | Not a provider parity tracker. | +| `packages/ai/example/call-sites.md` | Route/value/provider-facade migration checklist and call-site sketches. | Architecture migration only; not AI SDK package parity. | + +## Current Implementation Snapshot + +| Native slice | Source | Current state | Main gaps | +| ---------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| OpenAI Chat | `src/protocols/openai-chat.ts`, `src/providers/openai.ts` | Usable. Streams text, reasoning deltas, tool calls, usage, images, and common generation controls. | No typed structured-output / `response_format` path. Limited typed OpenAI option surface compared with SDK escape hatches. | +| OpenAI Responses HTTP | `src/protocols/openai-responses.ts`, `src/providers/openai.ts` | Usable. Supports hosted-tool event surfacing, reasoning replay metadata, GPT-5 defaults, and cache usage. | No explicit `previous_response_id` path. Typed options cover only a subset of Responses fields. Structured output is still mostly synthetic-tool based. | +| OpenAI Responses WebSocket | `src/protocols/openai-responses.ts`, `src/route/transport/websocket.ts` | Present as `OpenAI.responsesWebSocket(...)`. | Runner/catalog support explicitly must not downgrade WebSocket routes; broader runtime selection is not complete. | +| OpenAI-compatible Chat | `src/protocols/openai-compatible-chat.ts`, `src/providers/openai-compatible.ts` | Usable for generic Chat and several profiles: Baseten, Cerebras, DeepInfra, DeepSeek, Fireworks, Groq, TogetherAI. | Family quirks are mostly endpoint defaults, not full typed behavior. | +| OpenAI-compatible Responses | `src/protocols/openai-compatible-responses.ts`, `src/providers/openai-compatible-responses.ts` | Usable for deployments that implement the OpenAI Responses wire protocol. | No named family profiles or recorded deployment coverage yet. | +| Anthropic-compatible Messages | `src/protocols/anthropic-messages.ts`, `src/providers/anthropic-compatible.ts` | Usable for deployments that implement the Anthropic Messages wire protocol. Named Anthropic composes this base; MiniMax M3 has recorded text and tool-loop coverage. | No named compatible family profiles yet. | +| Anthropic Messages | `src/protocols/anthropic-messages.ts`, `src/providers/anthropic.ts` | Usable. Supports tools, thinking, cache control, images, server-hosted tool events, and usage. | Provider option surface is small. Beta/header handling, metadata, and newer Messages fields need a typed parity pass. | +| Gemini Developer API | `src/protocols/gemini.ts`, `src/providers/google.ts` | Usable for Google API key flow. Supports text, images, tools, thinking signatures, and cache usage. | This is not Vertex. Typed provider options are narrow; many Gemini request fields currently require raw `http.body` overlays. | +| Vertex Gemini | `src/protocols/gemini.ts`, `src/providers/google-vertex.ts` | Usable through API-key express mode, explicit OAuth tokens, or ADC with project/location endpoint derivation, including tuned `endpoints/...` deployments. | Core runner/catalog mapping and recorded provider coverage are missing. | +| Vertex Chat | `src/protocols/openai-chat.ts`, `src/providers/google-vertex-chat.ts` | Usable for MaaS models through OpenAI-compatible Chat Completions with explicit OAuth tokens or ADC and project/location endpoint derivation. | Core runner/catalog mapping and recorded provider coverage are missing; MaaS family-specific request parity needs review. | +| Vertex Responses | `src/protocols/openai-responses.ts`, `src/providers/google-vertex-responses.ts` | Usable for Grok models through OpenAI-compatible Responses with explicit OAuth tokens or ADC, project/location endpoint derivation, and storage disabled by default. | Core runner/catalog mapping and recorded provider coverage are missing; stateful continuation is not supported by Vertex. | +| Vertex Messages | `src/protocols/anthropic-messages.ts`, `src/providers/google-vertex-messages.ts` | Usable through explicit OAuth tokens or ADC, including global, regional, and `eu`/`us` multi-region endpoints. | Core runner/catalog mapping and recorded provider coverage are missing; Vertex-specific hosted-tool parity needs review. | +| Bedrock Converse | `src/protocols/bedrock-converse.ts`, `src/providers/amazon-bedrock.ts` | Partial but real. Supports AWS event-stream framing, SigV4 with supplied credentials, bearer auth, tools, reasoning signatures, media, cache points, and recorded tests. | Native facade does not mirror the AI SDK plugin's default AWS credential chain/profile behavior. Runner/catalog mapping is missing. Guardrails, inference profiles, region-specific model ID fixes, and model-specific request fields need a parity pass. | +| Azure OpenAI | `src/providers/azure.ts` using OpenAI Chat/Responses protocols | Partial. Supports resource/base URL setup, API key auth, API version query, Chat, and Responses selectors. | Core runner does not map `@ai-sdk/azure` to this native facade. AAD/token auth and Azure-specific endpoint variants need review. | +| Cloudflare AI Gateway / Workers AI | `src/providers/cloudflare.ts` | Present via OpenAI-compatible Chat routes. | Useful but not part of the critical AI SDK replacement set yet. Needs per-product recorded coverage before relying on it broadly. | +| OpenRouter | `src/providers/openrouter.ts` | Present with OpenRouter-specific usage/reasoning/prompt-cache options over Chat. | Responses-style OpenRouter support is absent. | +| xAI | `src/providers/xai.ts` | Present with Responses and Chat selectors. | Needs package-parity review against the AI SDK xAI provider. | +| GitHub Copilot | `src/providers/github-copilot.ts` | Present as explicit-base-URL OpenAI Chat/Responses facade. | Runtime/catalog integration remains specialized and should stay separate from public OpenAI-compatible defaults. | + +## V2 Runner Status + +`packages/core/src/session/runner/model.ts` currently resolves only this native subset from catalog `aisdk` metadata: + +| Catalog API | Native route used today | +| --------------------------------------------------- | ---------------------------- | +| `aisdk:@ai-sdk/openai` | `OpenAIResponses.route` | +| `aisdk:@ai-sdk/anthropic` | `AnthropicMessages.route` | +| `aisdk:@ai-sdk/openai-compatible` with explicit URL | `OpenAICompatibleChat.route` | + +Other `aisdk:` packages, including Google Vertex, Azure, and Bedrock, currently fall back through the AI SDK loader in the production runner. The dependency-free resolver seam rejects them with `SessionRunnerModel.UnsupportedPackageError`; they are not native route mappings yet. + +## AI SDK Package Parity Matrix + +| AI SDK package | Intended native target | Status | Biggest gaps | +| --------------------------------- | -------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `@ai-sdk/openai` | `OpenAI.chat`, `OpenAI.responses`, `OpenAI.responsesWebSocket` | Partial / usable | Add complete typed option coverage, structured output strategy, explicit Responses continuation support, and runner route selection between Chat/Responses/WebSocket. | +| `@ai-sdk/openai-compatible` | Generic OpenAI-compatible Chat and Responses | Partial / usable | Decide per-family namespace/profile behavior and runner API selection for providers that support Responses versus Chat only. | +| `@ai-sdk/anthropic` | `AnthropicMessages` | Partial / usable | Finish Messages API parity for headers/betas/metadata/newer fields and document hosted-tool continuation expectations. | +| `@ai-sdk/google` | Gemini Developer API | Partial / usable | Add typed options for safety, response schema/modalities, cached content, grounding/search/code execution, and non-text output modes where supported. | +| `@ai-sdk/google-vertex` | Vertex Gemini namespace/facade | Partial / usable | Add runner/catalog mapping, recorded coverage, and broader provider-option parity. | +| `@ai-sdk/google-vertex/anthropic` | Anthropic Messages over Vertex namespace/facade | Partial / usable | Add runner/catalog mapping, recorded coverage, and Vertex-specific hosted-tool parity. | +| `@ai-sdk/google-vertex/maas` | Vertex Chat | Partial / usable | Add runner/catalog mapping, recorded coverage, and MaaS family-specific request parity. | +| `@ai-sdk/google-vertex/xai` | Vertex Chat / Responses | Partial / usable | Decide Chat/Responses selection for catalog models, add runner mapping and recorded coverage, and review xAI-specific request options. | +| `@ai-sdk/azure` | Azure OpenAI Chat/Responses facade | Partial | Map runner/catalog metadata to native Azure, handle resourceName/baseURL/apiVersion variants, add AAD/token auth story, and verify Chat vs Responses deployment selection. | +| `@ai-sdk/amazon-bedrock` | Bedrock Converse | Partial | Add default AWS credential chain/profile support, region/inference-profile model ID handling, provider option parity via `additionalModelRequestFields`, guardrails/performance config, and runner/catalog mapping. | +| `@ai-sdk/amazon-bedrock/mantle` | Bedrock Mantle OpenAI-compatible Chat/Responses namespace | Missing | Decide native Mantle shape, likely separate from Converse because it uses OpenAI-compatible Chat/Responses semantics over Bedrock. Add package mapping and tests. | + +## Highest-Risk Gaps + +1. Runner support is narrower than the LLM package. The package has native provider facades for Google, Azure, and Bedrock, but the V2 Session runner only maps OpenAI, Anthropic, and explicit OpenAI-compatible Chat from `aisdk` catalog metadata. +2. OpenAI-compatible Responses is available as a separate package entrypoint, but the V2 runner still maps `@ai-sdk/openai-compatible` to Chat only. Catalog selection must become API-aware before Responses deployments can use it. +3. Bedrock native auth is not AI SDK parity. The AI SDK plugin uses the default AWS provider chain, profile, container credentials, and Bedrock bearer token env behavior. Native Bedrock currently expects explicit credentials or bearer auth on the facade. +4. Vertex Gemini, Vertex Chat, Vertex Responses, and Vertex Messages now have native package entrypoints, but the core runner does not map catalog metadata to them yet and recorded provider coverage is still missing. +5. Azure is only a provider facade, not a full runtime replacement. Native Azure exists, but the catalog runner does not select it, and token auth/resource variants need review. +6. Provider option typing is uneven. OpenAI, Anthropic, Gemini, Bedrock, and OpenRouter each expose a small typed subset plus raw HTTP overlays; this is useful but not equivalent to AI SDK provider option coverage. +7. Structured output is not provider-native yet. `LLM.generateObject` still uses a synthetic tool strategy, while the future design expects native structured output where reliable and tool fallback where needed. +8. Package/namespace boundaries for the current native loading set are explicit in docs and exports. Other exported provider facades are not catalog package entrypoints until they implement the contract. Vertex xAI still needs catalog API selection; the missing native boundary is Bedrock Mantle. +9. Recorded coverage is uneven. OpenAI, Anthropic, Gemini, Bedrock Converse, Cloudflare, OpenRouter, and several OpenAI-compatible Chat providers have cassettes. Azure, Vertex, and Mantle need first-class recorded scenarios before switching defaults. + +## Native Namespace Shape + +These are implementation/API slices, not separate npm packages. + +| API slice | Package-like entrypoint | Purpose | +| ----------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------- | +| OpenAI Chat | `@opencode-ai/ai/providers/openai/chat` | OpenAI `/chat/completions` semantics. | +| OpenAI Responses | `@opencode-ai/ai/providers/openai/responses` | OpenAI `/responses` semantics with HTTP/WebSocket selected through settings. | +| OpenAI-compatible Chat | `@opencode-ai/ai/providers/openai-compatible` | Generic OpenAI-compatible `/chat/completions`. | +| OpenAI-compatible Responses | `@opencode-ai/ai/providers/openai-compatible/responses` | Generic OpenAI-compatible `/responses`. | +| Anthropic-compatible Messages | `@opencode-ai/ai/providers/anthropic-compatible` | Generic Anthropic-compatible `/messages`. | +| Anthropic Messages | `@opencode-ai/ai/providers/anthropic` | Anthropic Messages API. | +| Gemini Developer API | `@opencode-ai/ai/providers/google` | Google AI Studio Gemini API. | +| Vertex Gemini | `@opencode-ai/ai/providers/google-vertex/gemini` | Vertex Gemini API; `providers/google-vertex` is the default alias. | +| Vertex Chat | `@opencode-ai/ai/providers/google-vertex/chat` | Vertex OpenAI-compatible Chat Completions for MaaS models. | +| Vertex Responses | `@opencode-ai/ai/providers/google-vertex/responses` | Vertex OpenAI-compatible Responses for Grok models. | +| Vertex Messages | `@opencode-ai/ai/providers/google-vertex/messages` | Vertex-hosted Anthropic Messages API. | +| Bedrock Converse | `@opencode-ai/ai/providers/amazon-bedrock` | AWS Bedrock Converse API. | +| Bedrock Mantle | Missing | AWS Bedrock Mantle OpenAI-compatible APIs. | +| Azure OpenAI Chat | `@opencode-ai/ai/providers/azure/chat` | Azure specialization of OpenAI Chat. | +| Azure OpenAI Responses | `@opencode-ai/ai/providers/azure/responses` | Azure specialization of OpenAI Responses. | + +## Suggested Next Work Slices + +1. Add native runner/catalog mappings for `@ai-sdk/azure`, `@ai-sdk/google`, and `@ai-sdk/amazon-bedrock` where the existing native facades are already close. +2. Add API-aware runner/catalog selection between OpenAI-compatible Chat and Responses. +3. Bring Bedrock native auth/config to AI SDK parity: region, profile, default AWS credential chain, bearer token env, endpoint override, and cross-region inference profile handling. +4. Add runner/catalog mappings and recorded scenarios for the native Vertex Gemini, Chat, Responses, and Messages entrypoints. +5. Decide Chat/Responses selection for `@ai-sdk/google-vertex/xai` catalog models. +6. Add Bedrock Mantle as a separate OpenAI-compatible Bedrock namespace after deciding whether it uses Chat, Responses, or both by model. +7. Expand typed provider options from the existing V1 lowerer knowledge in `packages/core/src/v1/config/provider-options.ts` before adding more raw overlay examples. +8. Add recorded provider tests for Azure, Vertex Gemini, Vertex Chat, Vertex Responses, Vertex Messages, Bedrock credential-chain behavior, and Mantle before making native runtime the default for those packages. diff --git a/packages/llm/example/call-sites.md b/packages/ai/example/call-sites.md similarity index 91% rename from packages/llm/example/call-sites.md rename to packages/ai/example/call-sites.md index 093f74e51d..0b33d28c48 100644 --- a/packages/llm/example/call-sites.md +++ b/packages/ai/example/call-sites.md @@ -7,7 +7,7 @@ values directly. ## Conversation Summary Kit and Aidan want provider-specific LLM behavior to move out of opencode's AI -SDK transform path and into `packages/llm` where possible. The goal is not a big +SDK transform path and into `packages/ai` where possible. The goal is not a big generic transform layer; the goal is small composable route definitions backed by recorded golden tests. @@ -342,14 +342,51 @@ const response = ) ``` -HTTP versus WebSocket is represented as named route selectors, not as model or -request overrides. Same protocol, different transport, different route: +For direct provider-facade calls, HTTP versus WebSocket is represented as named +route selectors, not as model or request overrides. Same protocol, different +transport, different route: ```ts OpenAI.responses("gpt-4o") OpenAI.responsesWebSocket("gpt-4o") ``` +The package-like OpenAI Responses entrypoint instead keeps transport scoped to +Responses settings while preserving the same `model(...)` contract: + +```ts +import { model } from "@opencode-ai/ai/providers/openai/responses" + +model("gpt-4o", { apiKey, transport: "websocket" }) +``` + +Vertex keeps Gemini, Chat, Responses, and Messages as separate package-like entrypoints, +while sharing project/location resolution and ADC authentication internally: + +```ts +import { model } from "@opencode-ai/ai/providers/google-vertex/gemini" + +model("gemini-3.5-flash", { project, location: "global" }) +``` + +```ts +import { model } from "@opencode-ai/ai/providers/google-vertex/chat" + +model("deepseek-ai/deepseek-v3.2-maas", { project, location: "global" }) +``` + +```ts +import { model } from "@opencode-ai/ai/providers/google-vertex/responses" + +model("xai/grok-4.20-reasoning", { project, location: "global" }) +``` + +```ts +import { model } from "@opencode-ai/ai/providers/google-vertex/messages" + +model("claude-sonnet-4-6", { project, location: "global" }) +``` + The client should not require a different public layer just because a selected route uses WebSocket. Use one `LLMClient.layer` with HTTP and WebSocket runtime capabilities available; routes that do not need WebSocket simply never touch it. @@ -468,10 +505,10 @@ const model = ``` That boundary can branch on durable config/catalog metadata and call typed -provider APIs directly. Transport selection belongs there too: map metadata like -`endpoint.websocket` to `OpenAI.responsesWebSocket(apiModelID)`; otherwise use -the normal `OpenAI.responses(apiModelID)` route. The client runtime only executes -the route carried by the model. +provider APIs directly. A direct provider-facade boundary maps metadata like +`endpoint.websocket` to `OpenAI.responsesWebSocket(apiModelID)`. A package-loading +boundary passes `transport: "websocket"` to the OpenAI Responses entrypoint. +The client runtime only executes the route carried by the resulting model. ## Competitive Shape @@ -507,8 +544,9 @@ App boundary = explicit durable-config -> typed-provider call id. - No `model(id, overrides)` escape hatch. Model selection takes the model id; endpoint/auth/deployment customization happens by configuring the route first. -- No transport override on model/request. HTTP SSE versus WebSocket is a named - route selector such as `responses` versus `responsesWebSocket`. +- No transport override on an executable model or request. Direct provider + facades use `responses` versus `responsesWebSocket`; the package-like Responses + entrypoint maps its scoped `transport` setting before constructing the model. - No separate public `LLMClient.layerWithWebSocket`. The runtime should expose one client layer with the available transport capabilities. - No executable `ModelRef`. The executable handle is `Model`; durable model diff --git a/packages/llm/example/tutorial.ts b/packages/ai/example/tutorial.ts similarity index 97% rename from packages/llm/example/tutorial.ts rename to packages/ai/example/tutorial.ts index fddc345966..b109ef6230 100644 --- a/packages/llm/example/tutorial.ts +++ b/packages/ai/example/tutorial.ts @@ -1,12 +1,12 @@ import { Config, Effect, Formatter, Layer, Schema, Stream } from "effect" -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" +import { LLM, LLMClient, Message, ProviderID, Tool, ToolRuntime } from "@opencode-ai/ai" +import { Route, Auth, Endpoint, Framing, Protocol, RequestExecutor, WebSocketExecutor } from "@opencode-ai/ai/route" +import { OpenAI } from "@opencode-ai/ai/providers" /** * A runnable walkthrough of the LLM package use-site API. * - * Run from `packages/llm` with an OpenAI key in the environment: + * Run from `packages/ai` with an OpenAI key in the environment: * * OPENAI_API_KEY=... bun example/tutorial.ts * diff --git a/packages/ai/package.json b/packages/ai/package.json new file mode 100644 index 0000000000..36c3e22673 --- /dev/null +++ b/packages/ai/package.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "version": "1.17.20", + "name": "@opencode-ai/ai", + "type": "module", + "license": "MIT", + "scripts": { + "setup:recording-env": "bun run script/setup-recording-env.ts", + "test": "bun test --timeout 30000 --only-failures", + "typecheck": "tsgo --noEmit", + "build": "tsc -p tsconfig.build.json" + }, + "files": [ + "dist" + ], + "exports": { + ".": "./src/index.ts", + "./*": "./src/*.ts" + }, + "devDependencies": { + "@clack/prompts": "1.0.0-alpha.1", + "@effect/platform-node": "catalog:", + "@opencode-ai/http-recorder": "workspace:*", + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:" + }, + "dependencies": { + "@smithy/eventstream-codec": "4.2.14", + "@smithy/util-utf8": "4.2.2", + "@opencode-ai/schema": "workspace:*", + "aws4fetch": "1.0.20", + "effect": "catalog:", + "google-auth-library": "10.5.0" + } +} diff --git a/packages/ai/script/publish.ts b/packages/ai/script/publish.ts new file mode 100644 index 0000000000..f03dd084b0 --- /dev/null +++ b/packages/ai/script/publish.ts @@ -0,0 +1,38 @@ +#!/usr/bin/env bun +import { Script } from "@opencode-ai/script" +import { $ } from "bun" +import { fileURLToPath } from "url" + +const dir = fileURLToPath(new URL("..", import.meta.url)) +process.chdir(dir) + +async function published(name: string, version: string) { + return (await $`npm view ${name}@${version} version`.nothrow()).exitCode === 0 +} + +await $`bun run build` +const originalText = await Bun.file("package.json").text() +const pkg = JSON.parse(originalText) as { + name: string + version: string + exports: Record +} +if (await published(pkg.name, pkg.version)) { + console.log(`already published ${pkg.name}@${pkg.version}`) +} else { + for (const [key, value] of Object.entries(pkg.exports)) { + const file = value.replace("./src/", "./dist/").replace(".ts", "") + // @ts-ignore + pkg.exports[key] = { + import: file + ".js", + types: file + ".d.ts", + } + } + await Bun.write("package.json", JSON.stringify(pkg, null, 2)) + try { + await $`bun pm pack` + await $`npm publish *.tgz --tag ${Script.channel} --access public` + } finally { + await Bun.write("package.json", originalText) + } +} diff --git a/packages/llm/script/recording-cost-report.ts b/packages/ai/script/recording-cost-report.ts similarity index 99% rename from packages/llm/script/recording-cost-report.ts rename to packages/ai/script/recording-cost-report.ts index 1f42dc5932..5b08e72d5c 100644 --- a/packages/llm/script/recording-cost-report.ts +++ b/packages/ai/script/recording-cost-report.ts @@ -2,7 +2,7 @@ import * as fs from "node:fs/promises" import * as path from "node:path" const RECORDINGS_DIR = path.resolve(import.meta.dir, "..", "test", "fixtures", "recordings") -const MODELS_DEV_URL = "https://models.opencode.ai/api.json" +const MODELS_DEV_URL = "https://models.dev/api.json" type JsonRecord = Record diff --git a/packages/llm/script/setup-recording-env.ts b/packages/ai/script/setup-recording-env.ts similarity index 97% rename from packages/llm/script/setup-recording-env.ts rename to packages/ai/script/setup-recording-env.ts index d32769b3ce..281c9b8a8d 100644 --- a/packages/llm/script/setup-recording-env.ts +++ b/packages/ai/script/setup-recording-env.ts @@ -161,6 +161,18 @@ const PROVIDERS: ReadonlyArray = [ vars: [{ name: "TOGETHER_AI_API_KEY" }], validate: (env) => validateBearer("https://api.together.xyz/v1/models", Redacted.make(env.TOGETHER_AI_API_KEY)), }, + { + id: "minimax", + label: "MiniMax", + tier: "compatible", + note: "Anthropic-compatible Messages text/tool recorded tests", + vars: [{ name: "MINIMAX_API_KEY" }], + validate: (env) => + HttpClientRequest.get("https://api.minimax.io/anthropic/v1/models").pipe( + HttpClientRequest.setHeader("x-api-key", Redacted.value(Redacted.make(env.MINIMAX_API_KEY))), + executeRequest, + ), + }, { id: "mistral", label: "Mistral", diff --git a/packages/llm/src/cache-policy.ts b/packages/ai/src/cache-policy.ts similarity index 100% rename from packages/llm/src/cache-policy.ts rename to packages/ai/src/cache-policy.ts diff --git a/packages/llm/src/index.ts b/packages/ai/src/index.ts similarity index 82% rename from packages/llm/src/index.ts rename to packages/ai/src/index.ts index 735520ff77..273861b26f 100644 --- a/packages/llm/src/index.ts +++ b/packages/ai/src/index.ts @@ -1,6 +1,7 @@ export { LLMClient } from "./route/client" export { Auth } from "./route/auth" export { Provider } from "./provider" +export { ProviderPackage } from "./provider-package" export { isContextOverflow, isContextOverflowFailure } from "./provider-error" export type { RouteModelInput, @@ -17,7 +18,7 @@ export type { AnyTool, ExecutableTool, ExecutableTools, - Tool as ToolShape, + Definition as ToolShape, ToolExecute, ToolExecuteContext, ToolModelOutputInput, @@ -31,3 +32,4 @@ export type { ModelFactory as ProviderModelFactory, ModelOptions as ProviderModelOptions, } from "./provider" +export type { Definition as ProviderPackageDefinition, Settings as ProviderPackageSettings } from "./provider-package" diff --git a/packages/llm/src/llm.ts b/packages/ai/src/llm.ts similarity index 100% rename from packages/llm/src/llm.ts rename to packages/ai/src/llm.ts diff --git a/packages/ai/src/protocols.ts b/packages/ai/src/protocols.ts new file mode 100644 index 0000000000..96aa6ee43d --- /dev/null +++ b/packages/ai/src/protocols.ts @@ -0,0 +1 @@ +export * from "./protocols/index" diff --git a/packages/llm/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts similarity index 92% rename from packages/llm/src/protocols/anthropic-messages.ts rename to packages/ai/src/protocols/anthropic-messages.ts index 1c0dcd32a4..3abc8e9fb9 100644 --- a/packages/llm/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -5,6 +5,7 @@ import { Endpoint } from "../route/endpoint" import { Framing } from "../route/framing" import { Protocol } from "../route/protocol" import { + LLMError, LLMEvent, Usage, type CacheHint, @@ -19,7 +20,7 @@ import { type ToolResultPart, } from "../schema" import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" -import { isContextOverflow } from "../provider-error" +import { classifyProviderFailure } from "../provider-error" import * as Cache from "./utils/cache" import { Lifecycle } from "./utils/lifecycle" import { ToolSchemaProjection } from "./utils/tool-schema" @@ -148,9 +149,22 @@ const AnthropicToolChoice = Schema.Union([ Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }), ]) -const AnthropicThinking = Schema.Struct({ - type: Schema.tag("enabled"), - budget_tokens: Schema.Number, +const AnthropicThinking = Schema.Union([ + Schema.Struct({ + type: Schema.tag("enabled"), + budget_tokens: Schema.Number, + }), + Schema.Struct({ + type: Schema.tag("adaptive"), + display: Schema.optional(Schema.Literals(["summarized", "omitted"])), + }), + Schema.Struct({ + type: Schema.tag("disabled"), + }), +]) + +const AnthropicOutputConfig = Schema.Struct({ + effort: Schema.optional(Schema.String), }) const AnthropicBodyFields = { @@ -166,8 +180,9 @@ const AnthropicBodyFields = { top_k: Schema.optional(Schema.Number), stop_sequences: optionalArray(Schema.String), thinking: Schema.optional(AnthropicThinking), + output_config: Schema.optional(AnthropicOutputConfig), } -const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields) +export const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields) export type AnthropicMessagesBody = Schema.Schema.Type const AnthropicUsage = Schema.Struct({ @@ -301,7 +316,21 @@ const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult const wireType = serverToolResultType(part.name) if (!wireType) return yield* invalid(`Anthropic Messages does not know how to round-trip server tool result for ${part.name}`) - return { type: wireType, tool_use_id: part.id, content: part.result.value } satisfies AnthropicServerToolResultBlock + const errorType = `${wireType}_error` + const syntheticErrorCode = + ProviderShared.isRecord(part.result.value) && + ProviderShared.isRecord(part.result.value.error) && + part.result.value.error.type === "provider.invalid-output" + ? "invalid_tool_input" + : "unavailable" + const content = + part.result.type !== "error" || + (ProviderShared.isRecord(part.result.value) && + part.result.value.type === errorType && + typeof part.result.value.error_code === "string") + ? part.result.value + : { type: errorType, error_code: syntheticErrorCode } + return { type: wireType, tool_use_id: part.id, content } satisfies AnthropicServerToolResultBlock }) const lowerImage = Effect.fn("AnthropicMessages.lowerImage")(function* (part: MediaPart) { @@ -492,7 +521,18 @@ const anthropicOptions = (request: LLMRequest) => request.providerOptions?.anthr const lowerThinking = Effect.fn("AnthropicMessages.lowerThinking")(function* (request: LLMRequest) { const thinking = anthropicOptions(request)?.thinking - if (!ProviderShared.isRecord(thinking) || thinking.type !== "enabled") return undefined + if (!ProviderShared.isRecord(thinking)) return undefined + if (thinking.type === "adaptive") { + const display = + thinking.display === "summarized" + ? ("summarized" as const) + : thinking.display === "omitted" + ? ("omitted" as const) + : undefined + return { type: "adaptive" as const, ...(display === undefined ? {} : { display }) } + } + if (thinking.type === "disabled") return { type: "disabled" as const } + if (thinking.type !== "enabled") return undefined const budget = typeof thinking.budgetTokens === "number" ? thinking.budgetTokens @@ -503,6 +543,11 @@ const lowerThinking = Effect.fn("AnthropicMessages.lowerThinking")(function* (re return { type: "enabled" as const, budget_tokens: budget } }) +const outputConfig = (request: LLMRequest) => { + const effort = anthropicOptions(request)?.effort + return typeof effort === "string" ? { effort } : undefined +} + const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) { const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined const generation = request.generation @@ -549,6 +594,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques top_k: generation?.topK, stop_sequences: generation?.stop, thinking: yield* lowerThinking(request), + output_config: outputConfig(request), } }) @@ -671,7 +717,14 @@ const onContentBlockStart = (state: ParserState, event: AnthropicEvent): StepRes providerExecuted: block.type === "server_tool_use", }), }, - [...events, LLMEvent.toolInputStart({ id: block.id ?? String(event.index), name: block.name ?? "" })], + [ + ...events, + LLMEvent.toolInputStart({ + id: block.id ?? String(event.index), + name: block.name ?? "", + providerExecuted: block.type === "server_tool_use" ? true : undefined, + }), + ], ] } @@ -801,15 +854,12 @@ const providerErrorMessage = (event: AnthropicEvent): string => { return message || type || "Anthropic Messages stream error" } -const onError = (state: ParserState, event: AnthropicEvent): StepResult => [ - state, - [ - LLMEvent.providerError({ - message: providerErrorMessage(event), - classification: isContextOverflow(event.error?.message ?? "") ? "context-overflow" : undefined, - }), - ], -] +const onError = (event: AnthropicEvent) => + new LLMError({ + module: ADAPTER, + method: "stream", + reason: classifyProviderFailure({ message: providerErrorMessage(event), code: event.error?.type }), + }) const step = (state: ParserState, event: AnthropicEvent) => { if (event.type === "message_start") return Effect.succeed(onMessageStart(state, event)) @@ -817,7 +867,7 @@ const step = (state: ParserState, event: AnthropicEvent) => { if (event.type === "content_block_delta") return onContentBlockDelta(state, event) if (event.type === "content_block_stop") return onContentBlockStop(state, event) if (event.type === "message_delta") return Effect.succeed(onMessageDelta(state, event)) - if (event.type === "error") return Effect.succeed(onError(state, event)) + if (event.type === "error") return onError(event) return Effect.succeed([state, NO_EVENTS]) } @@ -845,6 +895,7 @@ export const protocol = Protocol.make({ export const route = Route.make({ id: ADAPTER, provider: "anthropic", + providerMetadataKey: "anthropic", protocol, endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }), auth: Auth.none, diff --git a/packages/llm/src/protocols/bedrock-converse.ts b/packages/ai/src/protocols/bedrock-converse.ts similarity index 95% rename from packages/llm/src/protocols/bedrock-converse.ts rename to packages/ai/src/protocols/bedrock-converse.ts index c447a1a39d..4ac3eeaf68 100644 --- a/packages/llm/src/protocols/bedrock-converse.ts +++ b/packages/ai/src/protocols/bedrock-converse.ts @@ -3,6 +3,7 @@ import { Route } from "../route/client" import { Endpoint } from "../route/endpoint" import { Protocol } from "../route/protocol" import { + LLMError, LLMEvent, Usage, type CacheHint, @@ -17,7 +18,7 @@ import { type ToolResultPart, } from "../schema" import { BedrockEventStream } from "./bedrock-event-stream" -import { isContextOverflow } from "../provider-error" +import { classifyProviderFailure } from "../provider-error" import { JsonObject, optionalArray, ProviderShared } from "./shared" import { BedrockAuth } from "./utils/bedrock-auth" import { BedrockCache } from "./utils/bedrock-cache" @@ -586,28 +587,24 @@ const step = (state: ParserState, event: BedrockEvent) => return [{ ...state, pendingFinish: { reason: state.pendingFinish?.reason ?? "stop", usage } }, []] as const } - if (event.internalServerException || event.modelStreamErrorException || event.serviceUnavailableException) { - const message = - event.internalServerException?.message ?? - event.modelStreamErrorException?.message ?? - event.serviceUnavailableException?.message ?? - "Bedrock Converse stream error" - return [state, [LLMEvent.providerError({ message, retryable: true })]] as const - } - - if (event.validationException || event.throttlingException) { - const message = - event.validationException?.message ?? event.throttlingException?.message ?? "Bedrock Converse error" - return [ - state, - [ - LLMEvent.providerError({ - message, - classification: event.validationException && isContextOverflow(message) ? "context-overflow" : undefined, - retryable: event.throttlingException !== undefined, - }), - ], + const exception = ( + [ + ["internalServerException", event.internalServerException], + ["modelStreamErrorException", event.modelStreamErrorException], + ["serviceUnavailableException", event.serviceUnavailableException], + ["throttlingException", event.throttlingException], + ["validationException", event.validationException], ] as const + ).find((entry) => entry[1] !== undefined) + if (exception) { + return yield* new LLMError({ + module: ADAPTER, + method: "stream", + reason: classifyProviderFailure({ + message: exception[1]?.message ?? "Bedrock Converse stream error", + code: exception[0], + }), + }) } return [state, []] as const @@ -658,6 +655,7 @@ export const protocol = Protocol.make({ export const route = Route.make({ id: ADAPTER, provider: "bedrock", + providerMetadataKey: "bedrock", protocol, // Bedrock's URL embeds the region in the route endpoint host and the // validated modelId in the path. We read the validated body so the URL diff --git a/packages/llm/src/protocols/bedrock-event-stream.ts b/packages/ai/src/protocols/bedrock-event-stream.ts similarity index 96% rename from packages/llm/src/protocols/bedrock-event-stream.ts rename to packages/ai/src/protocols/bedrock-event-stream.ts index d07d7de475..0312ea7d57 100644 --- a/packages/llm/src/protocols/bedrock-event-stream.ts +++ b/packages/ai/src/protocols/bedrock-event-stream.ts @@ -1,7 +1,7 @@ import { EventStreamCodec } from "@smithy/eventstream-codec" import { fromUtf8, toUtf8 } from "@smithy/util-utf8" import { Effect, Stream } from "effect" -import type { Framing } from "../route/framing" +import { Framing } from "../route/framing" import { ProviderShared } from "./shared" // Bedrock streams responses using the AWS event stream binary protocol — each @@ -79,7 +79,7 @@ const consumeFrames = (route: string) => (state: FrameBufferState, chunk: Uint8A * under its `:event-type` header so the chunk schema can match the JSON * payload directly. */ -export const framing = (route: string): Framing => ({ +export const framing = (route: string): Framing.Definition => ({ id: "aws-event-stream", frame: (bytes) => bytes.pipe(Stream.mapAccumEffect(() => initialFrameBuffer, consumeFrames(route))), }) diff --git a/packages/llm/src/protocols/gemini.ts b/packages/ai/src/protocols/gemini.ts similarity index 95% rename from packages/llm/src/protocols/gemini.ts rename to packages/ai/src/protocols/gemini.ts index c4bb9476a4..458b2e47e7 100644 --- a/packages/llm/src/protocols/gemini.ts +++ b/packages/ai/src/protocols/gemini.ts @@ -441,6 +441,9 @@ const step = (state: ParserState, event: GeminiEvent) => { if ("functionCall" in part) { const input = part.functionCall.args const id = `tool_${nextToolCallId++}` + const providerMetadata = part.thoughtSignature + ? googleMetadata({ thoughtSignature: part.thoughtSignature }) + : undefined lifecycle = Lifecycle.reasoningEnd( lifecycle, events, @@ -448,14 +451,27 @@ const step = (state: ParserState, event: GeminiEvent) => { reasoningSignature ? googleMetadata({ thoughtSignature: reasoningSignature }) : undefined, ) lifecycle = Lifecycle.stepStart(lifecycle, events) + if (typeof input === "string") { + events.push( + LLMEvent.toolInputStart({ id, name: part.functionCall.name, providerMetadata }), + LLMEvent.toolInputEnd({ id, name: part.functionCall.name, input, providerMetadata }), + LLMEvent.toolInputError({ + id, + name: part.functionCall.name, + raw: input, + message: `Invalid JSON input for ${ADAPTER} tool call ${part.functionCall.name}`, + providerMetadata, + }), + ) + hasToolCalls = true + continue + } events.push( LLMEvent.toolCall({ id, name: part.functionCall.name, input, - providerMetadata: part.thoughtSignature - ? googleMetadata({ thoughtSignature: part.thoughtSignature }) - : undefined, + providerMetadata, }), ) hasToolCalls = true @@ -500,6 +516,7 @@ export const protocol = Protocol.make({ export const route = Route.make({ id: ADAPTER, provider: "google", + providerMetadataKey: "google", protocol, // Gemini's path embeds the model id and pins SSE framing at the URL level. endpoint: Endpoint.path(({ request }) => `/models/${request.model.id}:streamGenerateContent?alt=sse`, { diff --git a/packages/llm/src/protocols/index.ts b/packages/ai/src/protocols/index.ts similarity index 80% rename from packages/llm/src/protocols/index.ts rename to packages/ai/src/protocols/index.ts index bd8c8d3d9d..d00d517a09 100644 --- a/packages/llm/src/protocols/index.ts +++ b/packages/ai/src/protocols/index.ts @@ -3,4 +3,5 @@ export * as BedrockConverse from "./bedrock-converse" export * as Gemini from "./gemini" export * as OpenAIChat from "./openai-chat" export * as OpenAICompatibleChat from "./openai-compatible-chat" +export * as OpenAICompatibleResponses from "./openai-compatible-responses" export * as OpenAIResponses from "./openai-responses" diff --git a/packages/llm/src/protocols/openai-chat.ts b/packages/ai/src/protocols/openai-chat.ts similarity index 90% rename from packages/llm/src/protocols/openai-chat.ts rename to packages/ai/src/protocols/openai-chat.ts index 9ac85b07b1..101ac61cf6 100644 --- a/packages/llm/src/protocols/openai-chat.ts +++ b/packages/ai/src/protocols/openai-chat.ts @@ -75,6 +75,8 @@ const OpenAIChatMessage = Schema.Union([ content: Schema.NullOr(Schema.String), tool_calls: optionalArray(OpenAIChatAssistantToolCall), reasoning_content: Schema.optional(Schema.String), + reasoning: Schema.optional(Schema.String), + reasoning_text: Schema.optional(Schema.String), }), Schema.Struct({ role: Schema.Literal("tool"), tool_call_id: Schema.String, content: Schema.String }), ]).pipe(Schema.toTaggedUnion("role")) @@ -145,6 +147,8 @@ type OpenAIChatToolCallDelta = Schema.Schema.Type +export type OpenAIChatEvent = Schema.Schema.Type type OpenAIChatRequestMessage = LLMRequest["messages"][number] -interface ParserState { +export interface ParserState { readonly tools: ToolStream.State readonly toolCallEvents: ReadonlyArray readonly usage?: Usage readonly finishReason?: FinishReason readonly lifecycle: Lifecycle.State + readonly reasoningField?: "reasoning" | "reasoning_content" | "reasoning_text" } -const invalid = ProviderShared.invalidRequest - // ============================================================================= // Request Lowering // ============================================================================= @@ -210,6 +213,12 @@ const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart const openAICompatibleReasoningContent = (native: unknown) => isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined +const reasoningField = (part: ReasoningPart) => { + const field = part.providerMetadata?.openai?.reasoningField + if (field === "reasoning" || field === "reasoning_content" || field === "reasoning_text") return field + return "reasoning_content" +} + const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (message: OpenAIChatRequestMessage) { const content: Array> = [] for (const part of message.content) { @@ -250,14 +259,20 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func continue } } + const text = reasoning.map((part) => part.text).join("") + const field = reasoning[0] ? reasoningField(reasoning[0]) : "reasoning_content" return { role: "assistant" as const, content: content.length === 0 ? null : ProviderShared.joinText(content), tool_calls: toolCalls.length === 0 ? undefined : toolCalls, reasoning_content: - reasoning.length > 0 - ? reasoning.map((part) => part.text).join("") - : openAICompatibleReasoningContent(message.native?.openaiCompatible), + reasoning.length === 0 + ? openAICompatibleReasoningContent(message.native?.openaiCompatible) + : field === "reasoning_content" + ? text + : undefined, + reasoning: reasoning.length > 0 && field === "reasoning" ? text : undefined, + reasoning_text: reasoning.length > 0 && field === "reasoning_text" ? text : undefined, } }) @@ -333,8 +348,6 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request: const lowerOptions = Effect.fn("OpenAIChat.lowerOptions")(function* (request: LLMRequest) { const store = OpenAIOptions.store(request) const reasoningEffort = OpenAIOptions.reasoningEffort(request) - if (reasoningEffort && !OpenAIOptions.isReasoningEffort(reasoningEffort)) - return yield* invalid(`OpenAI Chat does not support reasoning effort ${reasoningEffort}`) return { ...(store !== undefined ? { store } : {}), ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}), @@ -404,6 +417,12 @@ const mapUsage = (usage: OpenAIChatEvent["usage"]): Usage | undefined => { }) } +const reasoningDelta = (delta: Schema.Schema.Type | null | undefined) => { + if (delta?.reasoning_content) return { field: "reasoning_content", text: delta.reasoning_content } as const + if (delta?.reasoning) return { field: "reasoning", text: delta.reasoning } as const + if (delta?.reasoning_text) return { field: "reasoning_text", text: delta.reasoning_text } as const +} + const step = (state: ParserState, event: OpenAIChatEvent) => Effect.gen(function* () { const events: LLMEvent[] = [] @@ -416,8 +435,12 @@ const step = (state: ParserState, event: OpenAIChatEvent) => let lifecycle = state.lifecycle - if (delta?.reasoning_content) - lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", delta.reasoning_content) + const reasoning = reasoningDelta(delta) + const reasoningField = state.reasoningField ?? reasoning?.field + if (reasoning) + lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", reasoning.text, { + openai: { reasoningField: reasoningField ?? reasoning.field }, + }) if (delta?.content) { lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0") @@ -454,6 +477,7 @@ const step = (state: ParserState, event: OpenAIChatEvent) => usage, finishReason, lifecycle, + reasoningField, }, events, ] as const @@ -486,7 +510,12 @@ export const protocol = Protocol.make({ }, stream: { event: Protocol.jsonEvent(OpenAIChatEvent), - initial: () => ({ tools: ToolStream.empty(), toolCallEvents: [], lifecycle: Lifecycle.initial() }), + initial: () => ({ + tools: ToolStream.empty(), + toolCallEvents: [], + lifecycle: Lifecycle.initial(), + reasoningField: undefined, + }), step, onHalt: finishEvents, }, @@ -497,6 +526,7 @@ export const httpTransport = HttpTransport.sseJson.with() export const route = Route.make({ id: ADAPTER, provider: "openai", + providerMetadataKey: "openai", protocol, endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }), auth: Auth.none, diff --git a/packages/llm/src/protocols/openai-compatible-chat.ts b/packages/ai/src/protocols/openai-compatible-chat.ts similarity index 96% rename from packages/llm/src/protocols/openai-compatible-chat.ts rename to packages/ai/src/protocols/openai-compatible-chat.ts index ce3f0a83d7..9ae9a53b43 100644 --- a/packages/llm/src/protocols/openai-compatible-chat.ts +++ b/packages/ai/src/protocols/openai-compatible-chat.ts @@ -16,6 +16,7 @@ export type OpenAICompatibleChatModelInput = RouteRoutedModelInput */ export const route = Route.make({ id: ADAPTER, + providerMetadataKey: "openai", protocol: OpenAIChat.protocol, endpoint: Endpoint.path("/chat/completions"), framing: Framing.sse, diff --git a/packages/ai/src/protocols/openai-compatible-responses.ts b/packages/ai/src/protocols/openai-compatible-responses.ts new file mode 100644 index 0000000000..2c56aadafa --- /dev/null +++ b/packages/ai/src/protocols/openai-compatible-responses.ts @@ -0,0 +1,23 @@ +import { Route, type RouteRoutedModelInput } from "../route/client" +import { Endpoint } from "../route/endpoint" +import { OpenAIResponses } from "./openai-responses" + +const ADAPTER = "openai-compatible-responses" + +export type OpenAICompatibleResponsesModelInput = RouteRoutedModelInput + +/** + * Route for providers that expose an OpenAI Responses-compatible `/responses` + * endpoint. Provider helpers configure identity, endpoint, and auth before + * model selection while this route reuses the OpenAI Responses protocol. + */ +export const route = Route.make({ + id: ADAPTER, + providerMetadataKey: "openai", + protocol: OpenAIResponses.protocol, + endpoint: Endpoint.path(OpenAIResponses.PATH), + transport: OpenAIResponses.httpTransport, + defaults: { providerOptions: { openai: { store: false } } }, +}) + +export * as OpenAICompatibleResponses from "./openai-compatible-responses" diff --git a/packages/llm/src/protocols/openai-responses.ts b/packages/ai/src/protocols/openai-responses.ts similarity index 93% rename from packages/llm/src/protocols/openai-responses.ts rename to packages/ai/src/protocols/openai-responses.ts index 4936d31c92..15b930c3e0 100644 --- a/packages/llm/src/protocols/openai-responses.ts +++ b/packages/ai/src/protocols/openai-responses.ts @@ -5,6 +5,7 @@ import { Endpoint } from "../route/endpoint" import { HttpTransport, WebSocketTransport } from "../route/transport" import { Protocol } from "../route/protocol" import { + LLMError, LLMEvent, Usage, type FinishReason, @@ -19,7 +20,7 @@ import { type ToolResultPart, } from "../schema" import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" -import { isContextOverflow } from "../provider-error" +import { classifyProviderFailure } from "../provider-error" import { OpenAIOptions } from "./utils/openai-options" import { Lifecycle } from "./utils/lifecycle" import { ToolSchemaProjection } from "./utils/tool-schema" @@ -198,11 +199,11 @@ const OpenAIResponsesStreamItem = Schema.Struct({ }) type OpenAIResponsesStreamItem = Schema.Schema.Type -// OpenAI Responses surfaces provider failures in two related shapes. The -// streaming `error` event carries the details at the top level -// (`{ type: "error", code, message, param, sequence_number }`), while -// `response.failed` carries them under `response.error`. We capture both so -// the parser can surface a useful provider-error message in either path. +// The Responses schema puts streaming error details at the top level and +// response failures under `response.error`. The official SDK also recognizes +// an event-level HTTP-style `error` envelope, so accept all three shapes here. +// https://github.com/openai/openai-openapi/blob/5162af98d3147432c14680df789e8e12d4891e6b/openapi.yaml#L67234-L67382 +// https://github.com/openai/openai-node/blob/61539248cbe04665de68a71e6fd878127ae4db87/src/core/streaming.ts#L58-L85 const OpenAIResponsesErrorPayload = Schema.Struct({ code: optionalNull(Schema.String), message: optionalNull(Schema.String), @@ -227,9 +228,10 @@ const OpenAIResponsesEvent = Schema.Struct({ [Schema.Record(Schema.String, Schema.Unknown)], ), ), - code: Schema.optional(Schema.String), + code: optionalNull(Schema.String), message: Schema.optional(Schema.String), - param: Schema.optional(Schema.String), + param: optionalNull(Schema.String), + error: optionalNull(OpenAIResponsesErrorPayload), }) type OpenAIResponsesEvent = Schema.Schema.Type @@ -457,8 +459,6 @@ const lowerOptions = Effect.fn("OpenAIResponses.lowerOptions")(function* (reques const store = OpenAIOptions.store(request) const promptCacheKey = OpenAIOptions.promptCacheKey(request) const effort = OpenAIOptions.reasoningEffort(request) - if (effort && !OpenAIOptions.isReasoningEffort(effort)) - return yield* invalid(`OpenAI Responses does not support reasoning effort ${effort}`) const summary = OpenAIOptions.reasoningSummary(request) const include = OpenAIOptions.include(request) const verbosity = OpenAIOptions.textVerbosity(request) @@ -607,9 +607,8 @@ type StepResult = readonly [ParserState, ReadonlyArray] const NO_EVENTS: StepResult["1"] = [] // `response.completed` / `response.incomplete` are clean finishes that emit a -// `finish` event; `response.failed` is a hard failure that emits a -// `provider-error`. All three end the stream — kept in one set so `step` and -// the protocol's `terminal` predicate stay in sync. +// `finish` event; `response.failed` is a hard failure. All three end the stream, +// so keep this set aligned with `step` and the protocol's terminal predicate. const TERMINAL_TYPES = new Set(["response.completed", "response.incomplete", "response.failed"]) const onOutputTextDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { @@ -621,6 +620,11 @@ const onOutputTextDelta = (state: ParserState, event: OpenAIResponsesEvent): Ste ] } +const onOutputTextDone = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { + const events: LLMEvent[] = [] + return [{ ...state, lifecycle: Lifecycle.textEnd(state.lifecycle, events, event.item_id ?? "text-0") }, events] +} + const onReasoningDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { if (!event.delta) return [state, NO_EVENTS] const events: LLMEvent[] = [] @@ -812,24 +816,37 @@ const onOutputItemDone = Effect.fn("OpenAIResponses.onOutputItemDone")(function* const item = event.item if (!item) return [state, NO_EVENTS] satisfies StepResult + if (item.type === "message" && item.id) return onOutputTextDone(state, { ...event, item_id: item.id }) + if (item.type === "function_call") { if (!item.id || !item.call_id || !item.name) return [state, NO_EVENTS] satisfies StepResult - const tools = state.tools[item.id] + const existing = state.tools[item.id] + const providerMetadata = openaiMetadata({ itemId: item.id }) + const tools = existing ? state.tools - : ToolStream.start(state.tools, item.id, { id: item.call_id, name: item.name }) + : ToolStream.start(state.tools, item.id, { + id: item.call_id, + name: item.name, + providerMetadata, + }) const result = item.arguments === undefined ? yield* ToolStream.finish(ADAPTER, tools, item.id) : yield* ToolStream.finishWithInput(ADAPTER, tools, item.id, item.arguments) const events: LLMEvent[] = [] - const resultEvents = result.events ?? [] + const resultEvents = [ + ...(existing ? [] : [LLMEvent.toolInputStart({ id: item.call_id, name: item.name, providerMetadata })]), + ...(result.events ?? []), + ] const lifecycle = resultEvents.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle events.push(...resultEvents) return [ { ...state, lifecycle, - hasFunctionCall: resultEvents.some(LLMEvent.is.toolCall) ? true : state.hasFunctionCall, + hasFunctionCall: + state.hasFunctionCall || + resultEvents.some((event) => LLMEvent.is.toolCall(event) || LLMEvent.is.toolInputError(event)), tools: result.tools, }, events, @@ -894,7 +911,7 @@ const onResponseFinish = (state: ParserState, event: OpenAIResponsesEvent): Step // the bare message — production rate limits and context-length failures used // to be indistinguishable from generic stream drops. const providerErrorMessage = (event: OpenAIResponsesEvent, fallback: string): string => { - const nested = event.response?.error ?? undefined + const nested = event.error ?? event.response?.error ?? undefined const message = event.message || nested?.message || undefined const code = event.code || nested?.code || undefined if (message && code) return `${code}: ${message}` @@ -902,26 +919,18 @@ const providerErrorMessage = (event: OpenAIResponsesEvent, fallback: string): st } const providerError = (event: OpenAIResponsesEvent, fallback: string) => { - const code = event.code || event.response?.error?.code || undefined + const code = event.code || event.error?.code || event.response?.error?.code || undefined const message = providerErrorMessage(event, fallback) - return LLMEvent.providerError({ - message, - classification: code === "context_length_exceeded" || isContextOverflow(message) ? "context-overflow" : undefined, + return new LLMError({ + module: ADAPTER, + method: "stream", + reason: classifyProviderFailure({ message, code }), }) } -const onResponseFailed = (state: ParserState, event: OpenAIResponsesEvent): StepResult => [ - state, - [providerError(event, "OpenAI Responses response failed")], -] - -const onError = (state: ParserState, event: OpenAIResponsesEvent): StepResult => [ - state, - [providerError(event, "OpenAI Responses stream error")], -] - const step = (state: ParserState, event: OpenAIResponsesEvent) => { if (event.type === "response.output_text.delta") return Effect.succeed(onOutputTextDelta(state, event)) + if (event.type === "response.output_text.done") return Effect.succeed(onOutputTextDone(state, event)) if ( event.type === "response.reasoning_text.delta" || event.type === "response.reasoning_summary.delta" || @@ -943,8 +952,8 @@ const step = (state: ParserState, event: OpenAIResponsesEvent) => { if (event.type === "response.output_item.done") return onOutputItemDone(state, event) if (event.type === "response.completed" || event.type === "response.incomplete") return Effect.succeed(onResponseFinish(state, event)) - if (event.type === "response.failed") return Effect.succeed(onResponseFailed(state, event)) - if (event.type === "error") return Effect.succeed(onError(state, event)) + if (event.type === "response.failed") return providerError(event, "OpenAI Responses response failed") + if (event.type === "error") return providerError(event, "OpenAI Responses stream error") return Effect.succeed([state, NO_EVENTS]) } @@ -984,6 +993,7 @@ export const httpTransport = HttpTransport.sseJson.with() export const route = Route.make({ id: ADAPTER, provider: "openai", + providerMetadataKey: "openai", protocol, endpoint, auth, @@ -1012,6 +1022,7 @@ export const webSocketTransport = WebSocketTransport.jsonTransport.with< export const webSocketRoute = Route.make({ id: `${ADAPTER}-websocket`, provider: "openai", + providerMetadataKey: "openai", protocol, endpoint, auth, diff --git a/packages/llm/src/protocols/shared.ts b/packages/ai/src/protocols/shared.ts similarity index 94% rename from packages/llm/src/protocols/shared.ts rename to packages/ai/src/protocols/shared.ts index 173dc511bb..aad8fce5f9 100644 --- a/packages/llm/src/protocols/shared.ts +++ b/packages/ai/src/protocols/shared.ts @@ -9,6 +9,7 @@ import { type ContentPart, type LLMRequest, type MediaPart, + type ProviderMetadata, type ToolFileContent, type TextPart, type ToolResultPart, @@ -152,8 +153,34 @@ export const wrappedSystemUpdate = Effect.fn("ProviderShared.wrappedSystemUpdate * input deltas (e.g. zero-arg tools). The error message is uniform across * routes: `Invalid JSON input for tool call `. */ -export const parseToolInput = (route: string, name: string, raw: string) => - parseJson(route, raw || "{}", `Invalid JSON input for ${route} tool call ${name}`) +export const parseToolInput = ( + route: string, + tool: { + readonly id: string + readonly name: string + readonly providerExecuted?: boolean + readonly providerMetadata?: ProviderMetadata + }, + raw: string, +) => + Effect.try({ + try: () => decodeJson(raw || "{}"), + catch: () => + new LLMError({ + module: "ProviderShared", + method: "stream", + reason: new InvalidProviderOutputReason({ + route, + message: `Invalid JSON input for ${route} tool call ${tool.name}`, + raw, + source: "tool-input", + toolCallID: tool.id, + toolName: tool.name, + providerExecuted: tool.providerExecuted, + providerMetadata: tool.providerMetadata, + }), + }), + }) export const IMAGE_MIMES = ["image/png", "image/jpeg", "image/gif", "image/webp"] as const export const VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"] as const diff --git a/packages/llm/src/protocols/utils/bedrock-auth.ts b/packages/ai/src/protocols/utils/bedrock-auth.ts similarity index 100% rename from packages/llm/src/protocols/utils/bedrock-auth.ts rename to packages/ai/src/protocols/utils/bedrock-auth.ts diff --git a/packages/llm/src/protocols/utils/bedrock-cache.ts b/packages/ai/src/protocols/utils/bedrock-cache.ts similarity index 100% rename from packages/llm/src/protocols/utils/bedrock-cache.ts rename to packages/ai/src/protocols/utils/bedrock-cache.ts diff --git a/packages/llm/src/protocols/utils/bedrock-media.ts b/packages/ai/src/protocols/utils/bedrock-media.ts similarity index 100% rename from packages/llm/src/protocols/utils/bedrock-media.ts rename to packages/ai/src/protocols/utils/bedrock-media.ts diff --git a/packages/llm/src/protocols/utils/cache.ts b/packages/ai/src/protocols/utils/cache.ts similarity index 100% rename from packages/llm/src/protocols/utils/cache.ts rename to packages/ai/src/protocols/utils/cache.ts diff --git a/packages/llm/src/protocols/utils/gemini-tool-schema.ts b/packages/ai/src/protocols/utils/gemini-tool-schema.ts similarity index 100% rename from packages/llm/src/protocols/utils/gemini-tool-schema.ts rename to packages/ai/src/protocols/utils/gemini-tool-schema.ts diff --git a/packages/llm/src/protocols/utils/lifecycle.ts b/packages/ai/src/protocols/utils/lifecycle.ts similarity index 100% rename from packages/llm/src/protocols/utils/lifecycle.ts rename to packages/ai/src/protocols/utils/lifecycle.ts diff --git a/packages/llm/src/protocols/utils/openai-options.ts b/packages/ai/src/protocols/utils/openai-options.ts similarity index 78% rename from packages/llm/src/protocols/utils/openai-options.ts rename to packages/ai/src/protocols/utils/openai-options.ts index 51e56ae216..5414923eda 100644 --- a/packages/llm/src/protocols/utils/openai-options.ts +++ b/packages/ai/src/protocols/utils/openai-options.ts @@ -1,11 +1,9 @@ import { Schema } from "effect" -import type { LLMRequest, ReasoningEffort, TextVerbosity as TextVerbosityValue } from "../../schema" +import type { LLMRequest, TextVerbosity as TextVerbosityValue } from "../../schema" import { ReasoningEfforts, TextVerbosity } from "../../schema" -export const OpenAIReasoningEfforts = ReasoningEfforts.filter( - (effort): effort is Exclude => effort !== "max", -) -export type OpenAIReasoningEffort = (typeof OpenAIReasoningEfforts)[number] +export const OpenAIReasoningEfforts = ReasoningEfforts +export type OpenAIReasoningEffort = string // Mirrors OpenAI's `ResponseIncludable` union from the official SDK. Keep this // in lockstep with `openai-node/src/resources/responses/responses.ts`. @@ -23,22 +21,16 @@ export type OpenAIResponseIncludable = (typeof OpenAIResponseIncludables)[number export const OpenAIServiceTiers = ["auto", "default", "flex", "priority"] as const export type OpenAIServiceTier = (typeof OpenAIServiceTiers)[number] -const REASONING_EFFORTS = new Set(ReasoningEfforts) -const OPENAI_REASONING_EFFORTS = new Set(OpenAIReasoningEfforts) const TEXT_VERBOSITY = new Set(["low", "medium", "high"]) const INCLUDABLES = new Set(OpenAIResponseIncludables) const SERVICE_TIERS = new Set(OpenAIServiceTiers) -export const OpenAIReasoningEffort = Schema.Literals(OpenAIReasoningEfforts) +export const OpenAIReasoningEffort = Schema.String export const OpenAITextVerbosity = TextVerbosity export const OpenAIResponseIncludable = Schema.Literals(OpenAIResponseIncludables) export const OpenAIServiceTier = Schema.Literals(OpenAIServiceTiers) -const isAnyReasoningEffort = (effort: unknown): effort is ReasoningEffort => - typeof effort === "string" && REASONING_EFFORTS.has(effort) - -export const isReasoningEffort = (effort: unknown): effort is OpenAIReasoningEffort => - typeof effort === "string" && OPENAI_REASONING_EFFORTS.has(effort) +export const isReasoningEffort = (effort: unknown): effort is OpenAIReasoningEffort => typeof effort === "string" const isTextVerbosity = (value: unknown): value is TextVerbosityValue => typeof value === "string" && TEXT_VERBOSITY.has(value) @@ -50,9 +42,9 @@ export const store = (request: LLMRequest): boolean | undefined => { return typeof value === "boolean" ? value : undefined } -export const reasoningEffort = (request: LLMRequest): ReasoningEffort | undefined => { +export const reasoningEffort = (request: LLMRequest): string | undefined => { const value = options(request)?.reasoningEffort - return isAnyReasoningEffort(value) ? value : undefined + return typeof value === "string" ? value : undefined } export const reasoningSummary = (request: LLMRequest): "auto" | undefined => diff --git a/packages/llm/src/protocols/utils/tool-schema.ts b/packages/ai/src/protocols/utils/tool-schema.ts similarity index 100% rename from packages/llm/src/protocols/utils/tool-schema.ts rename to packages/ai/src/protocols/utils/tool-schema.ts diff --git a/packages/llm/src/protocols/utils/tool-stream.ts b/packages/ai/src/protocols/utils/tool-stream.ts similarity index 89% rename from packages/llm/src/protocols/utils/tool-stream.ts rename to packages/ai/src/protocols/utils/tool-stream.ts index 8e07a64bfe..233713f0d5 100644 --- a/packages/llm/src/protocols/utils/tool-stream.ts +++ b/packages/ai/src/protocols/utils/tool-stream.ts @@ -53,6 +53,7 @@ const inputStart = (tool: PendingTool) => LLMEvent.toolInputStart({ id: tool.id, name: tool.name, + providerExecuted: tool.providerExecuted ? true : undefined, providerMetadata: tool.providerMetadata, }) @@ -63,8 +64,9 @@ const inputDelta = (tool: PendingTool, text: string) => text, }) -const toolCall = (route: string, tool: PendingTool, inputOverride?: string) => - parseToolInput(route, tool.name, inputOverride ?? tool.input).pipe( +const toolCall = (route: string, tool: PendingTool, inputOverride?: string) => { + const raw = inputOverride ?? tool.input + return parseToolInput(route, tool, raw).pipe( Effect.map( (input): ToolCall => LLMEvent.toolCall({ @@ -75,7 +77,20 @@ const toolCall = (route: string, tool: PendingTool, inputOverride?: string) => providerMetadata: tool.providerMetadata, }), ), + Effect.match({ + onFailure: (error) => + LLMEvent.toolInputError({ + id: tool.id, + name: tool.name, + raw, + message: error.reason.message, + providerExecuted: tool.providerExecuted ? true : undefined, + providerMetadata: tool.providerMetadata, + }), + onSuccess: (event) => event, + }), ) +} /** Store the updated tool and produce the optional public delta event. */ const appendTool = ( @@ -158,8 +173,8 @@ export const appendExisting = ( /** * Finalize one pending tool call: parse the accumulated raw JSON, remove it - * from state, and return the optional public `tool-call` event. Missing keys are - * a no-op because some providers emit stop events for non-tool content blocks. + * from state, and emit either `tool-call` or `tool-input-error`. Missing keys + * are a no-op because some providers emit stop events for non-tool blocks. */ export const finish = (route: string, tools: State, key: K) => Effect.gen(function* () { @@ -186,7 +201,12 @@ export const finishWithInput = (route: string, tools: State return { tools: withoutTool(tools, key), events: [ - LLMEvent.toolInputEnd({ id: tool.id, name: tool.name, providerMetadata: tool.providerMetadata }), + LLMEvent.toolInputEnd({ + id: tool.id, + name: tool.name, + input, + providerMetadata: tool.providerMetadata, + }), yield* toolCall(route, tool, input), ], } diff --git a/packages/ai/src/provider-error.ts b/packages/ai/src/provider-error.ts new file mode 100644 index 0000000000..bec21ac8b2 --- /dev/null +++ b/packages/ai/src/provider-error.ts @@ -0,0 +1,155 @@ +import { Option, Schema } from "effect" +import { + AuthenticationReason, + ContentPolicyReason, + InvalidRequestReason, + LLMError, + ProviderErrorEvent, + ProviderInternalReason, + QuotaExceededReason, + RateLimitReason, + UnknownProviderReason, + type HttpContext, + type HttpRateLimitDetails, + type ProviderMetadata, +} from "./schema" + +const patterns = [ + /prompt is too long/i, + /input is too long for requested model/i, + /exceeds the context window/i, + /input token count.*exceeds the maximum/i, + /tokens in request more than max tokens allowed/i, + /maximum prompt length is \d+/i, + /reduce the length of the messages/i, + /maximum context length is \d+ tokens/i, + /exceeds the limit of \d+/i, + /exceeds the available context size/i, + /greater than the context length/i, + /context window exceeds limit/i, + /exceeded model token limit/i, + /context[_ ]length[_ ]exceeded/i, + /request entity too large/i, + /context length is only \d+ tokens/i, + /input length.*exceeds.*context length/i, + /prompt too long; exceeded (?:max )?context length/i, + /too large for model with \d+ maximum context length/i, + /model_context_window_exceeded/i, +] + +export const isContextOverflow = (message: string) => + patterns.some((pattern) => pattern.test(message)) || /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message) + +export const isContextOverflowFailure = (failure: unknown) => + failure instanceof LLMError + ? failure.reason._tag === "InvalidRequest" && failure.reason.classification === "context-overflow" + : Schema.is(ProviderErrorEvent)(failure) && failure.classification === "context-overflow" + +const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString) +const QUOTA_CODES = new Set(["insufficient_quota", "usage_not_included", "billing_error"]) +const SERVER_CODES = new Set([ + "api_error", + "internal_error", + "internalserverexception", + "modelstreamerrorexception", + "overloaded_error", + "server_error", + "server_is_overloaded", + "serviceunavailableexception", +]) +const INVALID_REQUEST_CODES = new Set(["invalid_prompt", "invalid_request_error", "validationexception"]) +const RATE_LIMIT_TEXT = /rate increased too quickly|rate[-_\s]?limit|too[_\s]?many[_\s]?requests/i +const QUOTA_TEXT = /insufficient[-_\s]?quota|quota[-_\s]?exceeded/i +const CONTENT_POLICY_TEXT = /content[-_\s]?policy|content_filter|safety/i + +export interface ProviderFailure { + readonly message: string + readonly status?: number | undefined + readonly code?: string | undefined + readonly retryAfterMs?: number | undefined + readonly rateLimit?: HttpRateLimitDetails | undefined + readonly http?: HttpContext | undefined + readonly providerMetadata?: ProviderMetadata | undefined +} + +// Keep HTTP failures and provider-reported stream failures on one typed path so +// session retry policy never needs provider-specific string matching. +export function classifyProviderFailure(input: ProviderFailure): LLMError["reason"] { + const body = input.http?.body ?? "" + const codes = [input.code, ...providerCodes(body), ...providerCodes(input.message)] + .filter((code): code is string => code !== undefined) + .map((code) => code.toLowerCase()) + const text = body || input.message + const common = { message: input.message, providerMetadata: input.providerMetadata, http: input.http } + const clientScoped = input.status === undefined || (input.status >= 400 && input.status < 500) + + if ( + clientScoped && + (codes.includes("context_length_exceeded") || + codes.includes("model_context_window_exceeded") || + isContextOverflow(text)) + ) + return new InvalidRequestReason({ ...common, classification: "context-overflow" }) + if (CONTENT_POLICY_TEXT.test(text)) return new ContentPolicyReason(common) + if (codes.some((code) => QUOTA_CODES.has(code)) || (input.status === 429 && QUOTA_TEXT.test(text))) + return new QuotaExceededReason(common) + if (input.status === 401) return new AuthenticationReason({ ...common, kind: "invalid" }) + if (input.status === 403) return new AuthenticationReason({ ...common, kind: "insufficient-permissions" }) + if (codes.includes("authentication_error")) return new AuthenticationReason({ ...common, kind: "invalid" }) + if (codes.includes("permission_error")) + return new AuthenticationReason({ ...common, kind: "insufficient-permissions" }) + if ( + codes.some((code) => code.includes("rate_limit") || code === "too_many_requests" || code === "throttlingexception") + ) + return new RateLimitReason({ + ...common, + retryAfterMs: input.retryAfterMs, + rateLimit: input.rateLimit, + }) + if (RATE_LIMIT_TEXT.test(text)) + return new RateLimitReason({ + ...common, + retryAfterMs: input.retryAfterMs, + rateLimit: input.rateLimit, + }) + if (codes.some((code) => SERVER_CODES.has(code) || code.includes("exhausted") || code.includes("unavailable"))) + return new ProviderInternalReason({ + ...common, + status: input.status, + retryAfterMs: input.retryAfterMs, + }) + if (input.status === 429) { + return new RateLimitReason({ + ...common, + retryAfterMs: input.retryAfterMs, + rateLimit: input.rateLimit, + }) + } + if (input.status !== undefined && input.status >= 500) + return new ProviderInternalReason({ + ...common, + status: input.status, + retryAfterMs: input.retryAfterMs, + }) + if (codes.some((code) => INVALID_REQUEST_CODES.has(code))) return new InvalidRequestReason(common) + if ( + input.status === 400 || + input.status === 404 || + input.status === 409 || + input.status === 413 || + input.status === 422 + ) + return new InvalidRequestReason(common) + return new UnknownProviderReason({ ...common, status: input.status }) +} + +function providerCodes(value: string) { + const decoded = Option.getOrUndefined(decodeJson(value)) + if (!isRecord(decoded)) return [] + const error = isRecord(decoded.error) ? decoded.error : undefined + return [decoded.code, error?.code, error?.type].filter((value): value is string => typeof value === "string") +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null +} diff --git a/packages/ai/src/provider-package.ts b/packages/ai/src/provider-package.ts new file mode 100644 index 0000000000..fd878a9d06 --- /dev/null +++ b/packages/ai/src/provider-package.ts @@ -0,0 +1,16 @@ +import type { Model } from "./schema" + +export interface Settings extends Readonly> { + readonly headers?: Readonly> + readonly body?: Readonly> + readonly limits?: { + readonly context: number + readonly output: number + } +} + +export interface Definition { + readonly model: (modelID: string, settings: ProviderSettings) => Model +} + +export * as ProviderPackage from "./provider-package" diff --git a/packages/llm/src/provider.ts b/packages/ai/src/provider.ts similarity index 100% rename from packages/llm/src/provider.ts rename to packages/ai/src/provider.ts diff --git a/packages/ai/src/providers.ts b/packages/ai/src/providers.ts new file mode 100644 index 0000000000..e43654f054 --- /dev/null +++ b/packages/ai/src/providers.ts @@ -0,0 +1 @@ +export * from "./providers/index" diff --git a/packages/llm/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts similarity index 56% rename from packages/llm/src/providers/amazon-bedrock.ts rename to packages/ai/src/providers/amazon-bedrock.ts index 2f1791e0d6..4e5040c9ec 100644 --- a/packages/llm/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -1,5 +1,6 @@ import type { RouteDefaultsInput } from "../route/client" import { Auth } from "../route/auth" +import type { ProviderPackage } from "../provider-package" import { ProviderID, type ModelID } from "../schema" import * as BedrockConverse from "../protocols/bedrock-converse" import type { BedrockCredentials } from "../protocols/bedrock-converse" @@ -15,6 +16,15 @@ export type Config = RouteDefaultsInput & { /** Override the computed `https://bedrock-runtime..amazonaws.com` URL. */ readonly baseURL?: string } + +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly auth?: "bearer" | "sigv4" + readonly baseURL?: string + readonly credentials?: BedrockCredentials + readonly region?: string + readonly topP?: number +} export const routes = [BedrockConverse.route] const bedrockBaseURL = (region: string) => `https://bedrock-runtime.${region}.amazonaws.com` @@ -40,4 +50,19 @@ export const configure = (input: Config = {}) => { } export const provider = configure() -export const model = provider.model +export const model: ProviderPackage.Definition["model"] = (modelID, settings) => { + if (settings.auth === "bearer" && settings.apiKey === undefined) + throw new Error("Amazon Bedrock bearer auth requires apiKey") + if (settings.auth === "sigv4" && settings.apiKey !== undefined) + throw new Error("Amazon Bedrock SigV4 auth does not accept apiKey") + return configure({ + apiKey: settings.auth === "sigv4" ? undefined : settings.apiKey, + baseURL: settings.baseURL, + credentials: settings.credentials, + generation: settings.topP === undefined ? undefined : { topP: settings.topP }, + headers: settings.headers === undefined ? undefined : { ...settings.headers }, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, + limits: settings.limits, + region: settings.region, + }).model(modelID) +} diff --git a/packages/ai/src/providers/anthropic-compatible.ts b/packages/ai/src/providers/anthropic-compatible.ts new file mode 100644 index 0000000000..7578d63075 --- /dev/null +++ b/packages/ai/src/providers/anthropic-compatible.ts @@ -0,0 +1,67 @@ +import type { ProviderPackage } from "../provider-package" +import { AnthropicMessages } from "../protocols/anthropic-messages" +import { Auth } from "../route/auth" +import type { ProviderAuthOption } from "../route/auth-options" +import type { RouteDefaultsInput } from "../route/client" +import { ProviderID, type ModelID } from "../schema" + +export const id = ProviderID.make("anthropic-compatible") + +export type Config = RouteDefaultsInput & + ProviderAuthOption<"optional"> & { + readonly provider?: string + readonly baseURL: string + } + +export type Settings = ProviderPackage.Settings & + ( + | { readonly apiKey?: string; readonly authToken?: never } + | { readonly apiKey?: never; readonly authToken?: string } + ) & { + readonly baseURL: string + readonly provider?: string + } + +export const routes = [AnthropicMessages.route] + +const auth = (input: ProviderAuthOption<"optional">) => { + if ("auth" in input && input.auth) return input.auth + return Auth.optional("apiKey" in input ? input.apiKey : undefined, "apiKey").pipe(Auth.header("x-api-key")) +} + +export const configure = (input: Config) => { + if (!input.baseURL) throw new Error("Anthropic-compatible providers require a baseURL") + const provider = input.provider ?? "anthropic-compatible" + const { provider: _, baseURL, apiKey: _apiKey, auth: _auth, ...rest } = input + const route = AnthropicMessages.route.with({ + ...rest, + provider, + endpoint: { baseURL }, + auth: auth(input), + }) + return { + id: ProviderID.make(provider), + model: (modelID: string | ModelID) => route.model({ id: modelID }), + configure, + } +} + +export const provider = { + id, + configure, +} + +export const model: ProviderPackage.Definition["model"] = (modelID, settings) => { + if (settings.apiKey !== undefined && settings.authToken !== undefined) + throw new Error("Anthropic-compatible apiKey cannot be combined with authToken") + return configure({ + ...(settings.authToken === undefined ? { apiKey: settings.apiKey } : { auth: Auth.bearer(settings.authToken) }), + baseURL: settings.baseURL, + headers: settings.headers === undefined ? undefined : { ...settings.headers }, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, + limits: settings.limits, + provider: settings.provider, + }).model(modelID) +} + +export * as AnthropicCompatible from "./anthropic-compatible" diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts new file mode 100644 index 0000000000..d317d49e1c --- /dev/null +++ b/packages/ai/src/providers/anthropic.ts @@ -0,0 +1,56 @@ +import type { RouteDefaultsInput } from "../route/client" +import { Auth } from "../route/auth" +import type { ProviderAuthOption } from "../route/auth-options" +import type { ProviderPackage } from "../provider-package" +import { ProviderID, type ModelID } from "../schema" +import { AnthropicMessages } from "../protocols/anthropic-messages" +import { AnthropicCompatible } from "./anthropic-compatible" + +export const id = ProviderID.make("anthropic") + +export const routes = [AnthropicMessages.route] + +export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string } + +export type Settings = ProviderPackage.Settings & + ( + | { readonly apiKey?: string; readonly authToken?: never } + | { readonly apiKey?: never; readonly authToken?: string } + ) & { + readonly baseURL?: string + } + +const auth = (options: ProviderAuthOption<"optional">) => { + if ("auth" in options && options.auth) return options.auth + return Auth.optional("apiKey" in options ? options.apiKey : undefined, "apiKey") + .orElse(Auth.config("ANTHROPIC_API_KEY")) + .pipe(Auth.header("x-api-key")) +} + +export const configure = (input: Config = {}) => { + const { apiKey: _, auth: _auth, baseURL, ...rest } = input + const compatible = AnthropicCompatible.configure({ + ...rest, + auth: auth(input), + baseURL: baseURL ?? AnthropicMessages.DEFAULT_BASE_URL, + provider: id, + }) + return { + id, + model: (modelID: string | ModelID) => compatible.model(modelID), + configure, + } +} + +export const provider = configure() +export const model: ProviderPackage.Definition["model"] = (modelID, settings) => { + if (settings.apiKey !== undefined && settings.authToken !== undefined) + throw new Error("Anthropic apiKey cannot be combined with authToken") + return configure({ + ...(settings.authToken === undefined ? { apiKey: settings.apiKey } : { auth: Auth.bearer(settings.authToken) }), + baseURL: settings.baseURL, + headers: settings.headers === undefined ? undefined : { ...settings.headers }, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, + limits: settings.limits, + }).model(modelID) +} diff --git a/packages/llm/src/providers/azure.ts b/packages/ai/src/providers/azure.ts similarity index 71% rename from packages/llm/src/providers/azure.ts rename to packages/ai/src/providers/azure.ts index bfac2d1cad..dd0691a539 100644 --- a/packages/llm/src/providers/azure.ts +++ b/packages/ai/src/providers/azure.ts @@ -1,6 +1,7 @@ import { Auth } from "../route/auth" import { type AtLeastOne, type ProviderAuthOption } from "../route/auth-options" import type { Route as RouteDef, RouteDefaultsInput } from "../route/client" +import type { ProviderPackage } from "../provider-package" import { ProviderID, type ModelID } from "../schema" import * as OpenAIChat from "../protocols/openai-chat" import * as OpenAIResponses from "../protocols/openai-responses" @@ -23,6 +24,14 @@ export type ModelOptions = AzureURL & } export type Config = ModelOptions +export type Settings = ProviderPackage.Settings & + AzureURL & { + readonly apiKey?: string + readonly apiVersion?: string + readonly queryParams?: Readonly> + readonly providerOptions?: OpenAIProviderOptionsInput + } + const resourceBaseURL = (resourceName: string) => `https://${resourceName.trim()}.openai.azure.com/openai/v1` const responsesRoute = OpenAIResponses.route.with({ @@ -108,3 +117,24 @@ export const provider = { id, configure, } + +const config = (settings: Settings): Config => { + const common = { + apiKey: settings.apiKey, + apiVersion: settings.apiVersion, + headers: settings.headers === undefined ? undefined : { ...settings.headers }, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, + limits: settings.limits, + providerOptions: settings.providerOptions, + queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams }, + } + if (settings.baseURL !== undefined) return { ...common, baseURL: settings.baseURL } + if (settings.resourceName !== undefined) return { ...common, resourceName: settings.resourceName } + throw new Error("Azure requires resourceName or baseURL") +} + +export const responsesModel: ProviderPackage.Definition["model"] = (modelID, settings) => + configure(config(settings)).responses(modelID) +export const chatModel: ProviderPackage.Definition["model"] = (modelID, settings) => + configure(config(settings)).chat(modelID) +export const model = responsesModel diff --git a/packages/ai/src/providers/azure/chat.ts b/packages/ai/src/providers/azure/chat.ts new file mode 100644 index 0000000000..ff3e474332 --- /dev/null +++ b/packages/ai/src/providers/azure/chat.ts @@ -0,0 +1,2 @@ +export { chatModel as model } from "../azure" +export type { Settings } from "../azure" diff --git a/packages/ai/src/providers/azure/responses.ts b/packages/ai/src/providers/azure/responses.ts new file mode 100644 index 0000000000..e7b8ab15ae --- /dev/null +++ b/packages/ai/src/providers/azure/responses.ts @@ -0,0 +1,2 @@ +export { responsesModel as model } from "../azure" +export type { Settings } from "../azure" diff --git a/packages/llm/src/providers/cloudflare.ts b/packages/ai/src/providers/cloudflare.ts similarity index 100% rename from packages/llm/src/providers/cloudflare.ts rename to packages/ai/src/providers/cloudflare.ts diff --git a/packages/llm/src/providers/github-copilot.ts b/packages/ai/src/providers/github-copilot.ts similarity index 100% rename from packages/llm/src/providers/github-copilot.ts rename to packages/ai/src/providers/github-copilot.ts diff --git a/packages/ai/src/providers/google-vertex-chat.ts b/packages/ai/src/providers/google-vertex-chat.ts new file mode 100644 index 0000000000..e2f9c1a415 --- /dev/null +++ b/packages/ai/src/providers/google-vertex-chat.ts @@ -0,0 +1,81 @@ +import type { ProviderPackage } from "../provider-package" +import { OpenAICompatibleChat } from "../protocols/openai-compatible-chat" +import type { RouteDefaultsInput } from "../route/client" +import { ProviderID, type ModelID, type ProviderOptions } from "../schema" +import { GoogleVertexShared } from "./google-vertex-shared" + +export const id = ProviderID.make("google-vertex") + +export type Config = RouteDefaultsInput & + GoogleVertexShared.OAuthOptions & { + readonly baseURL?: string + readonly location?: string + readonly project?: string + } + +export interface Settings extends ProviderPackage.Settings { + readonly accessToken?: string + readonly apiKey?: never + readonly baseURL?: string + readonly location?: string + readonly project?: string + readonly providerOptions?: ProviderOptions +} + +const route = OpenAICompatibleChat.route.with({ + id: "google-vertex-chat", + provider: id, +}) + +export const routes = [route] + +const configuredRoute = (input: Config) => { + if ("apiKey" in input && input.apiKey !== undefined) throw new Error("Google Vertex Chat does not support API keys") + const { + accessToken: _accessToken, + auth: _auth, + baseURL, + location: inputLocation, + project: inputProject, + ...rest + } = input + const location = GoogleVertexShared.location(inputLocation, "global") + const project = GoogleVertexShared.project(inputProject) + return route.with({ + ...rest, + endpoint: { + baseURL: + baseURL ?? + `https://aiplatform.googleapis.com/v1/projects/${GoogleVertexShared.requireProject(project)}/locations/${location}/endpoints/openapi`, + }, + auth: GoogleVertexShared.oauth(input, project), + }) +} + +export const configure = (input: Config = {}) => { + const route = configuredRoute(input) + return { + id, + model: (modelID: string | ModelID) => route.model({ id: modelID }), + configure, + } +} + +export const provider = { + id, + configure, +} + +export const model: ProviderPackage.Definition["model"] = (modelID, settings) => { + if (settings.apiKey !== undefined) throw new Error("Google Vertex Chat does not support API keys") + return configure({ + accessToken: settings.accessToken, + baseURL: settings.baseURL, + headers: settings.headers === undefined ? undefined : { ...settings.headers }, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, + limits: settings.limits, + location: settings.location, + project: settings.project, + providerOptions: settings.providerOptions, + }).model(modelID) +} diff --git a/packages/ai/src/providers/google-vertex-messages.ts b/packages/ai/src/providers/google-vertex-messages.ts new file mode 100644 index 0000000000..7cb6f9cfb2 --- /dev/null +++ b/packages/ai/src/providers/google-vertex-messages.ts @@ -0,0 +1,111 @@ +import { Effect, Schema, Struct } from "effect" +import type { ProviderPackage } from "../provider-package" +import { AnthropicMessages } from "../protocols/anthropic-messages" +import { Auth } from "../route/auth" +import { Route, type RouteDefaultsInput } from "../route/client" +import { Endpoint } from "../route/endpoint" +import { Framing } from "../route/framing" +import { Protocol } from "../route/protocol" +import { ProviderID, type ModelID, type ProviderOptions } from "../schema" +import { GoogleVertexShared } from "./google-vertex-shared" + +const VERSION = "vertex-2023-10-16" as const + +// models.dev uses this provider id even though the API contract is Anthropic Messages. +export const id = ProviderID.make("google-vertex-anthropic") + +export type Config = RouteDefaultsInput & + GoogleVertexShared.OAuthOptions & { + readonly baseURL?: string + readonly location?: string + readonly project?: string + } + +export interface Settings extends ProviderPackage.Settings { + readonly accessToken?: string + readonly apiKey?: never + readonly baseURL?: string + readonly location?: string + readonly project?: string + readonly providerOptions?: ProviderOptions +} + +const route = Route.make({ + id: "google-vertex-messages", + provider: id, + providerMetadataKey: "anthropic", + protocol: Protocol.make({ + id: AnthropicMessages.protocol.id, + body: { + schema: Schema.Struct({ + ...Struct.omit(AnthropicMessages.AnthropicMessagesBody.fields, ["model"]), + anthropic_version: Schema.Literal(VERSION), + }), + from: (request) => + AnthropicMessages.protocol.body.from(request).pipe( + Effect.map((body) => ({ + ...Struct.omit(body, ["model"]), + anthropic_version: VERSION, + })), + ), + }, + stream: AnthropicMessages.protocol.stream, + }), + endpoint: Endpoint.path(({ request }) => `/${request.model.id}:streamRawPredict`), + auth: Auth.none, + framing: Framing.sse, +}) + +export const routes = [route] + +const configuredRoute = (input: Config) => { + if ("apiKey" in input && input.apiKey !== undefined) + throw new Error("Google Vertex Messages does not support API keys") + const { + accessToken: _accessToken, + auth: _auth, + baseURL, + location: inputLocation, + project: inputProject, + ...rest + } = input + const location = GoogleVertexShared.location(inputLocation, "global") + const project = GoogleVertexShared.project(inputProject) + return route.with({ + ...rest, + endpoint: { + baseURL: + baseURL ?? + `https://${GoogleVertexShared.host(location)}/v1/projects/${GoogleVertexShared.requireProject(project)}/locations/${location}/publishers/anthropic/models`, + }, + auth: GoogleVertexShared.oauth(input, project), + }) +} + +export const configure = (input: Config = {}) => { + const route = configuredRoute(input) + return { + id, + model: (modelID: string | ModelID) => route.model({ id: modelID }), + configure, + } +} + +export const provider = { + id, + configure, +} + +export const model: ProviderPackage.Definition["model"] = (modelID, settings) => { + if (settings.apiKey !== undefined) throw new Error("Google Vertex Messages does not support API keys") + return configure({ + accessToken: settings.accessToken, + baseURL: settings.baseURL, + headers: settings.headers === undefined ? undefined : { ...settings.headers }, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, + limits: settings.limits, + location: settings.location, + project: settings.project, + providerOptions: settings.providerOptions, + }).model(modelID) +} diff --git a/packages/ai/src/providers/google-vertex-responses.ts b/packages/ai/src/providers/google-vertex-responses.ts new file mode 100644 index 0000000000..47ede23996 --- /dev/null +++ b/packages/ai/src/providers/google-vertex-responses.ts @@ -0,0 +1,82 @@ +import type { ProviderPackage } from "../provider-package" +import { OpenAICompatibleResponses } from "../protocols/openai-compatible-responses" +import type { RouteDefaultsInput } from "../route/client" +import { ProviderID, type ModelID, type ProviderOptions } from "../schema" +import { GoogleVertexShared } from "./google-vertex-shared" + +export const id = ProviderID.make("google-vertex") + +export type Config = RouteDefaultsInput & + GoogleVertexShared.OAuthOptions & { + readonly baseURL?: string + readonly location?: string + readonly project?: string + } + +export interface Settings extends ProviderPackage.Settings { + readonly accessToken?: string + readonly apiKey?: never + readonly baseURL?: string + readonly location?: string + readonly project?: string + readonly providerOptions?: ProviderOptions +} + +const route = OpenAICompatibleResponses.route.with({ + id: "google-vertex-responses", + provider: id, +}) + +export const routes = [route] + +const configuredRoute = (input: Config) => { + if ("apiKey" in input && input.apiKey !== undefined) + throw new Error("Google Vertex Responses does not support API keys") + const { + accessToken: _accessToken, + auth: _auth, + baseURL, + location: inputLocation, + project: inputProject, + ...rest + } = input + const location = GoogleVertexShared.location(inputLocation, "global") + const project = GoogleVertexShared.project(inputProject) + return route.with({ + ...rest, + endpoint: { + baseURL: + baseURL ?? + `https://aiplatform.googleapis.com/v1/projects/${GoogleVertexShared.requireProject(project)}/locations/${location}/endpoints/openapi`, + }, + auth: GoogleVertexShared.oauth(input, project), + }) +} + +export const configure = (input: Config = {}) => { + const route = configuredRoute(input) + return { + id, + model: (modelID: string | ModelID) => route.model({ id: modelID }), + configure, + } +} + +export const provider = { + id, + configure, +} + +export const model: ProviderPackage.Definition["model"] = (modelID, settings) => { + if (settings.apiKey !== undefined) throw new Error("Google Vertex Responses does not support API keys") + return configure({ + accessToken: settings.accessToken, + baseURL: settings.baseURL, + headers: settings.headers === undefined ? undefined : { ...settings.headers }, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, + limits: settings.limits, + location: settings.location, + project: settings.project, + providerOptions: settings.providerOptions, + }).model(modelID) +} diff --git a/packages/ai/src/providers/google-vertex-shared.ts b/packages/ai/src/providers/google-vertex-shared.ts new file mode 100644 index 0000000000..cd8a4168e6 --- /dev/null +++ b/packages/ai/src/providers/google-vertex-shared.ts @@ -0,0 +1,77 @@ +import type { AnyAuthClient } from "google-auth-library" +import { Effect, Redacted } from "effect" +import { Auth, MissingCredentialError } from "../route/auth" + +const SCOPE = "https://www.googleapis.com/auth/cloud-platform" + +export type OAuthOptions = + | { readonly accessToken?: string; readonly auth?: never } + | { readonly accessToken?: never; readonly auth?: Auth.Definition } + +export type ApiKeyOptions = + | (OAuthOptions & { readonly apiKey?: never }) + | { readonly accessToken?: never; readonly apiKey?: string; readonly auth?: never } + +export const project = (value?: string) => + value ?? + process.env.GOOGLE_VERTEX_PROJECT ?? + process.env.GOOGLE_CLOUD_PROJECT ?? + process.env.GCP_PROJECT ?? + process.env.GCLOUD_PROJECT + +export const location = (value: string | undefined, fallback: string) => + value ?? + process.env.GOOGLE_VERTEX_LOCATION ?? + process.env.GOOGLE_CLOUD_LOCATION ?? + process.env.VERTEX_LOCATION ?? + fallback + +export const host = (location: string) => { + if (location === "global") return "aiplatform.googleapis.com" + // Jurisdictional multi-regions use Regional Endpoint Platform domains. + if (location === "eu" || location === "us") return `aiplatform.${location}.rep.googleapis.com` + return `${location}-aiplatform.googleapis.com` +} + +export const requireProject = (value: string | undefined) => { + if (value) return value + throw new Error("Google Vertex requires a project when baseURL is not configured") +} + +export const apiKey = (input: ApiKeyOptions) => { + if (input.apiKey !== undefined && (input.accessToken !== undefined || input.auth !== undefined)) + throw new Error("Google Vertex apiKey cannot be combined with accessToken or auth") + if (input.accessToken !== undefined || input.auth !== undefined) return undefined + return input.apiKey ?? process.env.GOOGLE_VERTEX_API_KEY +} + +const adc = (project?: string) => { + let client: Promise | undefined + const loadClient = () => { + if (client) return client + client = import("google-auth-library").then(({ GoogleAuth }) => + new GoogleAuth({ projectId: project, scopes: [SCOPE] }).getClient(), + ) + return client + } + return Auth.effect( + Effect.tryPromise({ + try: async () => { + const token = await (await loadClient()).getAccessToken() + if (!token.token) throw new Error("Google ADC returned an empty access token") + return Redacted.make(token.token) + }, + catch: () => new MissingCredentialError("Google Application Default Credentials"), + }), + ).bearer() +} + +export const oauth = (input: OAuthOptions, project?: string) => { + if (input.accessToken !== undefined && input.auth !== undefined) + throw new Error("Google Vertex accessToken cannot be combined with auth") + if (input.auth) return input.auth + if (input.accessToken !== undefined) return Auth.bearer(input.accessToken) + return adc(project) +} + +export * as GoogleVertexShared from "./google-vertex-shared" diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts new file mode 100644 index 0000000000..a0f44711d4 --- /dev/null +++ b/packages/ai/src/providers/google-vertex.ts @@ -0,0 +1,98 @@ +import type { ProviderPackage } from "../provider-package" +import { Gemini } from "../protocols/gemini" +import { Auth } from "../route/auth" +import { Route, type RouteDefaultsInput } from "../route/client" +import { Endpoint } from "../route/endpoint" +import { Framing } from "../route/framing" +import { ProviderID, type ModelID, type ProviderOptions } from "../schema" +import { GoogleVertexShared } from "./google-vertex-shared" + +export const id = ProviderID.make("google-vertex") + +export type Config = RouteDefaultsInput & + GoogleVertexShared.ApiKeyOptions & { + readonly baseURL?: string + readonly location?: string + readonly project?: string + } + +export type Settings = ProviderPackage.Settings & + ( + | { readonly accessToken?: string; readonly apiKey?: never } + | { readonly accessToken?: never; readonly apiKey?: string } + ) & { + readonly baseURL?: string + readonly location?: string + readonly project?: string + readonly providerOptions?: ProviderOptions + } + +const route = Route.make({ + id: "google-vertex-gemini", + provider: id, + providerMetadataKey: "google", + protocol: Gemini.protocol, + endpoint: Endpoint.path(({ request }) => { + const model = String(request.model.id) + return `/${model.startsWith("endpoints/") ? model : `models/${model}`}:streamGenerateContent?alt=sse` + }), + auth: Auth.none, + framing: Framing.sse, +}) + +export const routes = [route] + +const configuredRoute = (input: Config, modelID: string | ModelID) => { + const { + accessToken: _accessToken, + apiKey: _apiKey, + auth: _auth, + baseURL, + location: inputLocation, + project: inputProject, + ...rest + } = input + const apiKey = GoogleVertexShared.apiKey(input) + const endpointModel = String(modelID).startsWith("endpoints/") + if (apiKey !== undefined && endpointModel) + throw new Error("Google Vertex tuned models do not support Express Mode API keys") + const location = GoogleVertexShared.location(inputLocation, "us-central1") + const project = GoogleVertexShared.project(inputProject) + const endpoint = + baseURL ?? + (apiKey + ? "https://aiplatform.googleapis.com/v1/publishers/google" + : `https://${GoogleVertexShared.host(location)}/v1beta1/projects/${GoogleVertexShared.requireProject(project)}/locations/${location}${endpointModel ? "" : "/publishers/google"}`) + return route.with({ + ...rest, + endpoint: { baseURL: endpoint }, + auth: apiKey === undefined ? GoogleVertexShared.oauth(input, project) : Auth.header("x-goog-api-key", apiKey), + }) +} + +export const configure = (input: Config = {}) => { + return { + id, + model: (modelID: string | ModelID) => configuredRoute(input, modelID).model({ id: modelID }), + configure, + } +} + +export const provider = { + id, + configure, +} +export const model: ProviderPackage.Definition["model"] = (modelID, settings) => { + if (settings.apiKey !== undefined && settings.accessToken !== undefined) + throw new Error("Google Vertex apiKey cannot be combined with accessToken or auth") + return configure({ + ...(settings.apiKey === undefined ? { accessToken: settings.accessToken } : { apiKey: settings.apiKey }), + baseURL: settings.baseURL, + headers: settings.headers === undefined ? undefined : { ...settings.headers }, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, + limits: settings.limits, + location: settings.location, + project: settings.project, + providerOptions: settings.providerOptions, + }).model(modelID) +} diff --git a/packages/ai/src/providers/google-vertex/chat.ts b/packages/ai/src/providers/google-vertex/chat.ts new file mode 100644 index 0000000000..085e5e1a28 --- /dev/null +++ b/packages/ai/src/providers/google-vertex/chat.ts @@ -0,0 +1,2 @@ +export { model } from "../google-vertex-chat" +export type { Settings } from "../google-vertex-chat" diff --git a/packages/ai/src/providers/google-vertex/gemini.ts b/packages/ai/src/providers/google-vertex/gemini.ts new file mode 100644 index 0000000000..d48c493513 --- /dev/null +++ b/packages/ai/src/providers/google-vertex/gemini.ts @@ -0,0 +1,2 @@ +export { model } from "../google-vertex" +export type { Settings } from "../google-vertex" diff --git a/packages/ai/src/providers/google-vertex/messages.ts b/packages/ai/src/providers/google-vertex/messages.ts new file mode 100644 index 0000000000..0ef1b3a427 --- /dev/null +++ b/packages/ai/src/providers/google-vertex/messages.ts @@ -0,0 +1,2 @@ +export { model } from "../google-vertex-messages" +export type { Settings } from "../google-vertex-messages" diff --git a/packages/ai/src/providers/google-vertex/responses.ts b/packages/ai/src/providers/google-vertex/responses.ts new file mode 100644 index 0000000000..9ed9fd99db --- /dev/null +++ b/packages/ai/src/providers/google-vertex/responses.ts @@ -0,0 +1,2 @@ +export { model } from "../google-vertex-responses" +export type { Settings } from "../google-vertex-responses" diff --git a/packages/llm/src/providers/google.ts b/packages/ai/src/providers/google.ts similarity index 60% rename from packages/llm/src/providers/google.ts rename to packages/ai/src/providers/google.ts index c8a72c31f6..6cf9ac21ec 100644 --- a/packages/llm/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -1,7 +1,8 @@ import type { RouteDefaultsInput } from "../route/client" import { Auth } from "../route/auth" import type { ProviderAuthOption } from "../route/auth-options" -import { ProviderID, type ModelID } from "../schema" +import type { ProviderPackage } from "../provider-package" +import { ProviderID, type ModelID, type ProviderOptions } from "../schema" import * as Gemini from "../protocols/gemini" export const id = ProviderID.make("google") @@ -10,6 +11,12 @@ export const routes = [Gemini.route] export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string } +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly baseURL?: string + readonly providerOptions?: ProviderOptions +} + const auth = (options: ProviderAuthOption<"optional">) => { if ("auth" in options && options.auth) return options.auth return Auth.optional("apiKey" in options ? options.apiKey : undefined, "apiKey") @@ -32,4 +39,12 @@ export const configure = (input: Config = {}) => { } export const provider = configure() -export const model = provider.model +export const model: ProviderPackage.Definition["model"] = (modelID, settings) => + configure({ + apiKey: settings.apiKey, + baseURL: settings.baseURL, + headers: settings.headers === undefined ? undefined : { ...settings.headers }, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, + limits: settings.limits, + providerOptions: settings.providerOptions, + }).model(modelID) diff --git a/packages/llm/src/providers/index.ts b/packages/ai/src/providers/index.ts similarity index 56% rename from packages/llm/src/providers/index.ts rename to packages/ai/src/providers/index.ts index 774274cf2d..9838e916be 100644 --- a/packages/llm/src/providers/index.ts +++ b/packages/ai/src/providers/index.ts @@ -1,11 +1,17 @@ export * as Anthropic from "./anthropic" +export * as AnthropicCompatible from "./anthropic-compatible" export * as AmazonBedrock from "./amazon-bedrock" export * as Azure from "./azure" export * as Cloudflare from "./cloudflare" export { CloudflareAIGateway, CloudflareWorkersAI } from "./cloudflare" export * as GitHubCopilot from "./github-copilot" export * as Google from "./google" +export * as GoogleVertex from "./google-vertex" +export * as GoogleVertexChat from "./google-vertex-chat" +export * as GoogleVertexMessages from "./google-vertex-messages" +export * as GoogleVertexResponses from "./google-vertex-responses" export * as OpenAI from "./openai" export * as OpenAICompatible from "./openai-compatible" +export * as OpenAICompatibleResponses from "./openai-compatible-responses" export * as OpenRouter from "./openrouter" export * as XAI from "./xai" diff --git a/packages/llm/src/providers/openai-compatible-profile.ts b/packages/ai/src/providers/openai-compatible-profile.ts similarity index 100% rename from packages/llm/src/providers/openai-compatible-profile.ts rename to packages/ai/src/providers/openai-compatible-profile.ts diff --git a/packages/ai/src/providers/openai-compatible-responses.ts b/packages/ai/src/providers/openai-compatible-responses.ts new file mode 100644 index 0000000000..58b8ab65ae --- /dev/null +++ b/packages/ai/src/providers/openai-compatible-responses.ts @@ -0,0 +1,55 @@ +import type { ProviderPackage } from "../provider-package" +import { OpenAICompatibleResponses } from "../protocols/openai-compatible-responses" +import { AuthOptions, type ProviderAuthOption } from "../route/auth-options" +import type { RouteDefaultsInput } from "../route/client" +import { ProviderID, type ModelID } from "../schema" +import type { OpenAIProviderOptionsInput } from "./openai-options" + +export const id = ProviderID.make("openai-compatible") + +export type Config = RouteDefaultsInput & + ProviderAuthOption<"optional"> & { + readonly provider?: string + readonly baseURL: string + } + +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly baseURL: string + readonly provider?: string + readonly providerOptions?: OpenAIProviderOptionsInput +} + +export const routes = [OpenAICompatibleResponses.route] + +export const configure = (input: Config) => { + const provider = input.provider ?? "openai-compatible" + const { provider: _, baseURL, apiKey: _apiKey, auth: _auth, ...rest } = input + const route = OpenAICompatibleResponses.route.with({ + ...rest, + provider, + endpoint: { baseURL }, + auth: AuthOptions.bearer(input, []), + }) + return { + id: ProviderID.make(provider), + model: (modelID: string | ModelID) => route.model({ id: modelID }), + configure, + } +} + +export const provider = { + id, + configure, +} + +export const model: ProviderPackage.Definition["model"] = (modelID, settings) => + configure({ + apiKey: settings.apiKey, + baseURL: settings.baseURL, + headers: settings.headers === undefined ? undefined : { ...settings.headers }, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, + limits: settings.limits, + provider: settings.provider, + providerOptions: settings.providerOptions, + }).model(modelID) diff --git a/packages/llm/src/providers/openai-compatible.ts b/packages/ai/src/providers/openai-compatible.ts similarity index 76% rename from packages/llm/src/providers/openai-compatible.ts rename to packages/ai/src/providers/openai-compatible.ts index a79f65f6df..8b5d23eed7 100644 --- a/packages/llm/src/providers/openai-compatible.ts +++ b/packages/ai/src/providers/openai-compatible.ts @@ -2,6 +2,7 @@ import { ProviderID, type ModelID } from "../schema" import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat" import type { RouteDefaultsInput } from "../route/client" import { AuthOptions, type ProviderAuthOption } from "../route/auth-options" +import type { ProviderPackage } from "../provider-package" import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile" export const id = ProviderID.make("openai-compatible") @@ -12,6 +13,12 @@ type GenericModelOptions = RouteDefaultsInput & readonly baseURL: string } +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly baseURL: string + readonly provider?: string +} + export type FamilyModelOptions = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string @@ -56,6 +63,16 @@ export const provider = { configure, } +export const model: ProviderPackage.Definition["model"] = (modelID, settings) => + configure({ + apiKey: settings.apiKey, + baseURL: settings.baseURL, + headers: settings.headers === undefined ? undefined : { ...settings.headers }, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, + limits: settings.limits, + provider: settings.provider, + }).model(modelID) + export const baseten = define(profiles.baseten) export const cerebras = define(profiles.cerebras) export const deepinfra = define(profiles.deepinfra) diff --git a/packages/ai/src/providers/openai-compatible/responses.ts b/packages/ai/src/providers/openai-compatible/responses.ts new file mode 100644 index 0000000000..03404ca68b --- /dev/null +++ b/packages/ai/src/providers/openai-compatible/responses.ts @@ -0,0 +1 @@ +export * from "../openai-compatible-responses" diff --git a/packages/llm/src/providers/openai-options.ts b/packages/ai/src/providers/openai-options.ts similarity index 100% rename from packages/llm/src/providers/openai-options.ts rename to packages/ai/src/providers/openai-options.ts diff --git a/packages/llm/src/providers/openai.ts b/packages/ai/src/providers/openai.ts similarity index 60% rename from packages/llm/src/providers/openai.ts rename to packages/ai/src/providers/openai.ts index 098cad8493..73819eaf47 100644 --- a/packages/llm/src/providers/openai.ts +++ b/packages/ai/src/providers/openai.ts @@ -1,5 +1,6 @@ import { AuthOptions, type ProviderAuthOption } from "../route/auth-options" import type { Route, RouteDefaultsInput } from "../route/client" +import type { ProviderPackage } from "../provider-package" import { ProviderID, type ModelID } from "../schema" import * as OpenAIChat from "../protocols/openai-chat" import * as OpenAIResponses from "../protocols/openai-responses" @@ -21,6 +22,16 @@ export type Config = RouteDefaultsInput & readonly providerOptions?: OpenAIProviderOptionsInput } +export interface Settings extends ProviderPackage.Settings { + readonly apiKey?: string + readonly baseURL?: string + readonly organization?: string + readonly project?: string + readonly queryParams?: Readonly> + readonly transport?: "http" | "websocket" + readonly providerOptions?: OpenAIProviderOptionsInput +} + const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "OPENAI_API_KEY") const defaults = (input: Config) => { @@ -57,7 +68,32 @@ export const configure = (input: Config = {}) => { export const provider = configure() -export const model = provider.model +const config = (settings: Settings): Config => { + const headers = { + ...(settings.organization === undefined ? {} : { "OpenAI-Organization": settings.organization }), + ...(settings.project === undefined ? {} : { "OpenAI-Project": settings.project }), + ...settings.headers, + } + return { + apiKey: settings.apiKey, + baseURL: settings.baseURL, + headers: Object.keys(headers).length === 0 ? undefined : headers, + http: settings.body === undefined ? undefined : { body: { ...settings.body } }, + limits: settings.limits, + providerOptions: settings.providerOptions, + queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams }, + } +} + +export const model: ProviderPackage.Definition["model"] = (modelID, settings) => { + const configured = configure(config(settings)) + if (settings.transport === undefined || settings.transport === "http") return configured.responses(modelID) + if (settings.transport === "websocket") return configured.responsesWebSocket(modelID) + throw new Error(`Unsupported OpenAI Responses transport: ${String(settings.transport)}`) +} + +export const chatModel: ProviderPackage.Definition["model"] = (modelID, settings) => + configure(config(settings)).chat(modelID) export const responses = provider.responses export const responsesWebSocket = provider.responsesWebSocket export const chat = provider.chat diff --git a/packages/ai/src/providers/openai/chat.ts b/packages/ai/src/providers/openai/chat.ts new file mode 100644 index 0000000000..eb92db8ac5 --- /dev/null +++ b/packages/ai/src/providers/openai/chat.ts @@ -0,0 +1,2 @@ +export { chatModel as model } from "../openai" +export type { Settings } from "../openai" diff --git a/packages/ai/src/providers/openai/responses.ts b/packages/ai/src/providers/openai/responses.ts new file mode 100644 index 0000000000..4db3a232c7 --- /dev/null +++ b/packages/ai/src/providers/openai/responses.ts @@ -0,0 +1,2 @@ +export { model } from "../openai" +export type { Settings } from "../openai" diff --git a/packages/llm/src/providers/openrouter.ts b/packages/ai/src/providers/openrouter.ts similarity index 100% rename from packages/llm/src/providers/openrouter.ts rename to packages/ai/src/providers/openrouter.ts diff --git a/packages/llm/src/providers/xai.ts b/packages/ai/src/providers/xai.ts similarity index 100% rename from packages/llm/src/providers/xai.ts rename to packages/ai/src/providers/xai.ts diff --git a/packages/ai/src/route.ts b/packages/ai/src/route.ts new file mode 100644 index 0000000000..e76b133a7a --- /dev/null +++ b/packages/ai/src/route.ts @@ -0,0 +1 @@ +export * from "./route/index" diff --git a/packages/llm/src/route/auth-options.ts b/packages/ai/src/route/auth-options.ts similarity index 91% rename from packages/llm/src/route/auth-options.ts rename to packages/ai/src/route/auth-options.ts index 7e40aa12a2..957ae9b311 100644 --- a/packages/llm/src/route/auth-options.ts +++ b/packages/ai/src/route/auth-options.ts @@ -4,7 +4,7 @@ import { Auth } from "./auth" export type ApiKeyMode = "optional" | "required" export type AuthOverride = { - readonly auth: Auth + readonly auth: Auth.Definition readonly apiKey?: never } @@ -44,7 +44,10 @@ export type AtLeastOne = { * override, otherwise resolve `apiKey` (option > config var) and apply it as * a bearer token. */ -export const bearer = (options: ProviderAuthOption<"optional">, envVar: string | ReadonlyArray): Auth => { +export const bearer = ( + options: ProviderAuthOption<"optional">, + envVar: string | ReadonlyArray, +): Auth.Definition => { if ("auth" in options && options.auth) return options.auth return (Array.isArray(envVar) ? envVar : [envVar]) .reduce( diff --git a/packages/llm/src/route/auth.ts b/packages/ai/src/route/auth.ts similarity index 90% rename from packages/llm/src/route/auth.ts rename to packages/ai/src/route/auth.ts index 32871c0454..9bbbb2b84c 100644 --- a/packages/llm/src/route/auth.ts +++ b/packages/ai/src/route/auth.ts @@ -25,19 +25,19 @@ export interface AuthInput { export interface Credential { readonly load: Effect.Effect readonly orElse: (that: Credential) => Credential - readonly bearer: () => Auth - readonly header: (name: string) => Auth + readonly bearer: () => Definition + readonly header: (name: string) => Definition readonly pipe: (f: (self: Credential) => A) => A } -export interface Auth { +export interface Definition { readonly apply: (input: AuthInput) => Effect.Effect - readonly andThen: (that: Auth) => Auth - readonly orElse: (that: Auth) => Auth - readonly pipe: (f: (self: Auth) => A) => A + readonly andThen: (that: Definition) => Definition + readonly orElse: (that: Definition) => Definition + readonly pipe: (f: (self: Definition) => A) => A } -export const isAuth = (input: unknown): input is Auth => +export const isAuth = (input: unknown): input is Definition => typeof input === "object" && input !== null && "apply" in input && typeof input.apply === "function" const credential = (load: Effect.Effect): Credential => { @@ -51,8 +51,8 @@ const credential = (load: Effect.Effect): Cr return self } -const auth = (apply: Auth["apply"]): Auth => { - const self: Auth = { +const auth = (apply: Definition["apply"]): Definition => { + const self: Definition = { apply, andThen: (that) => auth((input) => apply(input).pipe(Effect.flatMap((headers) => that.apply({ ...input, headers })))), @@ -109,15 +109,15 @@ const credentialInput = (source: Secret | Credential) => ? credentialFromSecret(source, "value") : source -export function bearer(source: Secret | Credential): Auth +export function bearer(source: Secret | Credential): Definition export function bearer(source: Secret | Credential) { return credentialInput(source).bearer() } export const apiKey = bearer -export function header(name: string): (source: Secret | Credential) => Auth -export function header(name: string, source: Secret | Credential): Auth +export function header(name: string): (source: Secret | Credential) => Definition +export function header(name: string, source: Secret | Credential): Definition export function header(name: string, source?: Secret | Credential) { if (source === undefined) { return (next: Secret | Credential) => credentialInput(next).header(name) @@ -125,8 +125,8 @@ export function header(name: string, source?: Secret | Credential) { return credentialInput(source).header(name) } -export function bearerHeader(name: string): (source: Secret | Credential) => Auth -export function bearerHeader(name: string, source: Secret | Credential): Auth +export function bearerHeader(name: string): (source: Secret | Credential) => Definition +export function bearerHeader(name: string, source: Secret | Credential): Definition export function bearerHeader(name: string, source?: Secret | Credential) { const render = (input: Secret | Credential) => fromCredential(credentialInput(input), (secret) => ({ [name]: `Bearer ${secret}` })) @@ -149,7 +149,7 @@ const toLLMError = (error: AuthError): LLMError => { } export const toEffect = - (input: Auth) => + (input: Definition) => (authInput: AuthInput): Effect.Effect => input.apply(authInput).pipe(Effect.mapError(toLLMError)) diff --git a/packages/llm/src/route/client.ts b/packages/ai/src/route/client.ts similarity index 90% rename from packages/llm/src/route/client.ts rename to packages/ai/src/route/client.ts index d3b41f5817..067292329b 100644 --- a/packages/llm/src/route/client.ts +++ b/packages/ai/src/route/client.ts @@ -1,16 +1,16 @@ import { Cause, Context, Effect, Layer, Schema, Stream } from "effect" import * as Option from "effect/Option" -import { Auth, type Auth as AuthDef } from "./auth" +import { Auth } from "./auth" import { Endpoint, type EndpointPatch } from "./endpoint" import { RequestExecutor } from "./executor" -import type { Framing } from "./framing" +import { Framing } from "./framing" import { HttpTransport } from "./transport" import type { Transport, TransportRuntime } from "./transport" import { WebSocketExecutor } from "./transport" import type { Protocol } from "./protocol" import { applyCachePolicy } from "../cache-policy" import * as ProviderShared from "../protocols/shared" -import type { LLMError, LLMEvent, PreparedRequestOf, ProtocolID, ProviderOptions } from "../schema" +import type { LLMError, PreparedRequestOf, ProtocolID, ProviderOptions } from "../schema" import { GenerationOptions, HttpOptions, @@ -19,6 +19,7 @@ import { Model, ModelLimits, LLMError as LLMErrorClass, + LLMEvent, PreparedRequest, ProviderID, mergeGenerationOptions, @@ -36,9 +37,11 @@ export interface RouteBody { export interface Route { readonly id: string readonly provider?: ProviderID + /** ProviderMetadata namespace emitted and consumed by this route. */ + readonly providerMetadataKey?: string readonly protocol: ProtocolID - readonly endpoint: Endpoint - readonly auth: AuthDef + readonly endpoint: Endpoint.Definition + readonly auth: Auth.Definition readonly transport: Transport readonly defaults: RouteDefaults readonly body: RouteBody @@ -83,7 +86,7 @@ export interface RouteDefaultsInput { export interface RoutePatch extends RouteDefaultsInput { readonly id?: string readonly provider?: string | ProviderID - readonly auth?: AuthDef + readonly auth?: Auth.Definition readonly transport?: Transport readonly endpoint?: EndpointPatch } @@ -119,7 +122,7 @@ const mergeRouteDefaults = (base: RouteDefaults | undefined, patch: RouteDefault } } -const endpointBaseURL = (endpoint: Endpoint) => +const endpointBaseURL = (endpoint: Endpoint.Definition) => typeof endpoint.baseURL === "string" ? endpoint.baseURL : undefined const mergeHeaders = (...items: ReadonlyArray | undefined>) => { @@ -184,14 +187,16 @@ export interface MakeInput { readonly id: string /** Provider identity for route-owned model construction. */ readonly provider?: string | ProviderID + /** ProviderMetadata namespace emitted and consumed by this route. */ + readonly providerMetadataKey?: string /** Semantic API contract — owns body construction, body schema, and parsing. */ readonly protocol: Protocol /** Where the request is sent. */ - readonly endpoint: Endpoint + readonly endpoint: Endpoint.Definition /** Per-request transport auth. Provider facades override this via `route.with(...)`. */ - readonly auth?: AuthDef + readonly auth?: Auth.Definition /** Stream framing — bytes -> frames before `protocol.stream.event` decoding. */ - readonly framing: Framing + readonly framing: Framing.Definition /** Static / per-request headers added before `auth` runs. */ readonly headers?: (input: { readonly request: LLMRequest }) => Record /** Route/request defaults used when compiling requests for this route. */ @@ -203,12 +208,14 @@ export interface MakeTransportInput { readonly id: string /** Provider identity for route-owned model construction. */ readonly provider?: string | ProviderID + /** ProviderMetadata namespace emitted and consumed by this route. */ + readonly providerMetadataKey?: string /** Semantic API contract — owns body construction, body schema, and parsing. */ readonly protocol: Protocol /** Where the request is sent. */ - readonly endpoint: Endpoint + readonly endpoint: Endpoint.Definition /** Per-request transport auth. Provider facades override this via `route.with(...)`. */ - readonly auth?: AuthDef + readonly auth?: Auth.Definition /** Static / per-request headers added before `auth` runs. */ readonly headers?: (input: { readonly request: LLMRequest }) => Record /** Runnable transport route. */ @@ -223,6 +230,28 @@ const streamError = (route: string, message: string, cause: Cause.Cause return ProviderShared.eventError(route, message, Cause.pretty(cause)) } +const requireTerminalEvent = (route: string) => (events: Stream.Stream) => + Stream.suspend(() => { + let terminal = false + return events.pipe( + Stream.mapEffect((event) => { + if (terminal) + return Effect.fail( + ProviderShared.eventError(route, `Provider emitted ${event.type} after the terminal event`), + ) + if (LLMEvent.is.finish(event) || LLMEvent.is.providerError(event)) terminal = true + return Effect.succeed(event) + }), + Stream.onEnd( + Effect.suspend(() => + terminal + ? Effect.void + : Effect.fail(ProviderShared.eventError(route, "Provider stream ended without a terminal finish event")), + ), + ), + ) + }) + function makeFromTransport( input: MakeTransportInput, ): Route { @@ -248,6 +277,7 @@ function makeFromTransport( const route: Route = { id: routeInput.id, provider: routeInput.provider === undefined ? undefined : ProviderID.make(routeInput.provider), + providerMetadataKey: routeInput.providerMetadataKey, protocol: protocol.id, endpoint: routeInput.endpoint, auth: routeInput.auth ?? Auth.none, @@ -291,6 +321,7 @@ function makeFromTransport( protocol.stream.onHalt ? { onHalt: protocol.stream.onHalt } : undefined, ), Stream.catchCause((cause) => Stream.fail(streamError(route, `Failed to read ${route} stream`, cause))), + requireTerminalEvent(route), ) }, } satisfies Route @@ -329,6 +360,7 @@ export function make( return makeFromTransport({ id: input.id, provider: input.provider, + providerMetadataKey: input.providerMetadataKey, protocol, endpoint: input.endpoint, auth: input.auth, diff --git a/packages/llm/src/route/endpoint.ts b/packages/ai/src/route/endpoint.ts similarity index 78% rename from packages/llm/src/route/endpoint.ts rename to packages/ai/src/route/endpoint.ts index accbe53243..2b2077907d 100644 --- a/packages/llm/src/route/endpoint.ts +++ b/packages/ai/src/route/endpoint.ts @@ -19,21 +19,24 @@ export type EndpointPart = string | ((input: EndpointInput) => strin * URL embeds the model id, region, or another body field (e.g. Bedrock, * Gemini). */ -export interface Endpoint { +export interface Definition { readonly baseURL?: string readonly path: EndpointPart readonly query?: Record } -export type EndpointPatch = Partial> +export type EndpointPatch = Partial> /** Construct an `Endpoint` from a path string or path function. */ -export const path = (value: EndpointPart, options: Omit, "path"> = {}): Endpoint => ({ +export const path = ( + value: EndpointPart, + options: Omit, "path"> = {}, +): Definition => ({ ...options, path: value, }) -export const merge = (base: Endpoint, patch: EndpointPatch): Endpoint => ({ +export const merge = (base: Definition, patch: EndpointPatch): Definition => ({ ...base, ...patch, baseURL: patch.baseURL ?? base.baseURL, @@ -44,7 +47,7 @@ export const merge = (base: Endpoint, patch: EndpointPatch): E const renderPart = (part: EndpointPart, input: EndpointInput) => typeof part === "function" ? part(input) : part -export const render = (endpoint: Endpoint, input: EndpointInput) => { +export const render = (endpoint: Definition, input: EndpointInput) => { const url = new URL(`${ProviderShared.trimBaseUrl(endpoint.baseURL ?? "")}${renderPart(endpoint.path, input)}`) for (const [key, value] of Object.entries(endpoint.query ?? {})) url.searchParams.set(key, value) return url diff --git a/packages/llm/src/route/executor.ts b/packages/ai/src/route/executor.ts similarity index 77% rename from packages/llm/src/route/executor.ts rename to packages/ai/src/route/executor.ts index b2f679c683..ab97bdaba8 100644 --- a/packages/llm/src/route/executor.ts +++ b/packages/ai/src/route/executor.ts @@ -1,4 +1,4 @@ -import { Cause, Context, Effect, Layer, Random } from "effect" +import { Cause, Context, Effect, Layer } from "effect" import { FetchHttpClient, Headers, @@ -8,21 +8,14 @@ import { HttpClientResponse, } from "effect/unstable/http" import { - AuthenticationReason, - ContentPolicyReason, HttpContext, HttpRateLimitDetails, HttpRequestDetails, HttpResponseDetails, - InvalidRequestReason, LLMError, - ProviderInternalReason, - QuotaExceededReason, - RateLimitReason, TransportReason, - UnknownProviderReason, } from "../schema" -import { isContextOverflow } from "../provider-error" +import { classifyProviderFailure } from "../provider-error" export interface Interface { readonly execute: ( @@ -33,9 +26,6 @@ export interface Interface { export class Service extends Context.Service()("@opencode/LLM/RequestExecutor") {} const BODY_LIMIT = 16_384 -const MAX_RETRIES = 2 -const BASE_DELAY_MS = 500 -const MAX_DELAY_MS = 10_000 const REDACTED = "" // One source of truth for what counts as a sensitive name across headers, @@ -88,8 +78,6 @@ const requestId = (headers: Record) => { ) } -const retryableStatus = (status: number) => status === 429 || status === 503 || status === 504 || status === 529 - const retryAfterMs = (headers: Record) => { const millis = Number(headers["retry-after-ms"]) if (Number.isFinite(millis)) return Math.max(0, millis) @@ -222,58 +210,6 @@ const responseHttp = (input: { rateLimit: input.rateLimit, }) -const statusReason = (input: { - readonly status: number - readonly message: string - readonly retryAfterMs?: number | undefined - readonly rateLimit?: HttpRateLimitDetails | undefined - readonly http: HttpContext -}) => { - const body = input.http.body ?? "" - if (/content[-_\s]?policy|content_filter|safety/i.test(body)) { - return new ContentPolicyReason({ message: input.message, http: input.http }) - } - if (input.status === 401) { - return new AuthenticationReason({ message: input.message, kind: "invalid", http: input.http }) - } - if (input.status === 403) { - return new AuthenticationReason({ message: input.message, kind: "insufficient-permissions", http: input.http }) - } - if (input.status === 429) { - if (/insufficient[-_\s]?quota|quota[-_\s]?exceeded/i.test(body)) { - return new QuotaExceededReason({ message: input.message, http: input.http }) - } - return new RateLimitReason({ - message: input.message, - retryAfterMs: input.retryAfterMs, - rateLimit: input.rateLimit, - http: input.http, - }) - } - if ( - input.status === 400 || - input.status === 404 || - input.status === 409 || - input.status === 413 || - input.status === 422 - ) { - return new InvalidRequestReason({ - message: input.message, - classification: isContextOverflow(body) ? "context-overflow" : undefined, - http: input.http, - }) - } - if (input.status >= 500 || retryableStatus(input.status)) { - return new ProviderInternalReason({ - message: input.message, - status: input.status, - retryAfterMs: input.retryAfterMs, - http: input.http, - }) - } - return new UnknownProviderReason({ message: input.message, status: input.status, http: input.http }) -} - const statusError = (request: HttpClientRequest.HttpClientRequest, redactedNames: ReadonlyArray) => (response: HttpClientResponse.HttpClientResponse) => @@ -287,7 +223,7 @@ const statusError = return yield* new LLMError({ module: "RequestExecutor", method: "execute", - reason: statusReason({ + reason: classifyProviderFailure({ status: response.status, message: providerMessage(response.status, details), retryAfterMs: retryAfter, @@ -342,27 +278,6 @@ const toHttpError = (redactedNames: ReadonlyArray) => (error: u }) } -const retryDelay = (error: LLMError, attempt: number) => { - if (error.retryAfterMs !== undefined) return Effect.succeed(Math.min(error.retryAfterMs, MAX_DELAY_MS)) - return Random.nextBetween( - Math.min(BASE_DELAY_MS * 2 ** attempt * 0.8, MAX_DELAY_MS), - Math.min(BASE_DELAY_MS * 2 ** attempt * 1.2, MAX_DELAY_MS), - ).pipe(Effect.map((delay) => Math.round(delay))) -} - -const retryStatusFailures = ( - effect: Effect.Effect, - retries = MAX_RETRIES, - attempt = 0, -): Effect.Effect => - Effect.catchTag(effect, "LLM.Error", (error): Effect.Effect => { - if (!error.retryable || retries <= 0) return Effect.fail(error) - return retryDelay(error, attempt).pipe( - Effect.flatMap((delay) => Effect.sleep(delay)), - Effect.flatMap(() => retryStatusFailures(effect, retries - 1, attempt + 1)), - ) - }) - export const layer: Layer.Layer = Layer.effect( Service, Effect.gen(function* () { @@ -375,7 +290,7 @@ export const layer: Layer.Layer = Layer.e .pipe(Effect.mapError(toHttpError(redactedNames)), Effect.flatMap(statusError(request, redactedNames))) }) return Service.of({ - execute: (request) => retryStatusFailures(executeOnce(request)), + execute: executeOnce, }) }), ) diff --git a/packages/llm/src/route/framing.ts b/packages/ai/src/route/framing.ts similarity index 88% rename from packages/llm/src/route/framing.ts rename to packages/ai/src/route/framing.ts index ef4855817d..f4ec86cbf9 100644 --- a/packages/llm/src/route/framing.ts +++ b/packages/ai/src/route/framing.ts @@ -16,12 +16,12 @@ import type { LLMError } from "../schema" * The frame type is opaque to this layer; the protocol's `decode` step turns * a frame into a typed chunk. */ -export interface Framing { +export interface Definition { readonly id: string readonly frame: (bytes: Stream.Stream) => Stream.Stream } /** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */ -export const sse: Framing = { id: "sse", frame: ProviderShared.sseFraming } +export const sse: Definition = { id: "sse", frame: ProviderShared.sseFraming } export * as Framing from "./framing" diff --git a/packages/llm/src/route/index.ts b/packages/ai/src/route/index.ts similarity index 78% rename from packages/llm/src/route/index.ts rename to packages/ai/src/route/index.ts index 48f4b7bc33..70db881ea4 100644 --- a/packages/llm/src/route/index.ts +++ b/packages/ai/src/route/index.ts @@ -17,9 +17,9 @@ export { Framing } from "./framing" export { Protocol } from "./protocol" export { HttpTransport, WebSocketExecutor, WebSocketTransport } from "./transport" export * as Transport from "./transport" -export type { Auth as AuthShape, AuthInput, Credential, CredentialError } from "./auth" +export type { Definition as AuthShape, AuthInput, Credential, CredentialError } from "./auth" export type { ApiKeyMode, AuthOverride, ProviderAuthOption } from "./auth-options" -export type { Endpoint as EndpointFn, EndpointInput } from "./endpoint" -export type { Framing as FramingDef } from "./framing" +export type { Definition as EndpointFn, EndpointInput } from "./endpoint" +export type { Definition as FramingDef } from "./framing" export type { Protocol as ProtocolDef } from "./protocol" export type { Transport as TransportDef, TransportRuntime } from "./transport" diff --git a/packages/llm/src/route/protocol.ts b/packages/ai/src/route/protocol.ts similarity index 100% rename from packages/llm/src/route/protocol.ts rename to packages/ai/src/route/protocol.ts diff --git a/packages/llm/src/route/transport/http.ts b/packages/ai/src/route/transport/http.ts similarity index 96% rename from packages/llm/src/route/transport/http.ts rename to packages/ai/src/route/transport/http.ts index acc52c6ea1..785e450a5e 100644 --- a/packages/llm/src/route/transport/http.ts +++ b/packages/ai/src/route/transport/http.ts @@ -2,7 +2,7 @@ import { Effect, Stream } from "effect" import { Headers, HttpClientRequest } from "effect/unstable/http" import { Auth } from "../auth" import { render as renderEndpoint } from "../endpoint" -import { Framing, type Framing as FramingDef } from "../framing" +import { Framing } from "../framing" import type { Transport, TransportPrepareInput } from "./index" import * as ProviderShared from "../../protocols/shared" import { mergeJsonRecords, type LLMRequest } from "../../schema" @@ -18,7 +18,7 @@ export interface JsonRequestParts { export interface HttpPrepared { readonly request: HttpClientRequest.HttpClientRequest - readonly framing: FramingDef + readonly framing: Framing.Definition } const applyQuery = (url: string, query: Record | undefined) => { @@ -29,6 +29,7 @@ const applyQuery = (url: string, query: Record | undefined) => { } const PROTOCOL_BODY_OVERLAY_DENYLIST = new Set([ + "anthropic_version", "content", "contents", "frequencyPenalty", @@ -106,7 +107,7 @@ export const jsonRequestParts = (input: JsonRequestInput) => }) export interface HttpJsonInput<_Body, Frame> { - readonly framing: FramingDef + readonly framing: Framing.Definition } export type HttpJsonPatch = Partial> diff --git a/packages/llm/src/route/transport/index.ts b/packages/ai/src/route/transport/index.ts similarity index 87% rename from packages/llm/src/route/transport/index.ts rename to packages/ai/src/route/transport/index.ts index fde9d6c415..cf8fef1d08 100644 --- a/packages/llm/src/route/transport/index.ts +++ b/packages/ai/src/route/transport/index.ts @@ -1,6 +1,6 @@ import type { Effect, Stream } from "effect" -import type { Endpoint } from "../endpoint" -import type { Auth } from "../auth" +import { Endpoint } from "../endpoint" +import { Auth } from "../auth" import type { Interface as RequestExecutorInterface } from "../executor" import type { Interface as WebSocketExecutorInterface } from "./websocket" import type { LLMError, LLMRequest } from "../../schema" @@ -23,8 +23,8 @@ export interface Transport { export interface TransportPrepareInput { readonly body: Body readonly request: LLMRequest - readonly endpoint: Endpoint - readonly auth: Auth + readonly endpoint: Endpoint.Definition + readonly auth: Auth.Definition readonly encodeBody: (body: Body) => string readonly headers?: (input: { readonly request: LLMRequest }) => Record } diff --git a/packages/llm/src/route/transport/websocket.ts b/packages/ai/src/route/transport/websocket.ts similarity index 100% rename from packages/llm/src/route/transport/websocket.ts rename to packages/ai/src/route/transport/websocket.ts diff --git a/packages/llm/src/schema/errors.ts b/packages/ai/src/schema/errors.ts similarity index 89% rename from packages/llm/src/schema/errors.ts rename to packages/ai/src/schema/errors.ts index 072e4e8389..39b185726b 100644 --- a/packages/llm/src/schema/errors.ts +++ b/packages/ai/src/schema/errors.ts @@ -38,11 +38,7 @@ export class InvalidRequestReason extends Schema.Class("LL classification: Schema.optional(ProviderFailureClassification), providerMetadata: Schema.optional(ProviderMetadata), http: Schema.optional(HttpContext), -}) { - get retryable() { - return false - } -} +}) {} export class NoRouteReason extends Schema.Class("LLM.Error.NoRoute")({ _tag: Schema.tag("NoRoute"), @@ -50,10 +46,6 @@ export class NoRouteReason extends Schema.Class("LLM.Error.NoRout provider: ProviderID, model: ModelID, }) { - get retryable() { - return false - } - get message() { return `No LLM route for ${this.provider}/${this.model} using ${this.route}` } @@ -65,11 +57,7 @@ export class AuthenticationReason extends Schema.Class("LL kind: Schema.Literals(["missing", "invalid", "expired", "insufficient-permissions", "unknown"]), providerMetadata: Schema.optional(ProviderMetadata), http: Schema.optional(HttpContext), -}) { - get retryable() { - return false - } -} +}) {} export class RateLimitReason extends Schema.Class("LLM.Error.RateLimit")({ _tag: Schema.tag("RateLimit"), @@ -78,46 +66,30 @@ export class RateLimitReason extends Schema.Class("LLM.Error.Ra rateLimit: Schema.optional(HttpRateLimitDetails), providerMetadata: Schema.optional(ProviderMetadata), http: Schema.optional(HttpContext), -}) { - get retryable() { - return true - } -} +}) {} export class QuotaExceededReason extends Schema.Class("LLM.Error.QuotaExceeded")({ _tag: Schema.tag("QuotaExceeded"), message: Schema.String, providerMetadata: Schema.optional(ProviderMetadata), http: Schema.optional(HttpContext), -}) { - get retryable() { - return false - } -} +}) {} export class ContentPolicyReason extends Schema.Class("LLM.Error.ContentPolicy")({ _tag: Schema.tag("ContentPolicy"), message: Schema.String, providerMetadata: Schema.optional(ProviderMetadata), http: Schema.optional(HttpContext), -}) { - get retryable() { - return false - } -} +}) {} export class ProviderInternalReason extends Schema.Class("LLM.Error.ProviderInternal")({ _tag: Schema.tag("ProviderInternal"), message: Schema.String, - status: Schema.Number, + status: Schema.optional(Schema.Number), retryAfterMs: Schema.optional(Schema.Number), providerMetadata: Schema.optional(ProviderMetadata), http: Schema.optional(HttpContext), -}) { - get retryable() { - return true - } -} +}) {} export class TransportReason extends Schema.Class("LLM.Error.Transport")({ _tag: Schema.tag("Transport"), @@ -125,11 +97,7 @@ export class TransportReason extends Schema.Class("LLM.Error.Tr kind: Schema.optional(Schema.String), url: Schema.optional(Schema.String), http: Schema.optional(HttpContext), -}) { - get retryable() { - return false - } -} +}) {} export class InvalidProviderOutputReason extends Schema.Class( "LLM.Error.InvalidProviderOutput", @@ -138,12 +106,12 @@ export class InvalidProviderOutputReason extends Schema.Class("LLM.Error.UnknownProvider")({ _tag: Schema.tag("UnknownProvider"), @@ -151,11 +119,7 @@ export class UnknownProviderReason extends Schema.Class(" status: Schema.optional(Schema.Number), providerMetadata: Schema.optional(ProviderMetadata), http: Schema.optional(HttpContext), -}) { - get retryable() { - return false - } -} +}) {} export const LLMErrorReason = Schema.Union([ InvalidRequestReason, @@ -178,14 +142,6 @@ export class LLMError extends Schema.TaggedErrorClass()("LLM.Error", { }) { override readonly cause = this.reason - get retryable() { - return this.reason.retryable - } - - get retryAfterMs() { - return "retryAfterMs" in this.reason ? this.reason.retryAfterMs : undefined - } - override get message() { return `${this.module}.${this.method}: ${this.reason.message}` } diff --git a/packages/llm/src/schema/events.ts b/packages/ai/src/schema/events.ts similarity index 96% rename from packages/llm/src/schema/events.ts rename to packages/ai/src/schema/events.ts index 98fcc9a24d..49b409a61b 100644 --- a/packages/llm/src/schema/events.ts +++ b/packages/ai/src/schema/events.ts @@ -129,6 +129,7 @@ export const ToolInputStart = Schema.Struct({ type: Schema.tag("tool-input-start"), id: ToolCallID, name: Schema.String, + providerExecuted: Schema.optional(Schema.Boolean), providerMetadata: Schema.optional(ProviderMetadata), }).annotate({ identifier: "LLM.Event.ToolInputStart" }) export type ToolInputStart = Schema.Schema.Type @@ -145,10 +146,22 @@ export const ToolInputEnd = Schema.Struct({ type: Schema.tag("tool-input-end"), id: ToolCallID, name: Schema.String, + input: Schema.optional(Schema.String), providerMetadata: Schema.optional(ProviderMetadata), }).annotate({ identifier: "LLM.Event.ToolInputEnd" }) export type ToolInputEnd = Schema.Schema.Type +export const ToolInputError = Schema.Struct({ + type: Schema.tag("tool-input-error"), + id: ToolCallID, + name: Schema.String, + raw: Schema.String, + message: Schema.String, + providerExecuted: Schema.optional(Schema.Boolean), + providerMetadata: Schema.optional(ProviderMetadata), +}).annotate({ identifier: "LLM.Event.ToolInputError" }) +export type ToolInputError = Schema.Schema.Type + export const ToolCall = Schema.Struct({ type: Schema.tag("tool-call"), id: ToolCallID, @@ -201,7 +214,6 @@ export const ProviderErrorEvent = Schema.Struct({ type: Schema.tag("provider-error"), message: Schema.String, classification: Schema.optional(ProviderFailureClassification), - retryable: Schema.optional(Schema.Boolean), providerMetadata: Schema.optional(ProviderMetadata), }).annotate({ identifier: "LLM.Event.ProviderError" }) export type ProviderErrorEvent = Schema.Schema.Type @@ -217,6 +229,7 @@ const llmEventTagged = Schema.Union([ ToolInputStart, ToolInputDelta, ToolInputEnd, + ToolInputError, ToolCall, ToolResult, ToolError, @@ -254,6 +267,8 @@ export const LLMEvent = Object.assign(llmEventTagged, { toolInputDelta: (input: WithID) => ToolInputDelta.make({ ...input, id: toolCallID(input.id) }), toolInputEnd: (input: WithID) => ToolInputEnd.make({ ...input, id: toolCallID(input.id) }), + toolInputError: (input: WithID) => + ToolInputError.make({ ...input, id: toolCallID(input.id) }), toolCall: (input: WithID) => ToolCall.make({ ...input, id: toolCallID(input.id) }), toolResult: (input: WithID) => ToolResult.make({ @@ -284,6 +299,7 @@ export const LLMEvent = Object.assign(llmEventTagged, { toolInputStart: llmEventTagged.guards["tool-input-start"], toolInputDelta: llmEventTagged.guards["tool-input-delta"], toolInputEnd: llmEventTagged.guards["tool-input-end"], + toolInputError: llmEventTagged.guards["tool-input-error"], toolCall: llmEventTagged.guards["tool-call"], toolResult: llmEventTagged.guards["tool-result"], toolError: llmEventTagged.guards["tool-error"], @@ -499,6 +515,7 @@ const reduceToolInputEnd = (state: ResponseState, event: ToolInputEnd): Response [event.id]: { ...current, name: event.name, + text: event.input ?? current.text, providerMetadata: event.providerMetadata ?? current.providerMetadata, }, }, @@ -549,6 +566,8 @@ const reduceResponseState = (state: ResponseState, event: LLMEvent): ResponseSta return reduceToolInputDelta(next, event) case "tool-input-end": return reduceToolInputEnd(next, event) + case "tool-input-error": + return next case "tool-call": return reduceToolCall(next, event) case "tool-result": diff --git a/packages/llm/src/schema/ids.ts b/packages/ai/src/schema/ids.ts similarity index 87% rename from packages/llm/src/schema/ids.ts rename to packages/ai/src/schema/ids.ts index 7eb7409802..4775caf2ef 100644 --- a/packages/llm/src/schema/ids.ts +++ b/packages/ai/src/schema/ids.ts @@ -1,5 +1,5 @@ import { Schema } from "effect" -import { ProviderMetadata } from "@opencode-ai/schema/llm" +import { LLM, ProviderMetadata } from "@opencode-ai/schema/llm" export { ProviderMetadata } @@ -27,7 +27,7 @@ export const ToolCallID = Schema.String export type ToolCallID = Schema.Schema.Type export const ReasoningEfforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const -export const ReasoningEffort = Schema.Literals(ReasoningEfforts) +export const ReasoningEffort = Schema.String export type ReasoningEffort = Schema.Schema.Type export const TextVerbosity = Schema.Literals(["low", "medium", "high"]) @@ -36,7 +36,7 @@ export type TextVerbosity = Schema.Schema.Type 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"]) +export const FinishReason = LLM.FinishReason export type FinishReason = Schema.Schema.Type export const JsonSchema = Schema.Record(Schema.String, Schema.Unknown) diff --git a/packages/llm/src/schema/index.ts b/packages/ai/src/schema/index.ts similarity index 100% rename from packages/llm/src/schema/index.ts rename to packages/ai/src/schema/index.ts diff --git a/packages/llm/src/schema/messages.ts b/packages/ai/src/schema/messages.ts similarity index 100% rename from packages/llm/src/schema/messages.ts rename to packages/ai/src/schema/messages.ts diff --git a/packages/llm/src/schema/options.ts b/packages/ai/src/schema/options.ts similarity index 100% rename from packages/llm/src/schema/options.ts rename to packages/ai/src/schema/options.ts diff --git a/packages/llm/src/tool-runtime.ts b/packages/ai/src/tool-runtime.ts similarity index 100% rename from packages/llm/src/tool-runtime.ts rename to packages/ai/src/tool-runtime.ts diff --git a/packages/llm/src/tool.ts b/packages/ai/src/tool.ts similarity index 97% rename from packages/llm/src/tool.ts rename to packages/ai/src/tool.ts index 11ed9854ca..62bd0df82f 100644 --- a/packages/llm/src/tool.ts +++ b/packages/ai/src/tool.ts @@ -45,7 +45,7 @@ export type ToolToModelOutput, Success extend * Internally each tool also carries memoized codecs and a precomputed * `ToolDefinition` so callers do not rebuild them per invocation. */ -export interface Tool, Success extends ToolSchema> { +export interface Definition, Success extends ToolSchema> { readonly description: string readonly parameters: Parameters readonly success: Success @@ -68,9 +68,9 @@ export interface Tool, Success extends ToolSc readonly _definition: ToolDefinitionClass } -export type AnyTool = Tool +export type AnyTool = Definition -export type ExecutableTool, Success extends ToolSchema> = Tool< +export type ExecutableTool, Success extends ToolSchema> = Definition< Parameters, Success > & { @@ -145,7 +145,7 @@ export function make, Success extends ToolSch readonly execute?: undefined readonly toModelOutput?: ToolToModelOutput readonly toStructuredOutput?: (output: Success["Encoded"]) => unknown -}): Tool +}): Definition export function make(config: { readonly description: string readonly jsonSchema: JsonSchema.JsonSchema diff --git a/packages/llm/src/utils/record.ts b/packages/ai/src/utils/record.ts similarity index 100% rename from packages/llm/src/utils/record.ts rename to packages/ai/src/utils/record.ts diff --git a/packages/llm/sst-env.d.ts b/packages/ai/sst-env.d.ts similarity index 100% rename from packages/llm/sst-env.d.ts rename to packages/ai/sst-env.d.ts diff --git a/packages/llm/test/adapter.test.ts b/packages/ai/test/adapter.test.ts similarity index 90% rename from packages/llm/test/adapter.test.ts rename to packages/ai/test/adapter.test.ts index bbbb29f37a..912d89d1e6 100644 --- a/packages/llm/test/adapter.test.ts +++ b/packages/ai/test/adapter.test.ts @@ -105,6 +105,9 @@ const echoLayer = dynamicResponse(({ text, respond }) => ) const it = testEffect(echoLayer) +const unterminated = testEffect( + dynamicResponse(({ respond }) => Effect.succeed(respond(encodeJson([{ type: "text", text: "partial" }])))), +) describe("llm route", () => { it.effect("stream and generate use the route pipeline", () => @@ -125,6 +128,15 @@ describe("llm route", () => { }), ) + unterminated.effect("fails when the normalized stream ends without a terminal event", () => + Effect.gen(function* () { + const error = yield* (yield* LLMClient.Service).stream(request).pipe(Stream.runDrain, Effect.flip) + + expect(error.reason).toMatchObject({ _tag: "InvalidProviderOutput" }) + expect(error.message).toContain("Provider stream ended without a terminal finish event") + }), + ) + it.effect("selects routes by model route value", () => Effect.gen(function* () { const llm = yield* LLMClient.Service diff --git a/packages/llm/test/auth-options.types.ts b/packages/ai/test/auth-options.types.ts similarity index 64% rename from packages/llm/test/auth-options.types.ts rename to packages/ai/test/auth-options.types.ts index 18f9508c3c..4f3d1da039 100644 --- a/packages/llm/test/auth-options.types.ts +++ b/packages/ai/test/auth-options.types.ts @@ -5,10 +5,15 @@ import { Auth as RuntimeAuth } from "../src/route/auth" import * as OpenAIChat from "../src/protocols/openai-chat" import * as AmazonBedrock from "../src/providers/amazon-bedrock" import * as Anthropic from "../src/providers/anthropic" +import * as AnthropicCompatible from "../src/providers/anthropic-compatible" import * as Azure from "../src/providers/azure" import * as Cloudflare from "../src/providers/cloudflare" import * as GitHubCopilot from "../src/providers/github-copilot" import * as Google from "../src/providers/google" +import * as GoogleVertex from "../src/providers/google-vertex" +import * as GoogleVertexChat from "../src/providers/google-vertex-chat" +import * as GoogleVertexMessages from "../src/providers/google-vertex-messages" +import * as GoogleVertexResponses from "../src/providers/google-vertex-responses" import * as OpenAI from "../src/providers/openai" import * as OpenAICompatible from "../src/providers/openai-compatible" import * as OpenRouter from "../src/providers/openrouter" @@ -135,11 +140,93 @@ Azure.configure({ resourceName: "resource", apiKey: "azure-key", auth: RuntimeAu Anthropic.configure({ apiKey: "anthropic-key" }).model("claude-haiku") // @ts-expect-error Anthropic model selectors only accept model ids. Anthropic.configure({ apiKey: "anthropic-key" }).model("claude-haiku", {}) +// @ts-expect-error Anthropic package settings accept only one auth source. +Anthropic.model("claude-sonnet-4-6", { apiKey: "anthropic-key", authToken: "anthropic-token" }) + +AnthropicCompatible.configure({ + apiKey: "messages-key", + baseURL: "https://messages.example.com/v1", + provider: "example", +}).model("compatible-model") +// @ts-expect-error Anthropic-compatible providers require a base URL. +AnthropicCompatible.configure({ apiKey: "messages-key" }) +// @ts-expect-error Anthropic-compatible model selectors only accept model ids. +AnthropicCompatible.configure({ baseURL: "https://messages.example.com/v1" }).model("compatible-model", {}) +// @ts-expect-error Anthropic-compatible package settings accept only one auth source. +AnthropicCompatible.model("compatible-model", { + apiKey: "messages-key", + authToken: "messages-token", + baseURL: "https://messages.example.com/v1", +}) Google.configure({ apiKey: "google-key" }).model("gemini-2.5-flash") // @ts-expect-error Google model selectors only accept model ids. Google.configure({ apiKey: "google-key" }).model("gemini-2.5-flash", {}) +GoogleVertex.configure({ apiKey: "vertex-key" }).model("gemini-3.5-flash") +GoogleVertex.configure({ accessToken: "vertex-token", project: "project" }).model("gemini-3.5-flash") +GoogleVertex.configure({ auth: RuntimeAuth.bearer("vertex-token"), project: "project" }).model("gemini-3.5-flash") +// @ts-expect-error Vertex Gemini model selectors only accept model ids. +GoogleVertex.configure({ apiKey: "vertex-key" }).model("gemini-3.5-flash", {}) +// @ts-expect-error Vertex Gemini config accepts only one auth source. +GoogleVertex.configure({ accessToken: "vertex-token", apiKey: "vertex-key", project: "project" }) +// @ts-expect-error Vertex Gemini package settings accept only one auth source. +GoogleVertex.model("gemini-3.5-flash", { accessToken: "vertex-token", apiKey: "vertex-key", project: "project" }) + +GoogleVertexChat.configure({ accessToken: "vertex-token", project: "project" }).model("deepseek-ai/deepseek-v3.2-maas") +GoogleVertexChat.configure({ auth: RuntimeAuth.bearer("vertex-token"), project: "project" }).model( + "deepseek-ai/deepseek-v3.2-maas", +) +// @ts-expect-error Vertex Chat package settings do not accept API keys. +GoogleVertexChat.model("deepseek-ai/deepseek-v3.2-maas", { apiKey: "vertex-key", project: "project" }) +GoogleVertexChat.configure({ accessToken: "vertex-token", project: "project" }).model( + "deepseek-ai/deepseek-v3.2-maas", + // @ts-expect-error Vertex Chat model selectors only accept model ids. + {}, +) +GoogleVertexChat.configure({ + accessToken: "vertex-token", + // @ts-expect-error Vertex Chat config accepts only one auth source. + auth: RuntimeAuth.bearer("vertex-token"), + project: "project", +}) + +GoogleVertexResponses.configure({ accessToken: "vertex-token", project: "project" }).model("xai/grok-4.20-reasoning") +GoogleVertexResponses.configure({ auth: RuntimeAuth.bearer("vertex-token"), project: "project" }).model( + "xai/grok-4.20-reasoning", +) +// @ts-expect-error Vertex Responses package settings do not accept API keys. +GoogleVertexResponses.model("xai/grok-4.20-reasoning", { apiKey: "vertex-key", project: "project" }) +GoogleVertexResponses.configure({ accessToken: "vertex-token", project: "project" }).model( + "xai/grok-4.20-reasoning", + // @ts-expect-error Vertex Responses model selectors only accept model ids. + {}, +) +GoogleVertexResponses.configure({ + accessToken: "vertex-token", + // @ts-expect-error Vertex Responses config accepts only one auth source. + auth: RuntimeAuth.bearer("vertex-token"), + project: "project", +}) + +GoogleVertexMessages.configure({ accessToken: "vertex-token", project: "project" }).model("claude-sonnet-4-6") +// @ts-expect-error Vertex Messages package settings do not accept API keys. +GoogleVertexMessages.model("claude-sonnet-4-6", { apiKey: "vertex-key", project: "project" }) +GoogleVertexMessages.configure({ auth: RuntimeAuth.bearer("vertex-token"), project: "project" }).model( + "claude-sonnet-4-6", +) +GoogleVertexMessages.configure({ accessToken: "vertex-token", project: "project" }).model( + "claude-sonnet-4-6", + // @ts-expect-error Vertex Messages model selectors only accept model ids. + {}, +) +GoogleVertexMessages.configure({ + accessToken: "vertex-token", + // @ts-expect-error Vertex Messages config accepts only one auth source. + auth: RuntimeAuth.bearer("vertex-token"), + project: "project", +}) + AmazonBedrock.configure({ apiKey: "bedrock-key" }).model("anthropic.claude") // @ts-expect-error Bedrock model selectors only accept model ids. AmazonBedrock.configure({ apiKey: "bedrock-key" }).model("anthropic.claude", {}) diff --git a/packages/llm/test/auth.test.ts b/packages/ai/test/auth.test.ts similarity index 100% rename from packages/llm/test/auth.test.ts rename to packages/ai/test/auth.test.ts diff --git a/packages/llm/test/cache-policy.test.ts b/packages/ai/test/cache-policy.test.ts similarity index 100% rename from packages/llm/test/cache-policy.test.ts rename to packages/ai/test/cache-policy.test.ts diff --git a/packages/llm/test/continuation-scenarios.ts b/packages/ai/test/continuation-scenarios.ts similarity index 100% rename from packages/llm/test/continuation-scenarios.ts rename to packages/ai/test/continuation-scenarios.ts diff --git a/packages/llm/test/endpoint.test.ts b/packages/ai/test/endpoint.test.ts similarity index 100% rename from packages/llm/test/endpoint.test.ts rename to packages/ai/test/endpoint.test.ts diff --git a/packages/llm/test/executor.test.ts b/packages/ai/test/executor.test.ts similarity index 69% rename from packages/llm/test/executor.test.ts rename to packages/ai/test/executor.test.ts index 811f7a9ffe..2a0056896f 100644 --- a/packages/llm/test/executor.test.ts +++ b/packages/ai/test/executor.test.ts @@ -1,6 +1,5 @@ import { describe, expect } from "bun:test" -import { Effect, Fiber, Layer, Random, Ref } from "effect" -import * as TestClock from "effect/testing/TestClock" +import { Effect, Layer, Ref } from "effect" import { Headers, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { LLM, LLMError } from "../src" import { LLMClient, RequestExecutor } from "../src/route" @@ -59,11 +58,6 @@ const countedResponsesLayer = (attempts: Ref.Ref, responses: ReadonlyArr ), ) -const randomMidpoint = { - nextDoubleUnsafe: () => 0.5, - nextIntUnsafe: () => 0, -} - const expectLLMError = (error: unknown) => { expect(error).toBeInstanceOf(LLMError) if (!(error instanceof LLMError)) throw new Error("expected LLMError") @@ -113,17 +107,49 @@ describe("RequestExecutor", () => { }).pipe(Effect.provide(responsesLayer([new Response("invalid parameter", { status: 400 })]))), ) - it.effect("returns redacted diagnostics for retryable rate limits", () => + it.effect("classifies provider rate limits hidden behind HTTP 400", () => + Effect.gen(function* () { + const classify = (body: string) => + Effect.gen(function* () { + const executor = yield* RequestExecutor.Service + const error = yield* executor.execute(request).pipe(Effect.flip) + + expectLLMError(error) + expect(error.reason).toMatchObject({ _tag: "RateLimit" }) + }).pipe(Effect.provide(responsesLayer([new Response(body, { status: 400 })]))) + + yield* classify("Request rate increased too quickly") + yield* classify('{"type":"error","error":{"type":"too_many_requests"}}') + yield* classify('{"type":"error","error":{"code":"rate_limit_exceeded"}}') + }), + ) + + it.effect("classifies provider overloads hidden behind HTTP 400", () => + Effect.gen(function* () { + const classify = (body: string) => + Effect.gen(function* () { + const executor = yield* RequestExecutor.Service + const error = yield* executor.execute(request).pipe(Effect.flip) + + expectLLMError(error) + expect(error.reason).toMatchObject({ _tag: "ProviderInternal" }) + }).pipe(Effect.provide(responsesLayer([new Response(body, { status: 400 })]))) + + yield* classify('{"code":"resource_exhausted"}') + yield* classify('{"code":"service_unavailable"}') + }), + ) + + it.effect("returns redacted diagnostics for rate limits", () => Effect.gen(function* () { const executor = yield* RequestExecutor.Service const error = yield* executor.execute(request).pipe(Effect.flip) expectLLMError(error) expect(error).toMatchObject({ - retryable: true, - retryAfterMs: 0, reason: { _tag: "RateLimit", + retryAfterMs: 0, rateLimit: { retryAfterMs: 0 }, http: { requestId: "req_123", @@ -146,16 +172,12 @@ describe("RequestExecutor", () => { expect(errorHttp(error)?.body).toBe("rate limited") }).pipe( Effect.provide( - responsesLayer( - Array.from( - { length: 3 }, - () => - new Response("rate limited", { - status: 429, - headers: { "retry-after-ms": "0", "x-request-id": "req_123", "x-api-key": "secret" }, - }), - ), - ), + responsesLayer([ + new Response("rate limited", { + status: 429, + headers: { "retry-after-ms": "0", "x-request-id": "req_123", "x-api-key": "secret" }, + }), + ]), ), ), ) @@ -189,24 +211,20 @@ describe("RequestExecutor", () => { }) }).pipe( Effect.provide( - responsesLayer( - Array.from( - { length: 3 }, - () => - new Response("rate limited", { - status: 429, - headers: { - "retry-after-ms": "0", - "x-ratelimit-limit-requests": "500", - "x-ratelimit-limit-tokens": "30000", - "x-ratelimit-remaining-requests": "499", - "x-ratelimit-remaining-tokens": "29900", - "x-ratelimit-reset-requests": "1s", - "x-ratelimit-reset-tokens": "10s", - }, - }), - ), - ), + responsesLayer([ + new Response("rate limited", { + status: 429, + headers: { + "retry-after-ms": "0", + "x-ratelimit-limit-requests": "500", + "x-ratelimit-limit-tokens": "30000", + "x-ratelimit-remaining-requests": "499", + "x-ratelimit-remaining-tokens": "29900", + "x-ratelimit-reset-requests": "1s", + "x-ratelimit-reset-tokens": "10s", + }, + }), + ]), ), ), ) @@ -224,48 +242,48 @@ describe("RequestExecutor", () => { remaining: { requests: "12", "input-tokens": "9000" }, reset: { requests: "2026-05-06T12:00:00Z", "input-tokens": "2026-05-06T12:00:10Z" }, }) - }).pipe( - Effect.provide( - responsesLayer( - Array.from( - { length: 3 }, - () => - new Response("overloaded", { - status: 529, - headers: { - "retry-after-ms": "0", - "anthropic-ratelimit-requests-limit": "100", - "anthropic-ratelimit-requests-remaining": "12", - "anthropic-ratelimit-requests-reset": "2026-05-06T12:00:00Z", - "anthropic-ratelimit-input-tokens-limit": "10000", - "anthropic-ratelimit-input-tokens-remaining": "9000", - "anthropic-ratelimit-input-tokens-reset": "2026-05-06T12:00:10Z", - }, - }), - ), - ), - ), - ), - ) - - it.effect("retries retryable status responses before returning the stream", () => - Effect.gen(function* () { - const executor = yield* RequestExecutor.Service - const response = yield* executor.execute(request) - - expect(response.status).toBe(200) - expect(yield* response.text).toBe("ok") }).pipe( Effect.provide( responsesLayer([ - new Response("busy", { status: 503, headers: { "retry-after-ms": "0" } }), - new Response("ok", { status: 200 }), + new Response("overloaded", { + status: 529, + headers: { + "retry-after-ms": "0", + "anthropic-ratelimit-requests-limit": "100", + "anthropic-ratelimit-requests-remaining": "12", + "anthropic-ratelimit-requests-reset": "2026-05-06T12:00:00Z", + "anthropic-ratelimit-input-tokens-limit": "10000", + "anthropic-ratelimit-input-tokens-remaining": "9000", + "anthropic-ratelimit-input-tokens-reset": "2026-05-06T12:00:10Z", + }, + }), ]), ), ), ) - it.effect("marks 504 and 529 status responses retryable", () => + it.effect("returns provider status failures without retrying", () => + Effect.gen(function* () { + const attempts = yield* Ref.make(0) + const error = yield* Effect.gen(function* () { + const executor = yield* RequestExecutor.Service + return yield* executor.execute(request).pipe(Effect.flip) + }).pipe( + Effect.provide( + countedResponsesLayer(attempts, [ + new Response("busy", { status: 503, headers: { "retry-after-ms": "0" } }), + new Response("ok", { status: 200 }), + ]), + ), + ) + + expectLLMError(error) + expect(error.reason).toMatchObject({ _tag: "ProviderInternal", status: 503 }) + expect(yield* Ref.get(attempts)).toBe(1) + }), + ) + + it.effect("marks 504 and 529 status responses as provider-internal", () => Effect.gen(function* () { const failWith = (status: number) => Effect.gen(function* () { @@ -274,19 +292,14 @@ describe("RequestExecutor", () => { expectLLMError(error) expect(error.reason).toMatchObject({ _tag: "ProviderInternal", status }) - expect(error.retryable).toBe(true) }).pipe( Effect.provide( - responsesLayer( - Array.from( - { length: 3 }, - () => - new Response("retry", { - status, - headers: { "retry-after-ms": "0" }, - }), - ), - ), + responsesLayer([ + new Response("provider failure", { + status, + headers: { "retry-after-ms": "0" }, + }), + ]), ), ) @@ -295,14 +308,13 @@ describe("RequestExecutor", () => { }), ) - it.effect("does not retry non-retryable status responses and truncates large bodies", () => + it.effect("truncates large authentication error bodies", () => Effect.gen(function* () { const executor = yield* RequestExecutor.Service const error = yield* executor.execute(request).pipe(Effect.flip) expectLLMError(error) expect(error.reason).toMatchObject({ _tag: "Authentication" }) - expect(error.retryable).toBe(false) expect(errorHttp(error)?.bodyTruncated).toBe(true) expect(errorHttp(error)?.body).toHaveLength(16_384) }).pipe( @@ -355,77 +367,7 @@ describe("RequestExecutor", () => { ), ) - it.effect("honors Retry-After delta seconds before retrying", () => - Effect.gen(function* () { - const attempts = yield* Ref.make(0) - return yield* Effect.gen(function* () { - const executor = yield* RequestExecutor.Service - const fiber = yield* executor.execute(request).pipe(Effect.forkChild) - - yield* Effect.yieldNow - expect(yield* Ref.get(attempts)).toBe(1) - - yield* TestClock.adjust(1_999) - yield* Effect.yieldNow - expect(yield* Ref.get(attempts)).toBe(1) - - yield* TestClock.adjust(1) - const response = yield* Fiber.join(fiber) - - expect(response.status).toBe(200) - expect(yield* Ref.get(attempts)).toBe(2) - }).pipe( - Effect.provide( - countedResponsesLayer(attempts, [ - new Response("busy", { status: 503, headers: { "retry-after": "2" } }), - new Response("ok", { status: 200 }), - ]), - ), - ) - }), - ) - - it.effect("uses exponential jittered delay when retry-after is absent", () => - Effect.gen(function* () { - const attempts = yield* Ref.make(0) - return yield* Effect.gen(function* () { - const executor = yield* RequestExecutor.Service - const fiber = yield* executor.execute(request).pipe(Effect.flip, Effect.forkChild) - - yield* Effect.yieldNow - expect(yield* Ref.get(attempts)).toBe(1) - - yield* TestClock.adjust(499) - yield* Effect.yieldNow - expect(yield* Ref.get(attempts)).toBe(1) - - yield* TestClock.adjust(1) - yield* Effect.yieldNow - expect(yield* Ref.get(attempts)).toBe(2) - - yield* TestClock.adjust(999) - yield* Effect.yieldNow - expect(yield* Ref.get(attempts)).toBe(2) - - yield* TestClock.adjust(1) - const error = yield* Fiber.join(fiber) - - expectLLMError(error) - expect(error.reason).toMatchObject({ _tag: "ProviderInternal" }) - expect(yield* Ref.get(attempts)).toBe(3) - }).pipe( - Effect.provide( - countedResponsesLayer(attempts, [ - new Response("busy", { status: 503 }), - new Response("still busy", { status: 503 }), - new Response("done retrying", { status: 503 }), - ]), - ), - ) - }).pipe(Effect.provideService(Random.Random, randomMidpoint)), - ) - - it.effect("does not retry after a successful response reaches stream parsing", () => + it.effect("does not re-execute after a successful response reaches stream parsing", () => Effect.gen(function* () { const attempts = yield* Ref.make(0) const model = OpenAIChat.route diff --git a/packages/llm/test/exports.test.ts b/packages/ai/test/exports.test.ts similarity index 75% rename from packages/llm/test/exports.test.ts rename to packages/ai/test/exports.test.ts index 4bed7e2e13..c4e32d6601 100644 --- a/packages/llm/test/exports.test.ts +++ b/packages/ai/test/exports.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" -import { LLM, LLMClient, Provider } from "@opencode-ai/llm" -import { Route, Protocol } from "@opencode-ai/llm/route" -import { Provider as ProviderSubpath } from "@opencode-ai/llm/provider" +import { LLM, LLMClient, Provider } from "@opencode-ai/ai" +import { Route, Protocol } from "@opencode-ai/ai/route" +import { Provider as ProviderSubpath } from "@opencode-ai/ai/provider" import { CloudflareAIGateway, CloudflareWorkersAI, @@ -9,10 +9,15 @@ import { OpenAICompatible, OpenRouter, XAI, -} from "@opencode-ai/llm/providers" -import * as GitHubCopilot from "@opencode-ai/llm/providers/github-copilot" -import { OpenAIChat, OpenAICompatibleChat, OpenAIResponses } from "@opencode-ai/llm/protocols" -import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages" +} from "@opencode-ai/ai/providers" +import * as GitHubCopilot from "@opencode-ai/ai/providers/github-copilot" +import { + OpenAIChat, + OpenAICompatibleChat, + OpenAICompatibleResponses, + OpenAIResponses, +} from "@opencode-ai/ai/protocols" +import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages" describe("public exports", () => { test("root exposes app-facing runtime APIs", () => { @@ -28,13 +33,17 @@ describe("public exports", () => { expect(Protocol.make).toBeFunction() }) - test("provider barrels expose user-facing facades", () => { + test("provider barrels expose user-facing facades", async () => { + const { OpenAICompatibleResponses } = await import("@opencode-ai/ai/providers") + expect(OpenAI.model).toBeFunction() - expect(OpenAI.provider.model).toBe(OpenAI.model) expect(OpenAI.provider.responses).toBe(OpenAI.responses) expect(OpenAI.provider.responsesWebSocket).toBe(OpenAI.responsesWebSocket) expect(OpenAI.configure({ apiKey: "fixture" }).responses).toBeFunction() expect(OpenAICompatible.deepseek.model).toBeFunction() + expect( + OpenAICompatibleResponses.configure({ baseURL: "https://responses.test/v1" }).model("fixture").route.id, + ).toBe("openai-compatible-responses") expect(CloudflareAIGateway.configure).toBeFunction() expect(CloudflareAIGateway.configure({ accountId: "fixture", gatewayApiKey: "fixture" }).model).toBeFunction() expect(CloudflareWorkersAI.configure).toBeFunction() @@ -69,6 +78,7 @@ describe("public exports", () => { test("protocol barrels expose supported low-level routes", () => { expect(OpenAIChat.route.id).toBe("openai-chat") expect(OpenAICompatibleChat.route.id).toBe("openai-compatible-chat") + expect(OpenAICompatibleResponses.route.id).toBe("openai-compatible-responses") expect(OpenAIResponses.route.id).toBe("openai-responses") expect(OpenAIResponses.webSocketRoute.id).toBe("openai-responses-websocket") expect(AnthropicMessages.route.id).toBe("anthropic-messages") diff --git a/packages/llm/test/fixtures/media/restroom.png b/packages/ai/test/fixtures/media/restroom.png similarity index 100% rename from packages/llm/test/fixtures/media/restroom.png rename to packages/ai/test/fixtures/media/restroom.png diff --git a/packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-text.json b/packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-text.json new file mode 100644 index 0000000000..cddad69724 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-text.json @@ -0,0 +1,40 @@ +{ + "version": 1, + "metadata": { + "provider": "minimax", + "protocol": "anthropic-messages", + "route": "anthropic-messages", + "transport": "http", + "model": "MiniMax-M3", + "tags": [ + "prefix:anthropic-compatible-messages", + "provider:minimax", + "protocol:anthropic-messages", + "text", + "golden" + ], + "name": "anthropic-compatible-messages/minimax-m3-anthropic-compatible-text", + "recordedAt": "2026-07-18T03:42:22.893Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.minimax.io/anthropic/v1/messages", + "headers": { + "anthropic-version": "2023-06-01", + "content-type": "application/json" + }, + "body": "{\"model\":\"MiniMax-M3\",\"system\":[{\"type\":\"text\",\"text\":\"You are concise.\"}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Reply exactly with: Hello!\"}]}],\"stream\":true,\"max_tokens\":40,\"temperature\":0}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"1a0b363d0882af316faebcec4d4855a8\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":53,\"output_tokens\":0,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":114,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"!\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"input_tokens\":53,\"output_tokens\":2,\"cache_read_input_tokens\":114,\"service_tier\":\"standard\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-tool-call.json b/packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-tool-call.json new file mode 100644 index 0000000000..7e02d6e8ce --- /dev/null +++ b/packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-tool-call.json @@ -0,0 +1,41 @@ +{ + "version": 1, + "metadata": { + "provider": "minimax", + "protocol": "anthropic-messages", + "route": "anthropic-messages", + "transport": "http", + "model": "MiniMax-M3", + "tags": [ + "prefix:anthropic-compatible-messages", + "provider:minimax", + "protocol:anthropic-messages", + "tool", + "tool-call", + "golden" + ], + "name": "anthropic-compatible-messages/minimax-m3-anthropic-compatible-tool-call", + "recordedAt": "2026-07-18T03:42:23.876Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.minimax.io/anthropic/v1/messages", + "headers": { + "anthropic-version": "2023-06-01", + "content-type": "application/json" + }, + "body": "{\"model\":\"MiniMax-M3\",\"system\":[{\"type\":\"text\",\"text\":\"Call tools exactly as requested.\"}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Call get_weather with city exactly Paris.\"}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"tool_choice\":{\"type\":\"tool\",\"name\":\"get_weather\"},\"stream\":true,\"max_tokens\":80,\"temperature\":0}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"6731ecc323233459d1792df9a733dd98\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":404,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"call_function_vkxtif4epmvm_1\",\"name\":\"get_weather\",\"input\":{}}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"city\\\": \\\"Paris\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"input_tokens\":290,\"output_tokens\":27,\"cache_read_input_tokens\":114,\"service_tier\":\"standard\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-tool-loop.json b/packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-tool-loop.json new file mode 100644 index 0000000000..e87e275c79 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-tool-loop.json @@ -0,0 +1,60 @@ +{ + "version": 1, + "metadata": { + "provider": "minimax", + "protocol": "anthropic-messages", + "route": "anthropic-messages", + "transport": "http", + "model": "MiniMax-M3", + "tags": [ + "prefix:anthropic-compatible-messages", + "provider:minimax", + "protocol:anthropic-messages", + "tool", + "tool-loop", + "golden" + ], + "name": "anthropic-compatible-messages/minimax-m3-anthropic-compatible-tool-loop", + "recordedAt": "2026-07-18T03:42:25.248Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.minimax.io/anthropic/v1/messages", + "headers": { + "anthropic-version": "2023-06-01", + "content-type": "application/json" + }, + "body": "{\"model\":\"MiniMax-M3\",\"system\":[{\"type\":\"text\",\"text\":\"Use the get_weather tool exactly once. After the tool result, reply exactly: Paris is sunny.\"}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is the weather in Paris?\"}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"stream\":true,\"max_tokens\":80,\"temperature\":0}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"3807fa12f9ecb9357df511e099da6da0\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":417,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"call_function_yr64rwmre4gr_1\",\"name\":\"get_weather\",\"input\":{}}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"city\\\": \\\"Paris\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"input_tokens\":303,\"output_tokens\":27,\"cache_read_input_tokens\":114,\"service_tier\":\"standard\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n" + } + }, + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.minimax.io/anthropic/v1/messages", + "headers": { + "anthropic-version": "2023-06-01", + "content-type": "application/json" + }, + "body": "{\"model\":\"MiniMax-M3\",\"system\":[{\"type\":\"text\",\"text\":\"Use the get_weather tool exactly once. After the tool result, reply exactly: Paris is sunny.\"}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is the weather in Paris?\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"call_function_yr64rwmre4gr_1\",\"name\":\"get_weather\",\"input\":{\"city\":\"Paris\"}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_function_yr64rwmre4gr_1\",\"content\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"stream\":true,\"max_tokens\":80,\"temperature\":0}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"92f8a1e86f29946eb2699d40a088fc08\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":41,\"output_tokens\":0,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":430,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Paris\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" is sunny.\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"input_tokens\":41,\"output_tokens\":4,\"cache_read_input_tokens\":430,\"service_tier\":\"standard\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n" + } + } + ] +} diff --git a/packages/llm/test/fixtures/recordings/anthropic-messages-cache/writes-then-reads-cache-control-on-identical-second-call.json b/packages/ai/test/fixtures/recordings/anthropic-messages-cache/writes-then-reads-cache-control-on-identical-second-call.json similarity index 100% rename from packages/llm/test/fixtures/recordings/anthropic-messages-cache/writes-then-reads-cache-control-on-identical-second-call.json rename to packages/ai/test/fixtures/recordings/anthropic-messages-cache/writes-then-reads-cache-control-on-identical-second-call.json diff --git a/packages/llm/test/fixtures/recordings/anthropic-messages/accepts-malformed-assistant-tool-order-with-default-patch.json b/packages/ai/test/fixtures/recordings/anthropic-messages/accepts-malformed-assistant-tool-order-with-default-patch.json similarity index 100% rename from packages/llm/test/fixtures/recordings/anthropic-messages/accepts-malformed-assistant-tool-order-with-default-patch.json rename to packages/ai/test/fixtures/recordings/anthropic-messages/accepts-malformed-assistant-tool-order-with-default-patch.json diff --git a/packages/llm/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json b/packages/ai/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json similarity index 100% rename from packages/llm/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json rename to packages/ai/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json diff --git a/packages/llm/test/fixtures/recordings/anthropic-messages/claude-opus-4-7-drives-a-tool-loop.json b/packages/ai/test/fixtures/recordings/anthropic-messages/claude-opus-4-7-drives-a-tool-loop.json similarity index 100% rename from packages/llm/test/fixtures/recordings/anthropic-messages/claude-opus-4-7-drives-a-tool-loop.json rename to packages/ai/test/fixtures/recordings/anthropic-messages/claude-opus-4-7-drives-a-tool-loop.json diff --git a/packages/llm/test/fixtures/recordings/anthropic-messages/rejects-malformed-assistant-tool-order-without-patch.json b/packages/ai/test/fixtures/recordings/anthropic-messages/rejects-malformed-assistant-tool-order-without-patch.json similarity index 100% rename from packages/llm/test/fixtures/recordings/anthropic-messages/rejects-malformed-assistant-tool-order-without-patch.json rename to packages/ai/test/fixtures/recordings/anthropic-messages/rejects-malformed-assistant-tool-order-without-patch.json diff --git a/packages/llm/test/fixtures/recordings/anthropic-messages/streams-text.json b/packages/ai/test/fixtures/recordings/anthropic-messages/streams-text.json similarity index 100% rename from packages/llm/test/fixtures/recordings/anthropic-messages/streams-text.json rename to packages/ai/test/fixtures/recordings/anthropic-messages/streams-text.json diff --git a/packages/llm/test/fixtures/recordings/anthropic-messages/streams-tool-call.json b/packages/ai/test/fixtures/recordings/anthropic-messages/streams-tool-call.json similarity index 100% rename from packages/llm/test/fixtures/recordings/anthropic-messages/streams-tool-call.json rename to packages/ai/test/fixtures/recordings/anthropic-messages/streams-tool-call.json diff --git a/packages/llm/test/fixtures/recordings/bedrock-converse/drives-a-tool-loop.json b/packages/ai/test/fixtures/recordings/bedrock-converse/drives-a-tool-loop.json similarity index 100% rename from packages/llm/test/fixtures/recordings/bedrock-converse/drives-a-tool-loop.json rename to packages/ai/test/fixtures/recordings/bedrock-converse/drives-a-tool-loop.json diff --git a/packages/llm/test/fixtures/recordings/bedrock-converse/streams-a-tool-call.json b/packages/ai/test/fixtures/recordings/bedrock-converse/streams-a-tool-call.json similarity index 100% rename from packages/llm/test/fixtures/recordings/bedrock-converse/streams-a-tool-call.json rename to packages/ai/test/fixtures/recordings/bedrock-converse/streams-a-tool-call.json diff --git a/packages/llm/test/fixtures/recordings/bedrock-converse/streams-text.json b/packages/ai/test/fixtures/recordings/bedrock-converse/streams-text.json similarity index 100% rename from packages/llm/test/fixtures/recordings/bedrock-converse/streams-text.json rename to packages/ai/test/fixtures/recordings/bedrock-converse/streams-text.json diff --git a/packages/llm/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-gpt-oss-20b-tools-tool-call.json b/packages/ai/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-gpt-oss-20b-tools-tool-call.json similarity index 100% rename from packages/llm/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-gpt-oss-20b-tools-tool-call.json rename to packages/ai/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-gpt-oss-20b-tools-tool-call.json diff --git a/packages/llm/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-llama-3-1-8b-text.json b/packages/ai/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-llama-3-1-8b-text.json similarity index 100% rename from packages/llm/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-llama-3-1-8b-text.json rename to packages/ai/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-llama-3-1-8b-text.json diff --git a/packages/llm/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-gpt-oss-20b-tools-tool-call.json b/packages/ai/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-gpt-oss-20b-tools-tool-call.json similarity index 100% rename from packages/llm/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-gpt-oss-20b-tools-tool-call.json rename to packages/ai/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-gpt-oss-20b-tools-tool-call.json diff --git a/packages/llm/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-llama-3-1-8b-text.json b/packages/ai/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-llama-3-1-8b-text.json similarity index 100% rename from packages/llm/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-llama-3-1-8b-text.json rename to packages/ai/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-llama-3-1-8b-text.json diff --git a/packages/llm/test/fixtures/recordings/gemini-cache/reports-cachedcontenttokencount-on-identical-second-call.json b/packages/ai/test/fixtures/recordings/gemini-cache/reports-cachedcontenttokencount-on-identical-second-call.json similarity index 100% rename from packages/llm/test/fixtures/recordings/gemini-cache/reports-cachedcontenttokencount-on-identical-second-call.json rename to packages/ai/test/fixtures/recordings/gemini-cache/reports-cachedcontenttokencount-on-identical-second-call.json diff --git a/packages/llm/test/fixtures/recordings/gemini/gemini-2-5-flash-image.json b/packages/ai/test/fixtures/recordings/gemini/gemini-2-5-flash-image.json similarity index 100% rename from packages/llm/test/fixtures/recordings/gemini/gemini-2-5-flash-image.json rename to packages/ai/test/fixtures/recordings/gemini/gemini-2-5-flash-image.json diff --git a/packages/llm/test/fixtures/recordings/gemini/streams-text.json b/packages/ai/test/fixtures/recordings/gemini/streams-text.json similarity index 100% rename from packages/llm/test/fixtures/recordings/gemini/streams-text.json rename to packages/ai/test/fixtures/recordings/gemini/streams-text.json diff --git a/packages/llm/test/fixtures/recordings/gemini/streams-tool-call.json b/packages/ai/test/fixtures/recordings/gemini/streams-tool-call.json similarity index 100% rename from packages/llm/test/fixtures/recordings/gemini/streams-tool-call.json rename to packages/ai/test/fixtures/recordings/gemini/streams-tool-call.json diff --git a/packages/llm/test/fixtures/recordings/openai-chat/continues-after-tool-result.json b/packages/ai/test/fixtures/recordings/openai-chat/continues-after-tool-result.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-chat/continues-after-tool-result.json rename to packages/ai/test/fixtures/recordings/openai-chat/continues-after-tool-result.json diff --git a/packages/llm/test/fixtures/recordings/openai-chat/drives-a-tool-loop-end-to-end.json b/packages/ai/test/fixtures/recordings/openai-chat/drives-a-tool-loop-end-to-end.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-chat/drives-a-tool-loop-end-to-end.json rename to packages/ai/test/fixtures/recordings/openai-chat/drives-a-tool-loop-end-to-end.json diff --git a/packages/llm/test/fixtures/recordings/openai-chat/streams-text.json b/packages/ai/test/fixtures/recordings/openai-chat/streams-text.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-chat/streams-text.json rename to packages/ai/test/fixtures/recordings/openai-chat/streams-text.json diff --git a/packages/llm/test/fixtures/recordings/openai-chat/streams-tool-call.json b/packages/ai/test/fixtures/recordings/openai-chat/streams-tool-call.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-chat/streams-tool-call.json rename to packages/ai/test/fixtures/recordings/openai-chat/streams-tool-call.json diff --git a/packages/llm/test/fixtures/recordings/openai-compatible-chat/deepseek-streams-text.json b/packages/ai/test/fixtures/recordings/openai-compatible-chat/deepseek-streams-text.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-compatible-chat/deepseek-streams-text.json rename to packages/ai/test/fixtures/recordings/openai-compatible-chat/deepseek-streams-text.json diff --git a/packages/llm/test/fixtures/recordings/openai-compatible-chat/groq-llama-3-3-70b-drives-a-tool-loop.json b/packages/ai/test/fixtures/recordings/openai-compatible-chat/groq-llama-3-3-70b-drives-a-tool-loop.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-compatible-chat/groq-llama-3-3-70b-drives-a-tool-loop.json rename to packages/ai/test/fixtures/recordings/openai-compatible-chat/groq-llama-3-3-70b-drives-a-tool-loop.json diff --git a/packages/llm/test/fixtures/recordings/openai-compatible-chat/groq-streams-text.json b/packages/ai/test/fixtures/recordings/openai-compatible-chat/groq-streams-text.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-compatible-chat/groq-streams-text.json rename to packages/ai/test/fixtures/recordings/openai-compatible-chat/groq-streams-text.json diff --git a/packages/llm/test/fixtures/recordings/openai-compatible-chat/groq-streams-tool-call.json b/packages/ai/test/fixtures/recordings/openai-compatible-chat/groq-streams-tool-call.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-compatible-chat/groq-streams-tool-call.json rename to packages/ai/test/fixtures/recordings/openai-compatible-chat/groq-streams-tool-call.json diff --git a/packages/llm/test/fixtures/recordings/openai-compatible-chat/openrouter-claude-opus-4-7-drives-a-tool-loop.json b/packages/ai/test/fixtures/recordings/openai-compatible-chat/openrouter-claude-opus-4-7-drives-a-tool-loop.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-compatible-chat/openrouter-claude-opus-4-7-drives-a-tool-loop.json rename to packages/ai/test/fixtures/recordings/openai-compatible-chat/openrouter-claude-opus-4-7-drives-a-tool-loop.json diff --git a/packages/llm/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-4o-mini-drives-a-tool-loop.json b/packages/ai/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-4o-mini-drives-a-tool-loop.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-4o-mini-drives-a-tool-loop.json rename to packages/ai/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-4o-mini-drives-a-tool-loop.json diff --git a/packages/llm/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-5-5-drives-a-tool-loop.json b/packages/ai/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-5-5-drives-a-tool-loop.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-5-5-drives-a-tool-loop.json rename to packages/ai/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-5-5-drives-a-tool-loop.json diff --git a/packages/llm/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-text.json b/packages/ai/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-text.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-text.json rename to packages/ai/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-text.json diff --git a/packages/llm/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-tool-call.json b/packages/ai/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-tool-call.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-tool-call.json rename to packages/ai/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-tool-call.json diff --git a/packages/llm/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-text.json b/packages/ai/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-text.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-text.json rename to packages/ai/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-text.json diff --git a/packages/llm/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-tool-call.json b/packages/ai/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-tool-call.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-tool-call.json rename to packages/ai/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-tool-call.json diff --git a/packages/llm/test/fixtures/recordings/openai-responses-cache/reports-cached-tokens-on-identical-second-call.json b/packages/ai/test/fixtures/recordings/openai-responses-cache/reports-cached-tokens-on-identical-second-call.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-responses-cache/reports-cached-tokens-on-identical-second-call.json rename to packages/ai/test/fixtures/recordings/openai-responses-cache/reports-cached-tokens-on-identical-second-call.json diff --git a/packages/llm/test/fixtures/recordings/openai-responses/gpt-5-5-drives-a-tool-loop.json b/packages/ai/test/fixtures/recordings/openai-responses/gpt-5-5-drives-a-tool-loop.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-responses/gpt-5-5-drives-a-tool-loop.json rename to packages/ai/test/fixtures/recordings/openai-responses/gpt-5-5-drives-a-tool-loop.json diff --git a/packages/llm/test/fixtures/recordings/openai-responses/gpt-5-5-streams-text.json b/packages/ai/test/fixtures/recordings/openai-responses/gpt-5-5-streams-text.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-responses/gpt-5-5-streams-text.json rename to packages/ai/test/fixtures/recordings/openai-responses/gpt-5-5-streams-text.json diff --git a/packages/llm/test/fixtures/recordings/openai-responses/gpt-5-5-streams-tool-call.json b/packages/ai/test/fixtures/recordings/openai-responses/gpt-5-5-streams-tool-call.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-responses/gpt-5-5-streams-tool-call.json rename to packages/ai/test/fixtures/recordings/openai-responses/gpt-5-5-streams-tool-call.json diff --git a/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-image-tool-result.json b/packages/ai/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-image-tool-result.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-image-tool-result.json rename to packages/ai/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-image-tool-result.json diff --git a/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json b/packages/ai/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json rename to packages/ai/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json diff --git a/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning.json b/packages/ai/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning.json similarity index 100% rename from packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning.json rename to packages/ai/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning.json diff --git a/packages/ai/test/fixtures/recordings/openrouter-reasoning.json b/packages/ai/test/fixtures/recordings/openrouter-reasoning.json new file mode 100644 index 0000000000..46f8a28296 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/openrouter-reasoning.json @@ -0,0 +1,34 @@ +{ + "version": 1, + "metadata": { + "model": "anthropic/claude-sonnet-4.6", + "tags": [ + "prefix:openai-compatible-chat", + "provider:openrouter", + "protocol:openai-chat", + "reasoning" + ], + "name": "openrouter-reasoning", + "recordedAt": "2026-07-18T11:28:39.267Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://openrouter.ai/api/v1/chat/completions", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"anthropic/claude-sonnet-4.6\",\"messages\":[{\"role\":\"system\",\"content\":\"Think through the arithmetic, then reply with only the final integer.\"},{\"role\":\"user\",\"content\":\"What is 173 multiplied by 219?\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":1536,\"temperature\":0,\"reasoning\":{\"max_tokens\":1024}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream" + }, + "body": ": OPENROUTER PROCESSING\n\n: OPENROUTER PROCESSING\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\"173\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"173\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\n: OPENROUTER PROCESSING\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\" × 219\\n\\n173 × 200 = 34,600\\n173 × 19 = 173 × 20 - 173 = 3,460 - 173 = 3,287\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" × 219\\n\\n173 × 200 = 34,600\\n173 × 19 = 173 × 20 - 173 = 3,460 - 173 = 3,287\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\"\\n\\n34,600 + 3,287 = 37,887\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"\\n\\n34,600 + 3,287 = 37,887\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"signature\":\"EtgCCosBCA8YAipA0W4viH3kgBs43Cl5ewwVBPXTQElvzfbA2TLF4iSbKy9ZZDCSDjjAlF3Bs4ELEnP3vrrTuTioC6OB380lXQdyIDIRY2xhdWRlLXNvbm5ldC00LTY4AEIIdGhpbmtpbmdaJDRjMGYwNDZmLTI1ZmQtNDVmYi1iZmIzLWEwOGE4ZTI0OWNhNxIMMiUlJC3x/5p5PuTwGgwlc8eipZyoM94BHwMiMO45uQx/ymeOjbugi7RDVPZ4jZXSIiEbVi2CD7zPjAK5fFQoVGP1HD55v9CER823JCp6Dg5Xb7Lrk6NUd1XN2KTKrttK7mATE+IBrDTFmor/1cNeg+9gjIbxM/jn/6L5HPmh3/esEVu24Q0IGLZVoE7cTgGgxsrceKMD71Jp2XQgIWD8ltsPfWw3gSc4p+z18UuPN6LuR0mHHENTnClHrAPnOrxbDIl4ZwZgMX8YAQ==\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"37887\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":null},\"finish_reason\":\"stop\",\"native_finish_reason\":\"end_turn\"}]}\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"service_tier\":\"default\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"stop\",\"native_finish_reason\":\"end_turn\"}],\"usage\":{\"prompt_tokens\":61,\"completion_tokens\":80,\"total_tokens\":141,\"cost\":0.001383,\"is_byok\":false,\"prompt_tokens_details\":{\"cached_tokens\":0,\"cache_write_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":0.001383,\"upstream_inference_prompt_cost\":0.000183,\"upstream_inference_completions_cost\":0.0012},\"completion_tokens_details\":{\"reasoning_tokens\":29,\"image_tokens\":0,\"audio_tokens\":0}}}\n\ndata: [DONE]\n\n" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/vercel-ai-gateway-reasoning.json b/packages/ai/test/fixtures/recordings/vercel-ai-gateway-reasoning.json new file mode 100644 index 0000000000..9cf18c3909 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/vercel-ai-gateway-reasoning.json @@ -0,0 +1,34 @@ +{ + "version": 1, + "metadata": { + "model": "anthropic/claude-sonnet-4.6", + "tags": [ + "prefix:openai-compatible-chat", + "provider:vercel-ai-gateway", + "protocol:openai-chat", + "reasoning" + ], + "name": "vercel-ai-gateway-reasoning", + "recordedAt": "2026-07-18T11:28:42.077Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://ai-gateway.vercel.sh/v1/chat/completions", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"anthropic/claude-sonnet-4.6\",\"messages\":[{\"role\":\"system\",\"content\":\"Think through the arithmetic, then reply with only the final integer.\"},{\"role\":\"user\",\"content\":\"What is 173 multiplied by 219?\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":1536,\"temperature\":0,\"reasoning\":{\"enabled\":true,\"max_tokens\":1024}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream" + }, + "body": "data: {\"id\":\"gen_01KXTFRJXJ8CKP0W3DKC3WC004\",\"object\":\"chat.completion.chunk\",\"created\":1784374121,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\"},\"logprobs\":null,\"finish_reason\":null}],\"system_fingerprint\":\"fp_zfth1fcyet\"}\n\ndata: {\"id\":\"gen_01KXTFRJXJ8CKP0W3DKC3WC004\",\"object\":\"chat.completion.chunk\",\"created\":1784374121,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning\":\"173\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"173\",\"signature\":\"\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"logprobs\":null,\"finish_reason\":null}],\"system_fingerprint\":\"fp_zfth1fcyet\"}\n\ndata: {\"id\":\"gen_01KXTFRJXJ8CKP0W3DKC3WC004\",\"object\":\"chat.completion.chunk\",\"created\":1784374121,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning\":\" × 219\\n\\n173 × 200 = 34,600\\n173 × 19 = 173 × 20 - 173 = 3,460\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" × 219\\n\\n173 × 200 = 34,600\\n173 × 19 = 173 × 20 - 173 = 3,460\",\"signature\":\"\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"logprobs\":null,\"finish_reason\":null}],\"system_fingerprint\":\"fp_zfth1fcyet\"}\n\ndata: {\"id\":\"gen_01KXTFRJXJ8CKP0W3DKC3WC004\",\"object\":\"chat.completion.chunk\",\"created\":1784374121,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning\":\" - 173 = 3,287\\n\\n34,600 + 3,287 = 37,887\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" - 173 = 3,287\\n\\n34,600 + 3,287 = 37,887\",\"signature\":\"\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"logprobs\":null,\"finish_reason\":null}],\"system_fingerprint\":\"fp_zfth1fcyet\"}\n\ndata: {\"id\":\"gen_01KXTFRJXJ8CKP0W3DKC3WC004\",\"object\":\"chat.completion.chunk\",\"created\":1784374121,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning\":\"\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"\",\"signature\":\"EtgCCosBCA8YAipA0W4viH3kgBs43Cl5ewwVBPXTQElvzfbA2TLF4iSbKy9ZZDCSDjjAlF3Bs4ELEnP3vrrTuTioC6OB380lXQdyIDIRY2xhdWRlLXNvbm5ldC00LTY4AEIIdGhpbmtpbmdaJDNiOTNhNWRkLTczMDItNDgzZi1hZWFlLTM2MjA3NTU0OGFlMRIMKkQaBkOSYB8cgQNiGgwDinVmv/KYSAKQEsUiMPSSFWvVpNuyEyYC8HlxrEZsb5KEEETuMjAI2hcC3m/NwGR+PC7chh2JWwD7wyK+eyp6OoF973UNMHAsWsKykCSJv60aXeOiDomxdfR9CRWVtaroVTkhtL2FPgplBPZYr75XvS0l6If3fqCPKevNE5WaOsSaXNfnMCCKGX7A0Pkhs4NazmCnntWGOsW7J03bAQKzIZ+c+Yr0rSwmwvuiocDqzsSE8bOeVv4352EYAQ==\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"logprobs\":null,\"finish_reason\":null}],\"system_fingerprint\":\"fp_zfth1fcyet\"}\n\ndata: {\"id\":\"gen_01KXTFRJXJ8CKP0W3DKC3WC004\",\"object\":\"chat.completion.chunk\",\"created\":1784374121,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"37,887\"},\"logprobs\":null,\"finish_reason\":null}],\"system_fingerprint\":\"fp_zfth1fcyet\"}\n\ndata: {\"id\":\"gen_01KXTFRJXJ8CKP0W3DKC3WC004\",\"object\":\"chat.completion.chunk\",\"created\":1784374121,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"provider_metadata\":{\"anthropic\":{\"usage\":{\"input_tokens\":61,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":81,\"service_tier\":\"standard\",\"inference_geo\":\"global\",\"output_tokens_details\":{\"thinking_tokens\":73}},\"cacheCreationInputTokens\":0,\"stopSequence\":null,\"iterations\":null,\"container\":null,\"contextManagement\":null},\"gateway\":{\"routing\":{\"originalModelId\":\"anthropic/claude-sonnet-4.6\",\"resolvedProvider\":\"anthropic\",\"fallbacksAvailable\":[\"vertexAnthropic\",\"bedrock\"],\"planningReasoning\":\"System credentials planned for: anthropic, vertexAnthropic, bedrock. Total execution order: anthropic(system) → vertexAnthropic(system) → bedrock(system)\",\"canonicalSlug\":\"anthropic/claude-sonnet-4.6\",\"finalProvider\":\"anthropic\",\"modelAttemptCount\":1,\"modelAttempts\":[{\"canonicalSlug\":\"anthropic/claude-sonnet-4.6\",\"success\":true,\"providerAttemptCount\":1,\"providerAttempts\":[{\"provider\":\"anthropic\",\"credentialType\":\"system\",\"success\":true,\"startTime\":1784374119500,\"endTime\":1784374122066,\"providerRequestId\":\"req_011Cd9SjHykKUqTQ1S8cAG17\",\"statusCode\":200,\"providerResponseId\":\"msg_011Cd9SjLvMmTFY8Pyj8n336\"}]}],\"totalProviderAttemptCount\":1},\"cost\":\"0.001398\",\"marketCost\":\"0.001398\",\"surchargeCost\":\"0\",\"gatewayCost\":\"0.001398\",\"inferenceCost\":\"0.001398\",\"inputInferenceCost\":\"0.000183\",\"outputInferenceCost\":\"0.001215\",\"generationId\":\"gen_01KXTFRJXJ8CKP0W3DKC3WC004\"}}},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":61,\"completion_tokens\":81,\"total_tokens\":142,\"cost\":0.001398,\"is_byok\":false,\"prompt_tokens_details\":{\"cached_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":null,\"upstream_inference_prompt_cost\":0,\"upstream_inference_completions_cost\":0},\"completion_tokens_details\":{\"reasoning_tokens\":26,\"reasoning_tokens_estimated\":true,\"image_tokens\":0},\"cache_creation_input_tokens\":0,\"market_cost\":0.001398,\"gateway_cost\":0.001398},\"system_fingerprint\":\"fp_zfth1fcyet\",\"generationId\":\"gen_01KXTFRJXJ8CKP0W3DKC3WC004\"}\n\ndata: [DONE]\n\n" + } + } + ] +} diff --git a/packages/llm/test/generate-object.test.ts b/packages/ai/test/generate-object.test.ts similarity index 100% rename from packages/llm/test/generate-object.test.ts rename to packages/ai/test/generate-object.test.ts diff --git a/packages/llm/test/lib/effect.ts b/packages/ai/test/lib/effect.ts similarity index 100% rename from packages/llm/test/lib/effect.ts rename to packages/ai/test/lib/effect.ts diff --git a/packages/llm/test/lib/http.ts b/packages/ai/test/lib/http.ts similarity index 100% rename from packages/llm/test/lib/http.ts rename to packages/ai/test/lib/http.ts diff --git a/packages/llm/test/lib/openai-chunks.ts b/packages/ai/test/lib/openai-chunks.ts similarity index 100% rename from packages/llm/test/lib/openai-chunks.ts rename to packages/ai/test/lib/openai-chunks.ts diff --git a/packages/llm/test/lib/sse.ts b/packages/ai/test/lib/sse.ts similarity index 100% rename from packages/llm/test/lib/sse.ts rename to packages/ai/test/lib/sse.ts diff --git a/packages/llm/test/lib/tool-runtime.ts b/packages/ai/test/lib/tool-runtime.ts similarity index 100% rename from packages/llm/test/lib/tool-runtime.ts rename to packages/ai/test/lib/tool-runtime.ts diff --git a/packages/llm/test/llm.test.ts b/packages/ai/test/llm.test.ts similarity index 100% rename from packages/llm/test/llm.test.ts rename to packages/ai/test/llm.test.ts diff --git a/packages/llm/test/prepare.test.ts b/packages/ai/test/prepare.test.ts similarity index 100% rename from packages/llm/test/prepare.test.ts rename to packages/ai/test/prepare.test.ts diff --git a/packages/ai/test/provider-error.test.ts b/packages/ai/test/provider-error.test.ts new file mode 100644 index 0000000000..cfad7a8726 --- /dev/null +++ b/packages/ai/test/provider-error.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, test } from "bun:test" +import { isContextOverflow } from "../src" +import { classifyProviderFailure } from "../src/provider-error" + +describe("provider error classification", () => { + test("classifies Z.AI GLM token limit messages as context overflow", () => { + expect(isContextOverflow("tokens in request more than max tokens allowed")).toBe(true) + }) + + test("classifies V1 plain-text rate limit fallbacks", () => { + expect( + [ + "Request rate increased too quickly", + "Rate limit exceeded, please try again later", + "Too many requests, please slow down", + ].map((message) => classifyProviderFailure({ message })._tag), + ).toEqual(["RateLimit", "RateLimit", "RateLimit"]) + }) + + test("classifies V1 JSON rate limit fallbacks", () => { + expect( + [ + '{"type":"error","error":{"type":"too_many_requests"}}', + '{"type":"error","error":{"code":"rate_limit_exceeded"}}', + '{"code":"bad_request","error":{"code":"rate_limit_exceeded"}}', + '{"type":"error","error":{"code":"unknown","type":"too_many_requests"}}', + ].map((message) => classifyProviderFailure({ message })._tag), + ).toEqual(["RateLimit", "RateLimit", "RateLimit", "RateLimit"]) + }) + + test("classifies V1 overloaded provider codes", () => { + expect( + ['{"code":"resource_exhausted"}', '{"code":"service_unavailable"}'].map( + (message) => classifyProviderFailure({ message })._tag, + ), + ).toEqual(["ProviderInternal", "ProviderInternal"]) + }) + + test("classifies nested provider codes when a top-level code is also present", () => { + expect( + [ + '{"code":"bad_request","error":{"code":"usage_not_included"}}', + '{"code":"bad_request","error":{"code":"server_error"}}', + '{"code":"bad_request","error":{"type":"invalid_request_error"}}', + ].map((message) => classifyProviderFailure({ message })._tag), + ).toEqual(["QuotaExceeded", "ProviderInternal", "InvalidRequest"]) + }) + + test("keeps unknown and malformed provider payloads non-retryable", () => { + expect(classifyProviderFailure({ message: '{"error":{"message":"no_kv_space"}}' })._tag).toBe("UnknownProvider") + expect(classifyProviderFailure({ message: '{"type":"error","error":{"code":123}}' })._tag).toBe("UnknownProvider") + expect(classifyProviderFailure({ message: "not-json" })._tag).toBe("UnknownProvider") + }) +}) diff --git a/packages/ai/test/provider-package.test.ts b/packages/ai/test/provider-package.test.ts new file mode 100644 index 0000000000..46d4dbb503 --- /dev/null +++ b/packages/ai/test/provider-package.test.ts @@ -0,0 +1,296 @@ +import { describe, expect, test } from "bun:test" +import { model } from "@opencode-ai/ai/providers/openai" + +describe("provider package entrypoints", () => { + test("semantic API aliases expose the same contract", async () => { + const modules = await Promise.all([ + import("@opencode-ai/ai/providers/openai"), + import("@opencode-ai/ai/providers/openai/responses"), + import("@opencode-ai/ai/providers/openai/chat"), + import("@opencode-ai/ai/providers/anthropic"), + import("@opencode-ai/ai/providers/anthropic-compatible"), + import("@opencode-ai/ai/providers/openai-compatible"), + import("@opencode-ai/ai/providers/openai-compatible/responses"), + import("@opencode-ai/ai/providers/amazon-bedrock"), + import("@opencode-ai/ai/providers/azure"), + import("@opencode-ai/ai/providers/azure/responses"), + import("@opencode-ai/ai/providers/azure/chat"), + import("@opencode-ai/ai/providers/google"), + import("@opencode-ai/ai/providers/google-vertex"), + import("@opencode-ai/ai/providers/google-vertex/gemini"), + import("@opencode-ai/ai/providers/google-vertex/chat"), + import("@opencode-ai/ai/providers/google-vertex/responses"), + import("@opencode-ai/ai/providers/google-vertex/messages"), + ]) + + for (const module of modules) expect(module.model).toBeFunction() + expect(modules[0].model).toBe(modules[1].model) + expect(modules[8].model).toBe(modules[9].model) + expect(modules[12].model).toBe(modules[13].model) + }) + + test("maps package settings onto the executable model", () => { + const selected = model("gpt-5", { + apiKey: "fixture", + baseURL: "https://api.openai.test/v1", + headers: { "x-application": "opencode" }, + body: { service_tier: "priority" }, + limits: { context: 200_000, output: 64_000 }, + unrelatedInheritedSetting: true, + }) + + expect(selected.route.id).toBe("openai-responses") + expect(selected.route.defaults.headers).toEqual({ "x-application": "opencode" }) + expect(selected.route.defaults.http?.body).toEqual({ service_tier: "priority" }) + expect(selected.route.defaults.limits).toEqual({ context: 200_000, output: 64_000 }) + }) + + test("selects transport without changing the semantic API", () => { + expect(model("gpt-5", { apiKey: "fixture" }).route.id).toBe("openai-responses") + expect(model("gpt-5", { apiKey: "fixture", transport: "websocket" }).route.id).toBe("openai-responses-websocket") + }) + + test("maps OpenAI-compatible Responses settings onto the executable model", async () => { + const OpenAICompatibleResponses = await import("@opencode-ai/ai/providers/openai-compatible/responses") + const selected = OpenAICompatibleResponses.model("custom-model", { + apiKey: "fixture", + baseURL: "https://responses.example.test/v1", + provider: "example", + headers: { "x-application": "opencode" }, + body: { service_tier: "priority" }, + limits: { context: 200_000, output: 64_000 }, + providerOptions: { openai: { reasoningEffort: "low", store: true } }, + }) + + expect(String(selected.provider)).toBe("example") + expect(selected.route.id).toBe("openai-compatible-responses") + expect(selected.route.endpoint).toMatchObject({ + baseURL: "https://responses.example.test/v1", + path: "/responses", + }) + expect(selected.route.defaults.headers).toEqual({ "x-application": "opencode" }) + expect(selected.route.defaults.http?.body).toEqual({ service_tier: "priority" }) + expect(selected.route.defaults.limits).toEqual({ context: 200_000, output: 64_000 }) + expect(selected.route.defaults.providerOptions).toEqual({ + openai: { reasoningEffort: "low", store: true }, + }) + }) + + test("maps Anthropic-compatible settings onto the executable model", async () => { + const AnthropicCompatible = await import("@opencode-ai/ai/providers/anthropic-compatible") + const selected = AnthropicCompatible.model("compatible-model", { + apiKey: "fixture", + baseURL: "https://messages.example.test/v1", + provider: "example", + headers: { "x-application": "opencode" }, + body: { metadata: { user_id: "user_1" } }, + limits: { context: 200_000, output: 64_000 }, + }) + + expect(String(selected.provider)).toBe("example") + expect(selected.route.id).toBe("anthropic-messages") + expect(selected.route.endpoint).toMatchObject({ + baseURL: "https://messages.example.test/v1", + path: "/messages", + }) + expect(selected.route.defaults.headers).toEqual({ "x-application": "opencode" }) + expect(selected.route.defaults.http?.body).toEqual({ metadata: { user_id: "user_1" } }) + expect(selected.route.defaults.limits).toEqual({ context: 200_000, output: 64_000 }) + }) + + test("requires an Anthropic-compatible base URL at runtime", async () => { + const AnthropicCompatible = await import("@opencode-ai/ai/providers/anthropic-compatible") + expect(() => + Reflect.apply(AnthropicCompatible.model, undefined, ["compatible-model", { apiKey: "fixture" }]), + ).toThrow("Anthropic-compatible providers require a baseURL") + }) + + test("rejects conflicting Anthropic-compatible auth settings at runtime", async () => { + const Anthropic = await import("@opencode-ai/ai/providers/anthropic") + const AnthropicCompatible = await import("@opencode-ai/ai/providers/anthropic-compatible") + expect(() => + Reflect.apply(AnthropicCompatible.model, undefined, [ + "compatible-model", + { + apiKey: "fixture", + authToken: "token", + baseURL: "https://messages.example.test/v1", + }, + ]), + ).toThrow("Anthropic-compatible apiKey cannot be combined with authToken") + expect(() => + Reflect.apply(Anthropic.model, undefined, ["claude-sonnet-4-6", { apiKey: "fixture", authToken: "token" }]), + ).toThrow("Anthropic apiKey cannot be combined with authToken") + }) + + test("maps legacy OpenAI organization and project settings to headers", () => { + const selected = model("gpt-5", { + apiKey: "fixture", + organization: "org_123", + project: "proj_123", + }) + + expect(selected.route.defaults.headers).toMatchObject({ + "OpenAI-Organization": "org_123", + "OpenAI-Project": "proj_123", + }) + }) + + test("selects Azure API entrypoints with the same model contract", async () => { + const Azure = await import("@opencode-ai/ai/providers/azure") + const AzureChat = await import("@opencode-ai/ai/providers/azure/chat") + const AzureResponses = await import("@opencode-ai/ai/providers/azure/responses") + const settings = { + apiKey: "fixture", + resourceName: "opencode-test", + headers: { "x-application": "opencode" }, + body: { service_tier: "priority" }, + limits: { context: 200_000, output: 64_000 }, + } + + const responses = AzureResponses.model("deployment", settings) + const chat = AzureChat.model("deployment", settings) + + expect(Azure.model("deployment", settings).route.id).toBe("azure-openai-responses") + expect(responses.route.id).toBe("azure-openai-responses") + expect(responses.route.endpoint.baseURL).toBe("https://opencode-test.openai.azure.com/openai/v1") + expect(responses.route.defaults.headers).toEqual({ "x-application": "opencode" }) + expect(responses.route.defaults.http?.body).toEqual({ service_tier: "priority" }) + expect(responses.route.defaults.limits).toEqual({ context: 200_000, output: 64_000 }) + expect(chat.route.id).toBe("azure-openai-chat") + }) + + test("maps Google package settings onto the Gemini model", async () => { + const Google = await import("@opencode-ai/ai/providers/google") + const selected = Google.model("gemini-2.5-flash", { + apiKey: "fixture", + baseURL: "https://generativelanguage.test/v1beta", + headers: { "x-application": "opencode" }, + body: { safetySettings: [] }, + limits: { context: 1_000_000, output: 65_536 }, + providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 1_024 } } }, + }) + + expect(selected.route.id).toBe("gemini") + expect(selected.route.endpoint.baseURL).toBe("https://generativelanguage.test/v1beta") + expect(selected.route.defaults.headers).toEqual({ "x-application": "opencode" }) + expect(selected.route.defaults.http?.body).toEqual({ safetySettings: [] }) + expect(selected.route.defaults.limits).toEqual({ context: 1_000_000, output: 65_536 }) + expect(selected.route.defaults.providerOptions).toEqual({ + gemini: { thinkingConfig: { thinkingBudget: 1_024 } }, + }) + }) + + test("selects Vertex entrypoints with the same model contract", async () => { + const GoogleVertex = await import("@opencode-ai/ai/providers/google-vertex") + const GoogleVertexGemini = await import("@opencode-ai/ai/providers/google-vertex/gemini") + const GoogleVertexChat = await import("@opencode-ai/ai/providers/google-vertex/chat") + const GoogleVertexResponses = await import("@opencode-ai/ai/providers/google-vertex/responses") + const GoogleVertexMessages = await import("@opencode-ai/ai/providers/google-vertex/messages") + const gemini = GoogleVertex.model("gemini-3.5-flash", { + apiKey: "fixture", + headers: { "x-application": "opencode" }, + body: { safetySettings: [] }, + limits: { context: 1_000_000, output: 65_536 }, + }) + const messages = GoogleVertexMessages.model("claude-sonnet-4-6", { + accessToken: "fixture", + location: "global", + project: "vertex-project", + }) + const chat = GoogleVertexChat.model("deepseek-ai/deepseek-v3.2-maas", { + accessToken: "fixture", + location: "global", + project: "vertex-project", + }) + const responses = GoogleVertexResponses.model("xai/grok-4.20-reasoning", { + accessToken: "fixture", + location: "global", + project: "vertex-project", + }) + + expect(GoogleVertexGemini.model).toBe(GoogleVertex.model) + expect(gemini.route.id).toBe("google-vertex-gemini") + expect(gemini.route.protocol).toBe("gemini") + expect(gemini.route.endpoint.baseURL).toBe("https://aiplatform.googleapis.com/v1/publishers/google") + expect(gemini.route.defaults.headers).toEqual({ "x-application": "opencode" }) + expect(gemini.route.defaults.http?.body).toEqual({ safetySettings: [] }) + expect(gemini.route.defaults.limits).toEqual({ context: 1_000_000, output: 65_536 }) + expect( + GoogleVertex.model("gemini-3.5-flash", { + accessToken: "fixture", + location: "eu", + project: "vertex-project", + }).route.endpoint.baseURL, + ).toBe("https://aiplatform.eu.rep.googleapis.com/v1beta1/projects/vertex-project/locations/eu/publishers/google") + expect(messages.route.id).toBe("google-vertex-messages") + expect(messages.route.protocol).toBe("anthropic-messages") + expect(messages.route.endpoint.baseURL).toBe( + "https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/publishers/anthropic/models", + ) + expect(chat.route.id).toBe("google-vertex-chat") + expect(chat.route.protocol).toBe("openai-chat") + expect(chat.route.endpoint).toMatchObject({ + baseURL: "https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/endpoints/openapi", + path: "/chat/completions", + }) + expect(responses.route.id).toBe("google-vertex-responses") + expect(responses.route.protocol).toBe("openai-responses") + expect(responses.route.endpoint).toMatchObject({ + baseURL: "https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/endpoints/openapi", + path: "/responses", + }) + expect(responses.route.defaults.providerOptions).toEqual({ openai: { store: false } }) + }) + + test("rejects conflicting Vertex auth settings at runtime", async () => { + const GoogleVertex = await import("@opencode-ai/ai/providers/google-vertex") + const GoogleVertexChat = await import("@opencode-ai/ai/providers/google-vertex/chat") + const GoogleVertexMessages = await import("@opencode-ai/ai/providers/google-vertex/messages") + const GoogleVertexResponses = await import("@opencode-ai/ai/providers/google-vertex/responses") + const Providers = await import("@opencode-ai/ai/providers") + expect(() => + Reflect.apply(GoogleVertex.model, undefined, [ + "gemini-3.5-flash", + { accessToken: "token", apiKey: "fixture", project: "vertex-project" }, + ]), + ).toThrow("Google Vertex apiKey cannot be combined with accessToken or auth") + const configured = Reflect.apply(GoogleVertex.configure, undefined, [ + { accessToken: "token", auth: {}, project: "vertex-project" }, + ]) + expect(() => configured.model("gemini-3.5-flash")).toThrow("Google Vertex accessToken cannot be combined with auth") + expect(() => + Reflect.apply(GoogleVertexMessages.model, undefined, [ + "claude-sonnet-4-6", + { apiKey: "fixture", project: "vertex-project" }, + ]), + ).toThrow("Google Vertex Messages does not support API keys") + expect(() => + Reflect.apply(Providers.GoogleVertexMessages.configure, undefined, [ + { apiKey: "fixture", project: "vertex-project" }, + ]), + ).toThrow("Google Vertex Messages does not support API keys") + expect(() => + Reflect.apply(GoogleVertexChat.model, undefined, [ + "deepseek-ai/deepseek-v3.2-maas", + { apiKey: "fixture", project: "vertex-project" }, + ]), + ).toThrow("Google Vertex Chat does not support API keys") + expect(() => + Reflect.apply(Providers.GoogleVertexChat.configure, undefined, [ + { apiKey: "fixture", project: "vertex-project" }, + ]), + ).toThrow("Google Vertex Chat does not support API keys") + expect(() => + Reflect.apply(GoogleVertexResponses.model, undefined, [ + "xai/grok-4.20-reasoning", + { apiKey: "fixture", project: "vertex-project" }, + ]), + ).toThrow("Google Vertex Responses does not support API keys") + expect(() => + Reflect.apply(Providers.GoogleVertexResponses.configure, undefined, [ + { apiKey: "fixture", project: "vertex-project" }, + ]), + ).toThrow("Google Vertex Responses does not support API keys") + }) +}) diff --git a/packages/llm/test/provider.types.ts b/packages/ai/test/provider.types.ts similarity index 100% rename from packages/llm/test/provider.types.ts rename to packages/ai/test/provider.types.ts diff --git a/packages/llm/test/provider/anthropic-messages-cache.recorded.test.ts b/packages/ai/test/provider/anthropic-messages-cache.recorded.test.ts similarity index 100% rename from packages/llm/test/provider/anthropic-messages-cache.recorded.test.ts rename to packages/ai/test/provider/anthropic-messages-cache.recorded.test.ts diff --git a/packages/llm/test/provider/anthropic-messages.recorded.test.ts b/packages/ai/test/provider/anthropic-messages.recorded.test.ts similarity index 100% rename from packages/llm/test/provider/anthropic-messages.recorded.test.ts rename to packages/ai/test/provider/anthropic-messages.recorded.test.ts diff --git a/packages/llm/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts similarity index 88% rename from packages/llm/test/provider/anthropic-messages.test.ts rename to packages/ai/test/provider/anthropic-messages.test.ts index 8989312958..2fd0628593 100644 --- a/packages/llm/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -57,6 +57,23 @@ describe("Anthropic Messages route", () => { }), ) + it.effect("lowers adaptive thinking settings with effort", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.updateRequest(request, { + providerOptions: { + anthropic: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" }, + }, + }), + ) + + expect(prepared.body).toMatchObject({ + thinking: { type: "adaptive", display: "summarized" }, + output_config: { effort: "low" }, + }) + }), + ) + it.effect("lowers chronological system updates natively for Claude Opus 4.8 with cache hints", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( @@ -467,23 +484,22 @@ describe("Anthropic Messages route", () => { }), ) - it.effect("emits provider-error events for mid-stream provider errors", () => + it.effect("fails with a typed provider error for stream error frames", () => Effect.gen(function* () { - const response = yield* LLMClient.generate(request).pipe( + const error = yield* LLMClient.generate(request).pipe( Effect.provide( fixedResponse(sseEvents({ type: "error", error: { type: "overloaded_error", message: "Overloaded" } })), ), + Effect.flip, ) - // Prefix the error type so consumers can distinguish overloads, rate - // limits, and quota errors without parsing the message string. - expect(response.events).toEqual([{ type: "provider-error", message: "overloaded_error: Overloaded" }]) + expect(error.reason).toMatchObject({ _tag: "ProviderInternal", message: "overloaded_error: Overloaded" }) }), ) it.effect("classifies prompt-too-long provider errors", () => Effect.gen(function* () { - const response = yield* LLMClient.generate(request).pipe( + const error = yield* LLMClient.generate(request).pipe( Effect.provide( fixedResponse( sseEvents({ @@ -492,35 +508,36 @@ describe("Anthropic Messages route", () => { }), ), ), + Effect.flip, ) - expect(response.events).toEqual([ - { - type: "provider-error", - message: "invalid_request_error: prompt is too long: 210000 tokens", - classification: "context-overflow", - }, - ]) + expect(error.reason).toMatchObject({ + _tag: "InvalidRequest", + message: "invalid_request_error: prompt is too long: 210000 tokens", + classification: "context-overflow", + }) }), ) it.effect("falls back to error type when no message is present", () => Effect.gen(function* () { - const response = yield* LLMClient.generate(request).pipe( + const error = yield* LLMClient.generate(request).pipe( Effect.provide(fixedResponse(sseEvents({ type: "error", error: { type: "overloaded_error", message: "" } }))), + Effect.flip, ) - expect(response.events).toEqual([{ type: "provider-error", message: "overloaded_error" }]) + expect(error.reason).toMatchObject({ _tag: "ProviderInternal", message: "overloaded_error" }) }), ) it.effect("falls back to a stable default when error payload is absent", () => Effect.gen(function* () { - const response = yield* LLMClient.generate(request).pipe( + const error = yield* LLMClient.generate(request).pipe( Effect.provide(fixedResponse(sseEvents({ type: "error" }))), + Effect.flip, ) - expect(response.events).toEqual([{ type: "provider-error", message: "Anthropic Messages stream error" }]) + expect(error.reason).toMatchObject({ _tag: "UnknownProvider", message: "Anthropic Messages stream error" }) }), ) @@ -600,6 +617,43 @@ describe("Anthropic Messages route", () => { }), ) + it.effect("preserves provider execution identity for malformed server tool input", () => + Effect.gen(function* () { + const body = sseEvents( + { type: "message_start", message: { usage: { input_tokens: 5 } } }, + { + type: "content_block_start", + index: 0, + content_block: { type: "server_tool_use", id: "srvtoolu_malformed", name: "web_search" }, + }, + { + type: "content_block_delta", + index: 0, + delta: { type: "input_json_delta", partial_json: '{"query":"partial' }, + }, + { type: "content_block_stop", index: 0 }, + { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } }, + ) + const response = yield* LLMClient.generate( + LLM.updateRequest(request, { + tools: [{ name: "web_search", description: "Web search", inputSchema: { type: "object" } }], + }), + ).pipe(Effect.provide(fixedResponse(body))) + + expect(response.events.find((event) => event.type === "tool-input-start")).toMatchObject({ + type: "tool-input-start", + id: "srvtoolu_malformed", + providerExecuted: true, + }) + expect(response.events.find((event) => event.type === "tool-input-error")).toMatchObject({ + type: "tool-input-error", + id: "srvtoolu_malformed", + raw: '{"query":"partial', + providerExecuted: true, + }) + }), + ) + it.effect("decodes web_search_tool_result_error as provider-executed error result", () => Effect.gen(function* () { const body = sseEvents( @@ -691,6 +745,55 @@ describe("Anthropic Messages route", () => { }), ) + it.effect("lowers synthetic server tool failures to valid Anthropic error payloads", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + id: "req_server_tool_input_error", + model, + messages: [ + Message.assistant([ + { + type: "tool-call", + id: "srvtoolu_malformed", + name: "web_search", + input: {}, + providerExecuted: true, + }, + { + type: "tool-result", + id: "srvtoolu_malformed", + name: "web_search", + result: { + type: "error", + value: { + error: { type: "provider.invalid-output" }, + raw: '{"query":"partial', + }, + }, + providerExecuted: true, + }, + ]), + ], + }), + ) + + expect(prepared.body.messages).toMatchObject([ + { + role: "assistant", + content: [ + { type: "server_tool_use", id: "srvtoolu_malformed", name: "web_search", input: {} }, + { + type: "web_search_tool_result", + tool_use_id: "srvtoolu_malformed", + content: { type: "web_search_tool_result_error", error_code: "invalid_tool_input" }, + }, + ], + }, + ]) + }), + ) + it.effect("rejects round-trip for unknown server tool names", () => Effect.gen(function* () { const error = yield* LLMClient.prepare( diff --git a/packages/llm/test/provider/bedrock-converse-cache.recorded.test.ts b/packages/ai/test/provider/bedrock-converse-cache.recorded.test.ts similarity index 100% rename from packages/llm/test/provider/bedrock-converse-cache.recorded.test.ts rename to packages/ai/test/provider/bedrock-converse-cache.recorded.test.ts diff --git a/packages/llm/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts similarity index 94% rename from packages/llm/test/provider/bedrock-converse.test.ts rename to packages/ai/test/provider/bedrock-converse.test.ts index 46657331a3..222981bb99 100644 --- a/packages/llm/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -303,6 +303,36 @@ describe("Bedrock Converse route", () => { }), ) + it.effect("emits malformed streamed tool input without a tool call", () => + Effect.gen(function* () { + const body = eventStreamBody( + ["messageStart", { role: "assistant" }], + [ + "contentBlockStart", + { + contentBlockIndex: 0, + start: { toolUse: { toolUseId: "tool_malformed", name: "lookup" } }, + }, + ], + ["contentBlockDelta", { contentBlockIndex: 0, delta: { toolUse: { input: '{"query":"partial' } } }], + ["contentBlockStop", { contentBlockIndex: 0 }], + ["messageStop", { stopReason: "tool_use" }], + ) + const response = yield* LLMClient.generate( + LLM.updateRequest(baseRequest, { + tools: [{ name: "lookup", description: "Lookup", inputSchema: { type: "object" } }], + }), + ).pipe(Effect.provide(fixedBytes(body))) + + expect(response.toolCalls).toEqual([]) + expect(response.events.find((event) => event.type === "tool-input-error")).toMatchObject({ + type: "tool-input-error", + id: "tool_malformed", + raw: '{"query":"partial', + }) + }), + ) + it.effect("decodes reasoning deltas", () => Effect.gen(function* () { const body = eventStreamBody( @@ -355,35 +385,31 @@ describe("Bedrock Converse route", () => { }), ) - it.effect("emits provider-error for throttlingException", () => + it.effect("classifies throttlingException as a rate limit", () => Effect.gen(function* () { const body = eventStreamBody( ["messageStart", { role: "assistant" }], ["throttlingException", { message: "Slow down" }], ) - const response = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body))) + const error = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body)), Effect.flip) - expect(response.events.find((event) => event.type === "provider-error")).toEqual({ - type: "provider-error", - message: "Slow down", - retryable: true, - }) + expect(error.reason).toMatchObject({ _tag: "RateLimit", message: "Slow down" }) }), ) it.effect("classifies input-too-long validation exceptions", () => Effect.gen(function* () { - const response = yield* LLMClient.generate(baseRequest).pipe( + const error = yield* LLMClient.generate(baseRequest).pipe( Effect.provide( fixedBytes(eventStreamBody(["validationException", { message: "Input is too long for requested model" }])), ), + Effect.flip, ) - expect(response.events.find((event) => event.type === "provider-error")).toEqual({ - type: "provider-error", + expect(error.reason).toMatchObject({ + _tag: "InvalidRequest", message: "Input is too long for requested model", classification: "context-overflow", - retryable: false, }) }), ) diff --git a/packages/llm/test/provider/cloudflare.test.ts b/packages/ai/test/provider/cloudflare.test.ts similarity index 100% rename from packages/llm/test/provider/cloudflare.test.ts rename to packages/ai/test/provider/cloudflare.test.ts diff --git a/packages/llm/test/provider/gemini-cache.recorded.test.ts b/packages/ai/test/provider/gemini-cache.recorded.test.ts similarity index 100% rename from packages/llm/test/provider/gemini-cache.recorded.test.ts rename to packages/ai/test/provider/gemini-cache.recorded.test.ts diff --git a/packages/llm/test/provider/gemini.test.ts b/packages/ai/test/provider/gemini.test.ts similarity index 95% rename from packages/llm/test/provider/gemini.test.ts rename to packages/ai/test/provider/gemini.test.ts index 1dc253c0ea..ff38a7aaa5 100644 --- a/packages/llm/test/provider/gemini.test.ts +++ b/packages/ai/test/provider/gemini.test.ts @@ -490,6 +490,35 @@ describe("Gemini route", () => { }), ) + it.effect("reports string-encoded function arguments without repairing them", () => + Effect.gen(function* () { + const body = sseEvents({ + candidates: [ + { + content: { + role: "model", + parts: [{ functionCall: { name: "lookup", args: '{"query":"partial' } }], + }, + finishReason: "STOP", + }, + ], + }) + const response = yield* LLMClient.generate( + LLM.updateRequest(request, { + tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + }), + ).pipe(Effect.provide(fixedResponse(body))) + + expect(response.toolCalls).toEqual([]) + expect(response.events.find((event) => event.type === "tool-input-error")).toMatchObject({ + type: "tool-input-error", + id: "tool_0", + name: "lookup", + raw: '{"query":"partial', + }) + }), + ) + it.effect("assigns unique ids to multiple streamed tool calls", () => Effect.gen(function* () { const body = sseEvents({ diff --git a/packages/llm/test/provider/golden.recorded.test.ts b/packages/ai/test/provider/golden.recorded.test.ts similarity index 92% rename from packages/llm/test/provider/golden.recorded.test.ts rename to packages/ai/test/provider/golden.recorded.test.ts index ef67c866d8..2ab4bd66c5 100644 --- a/packages/llm/test/provider/golden.recorded.test.ts +++ b/packages/ai/test/provider/golden.recorded.test.ts @@ -1,4 +1,5 @@ import * as Anthropic from "../../src/providers/anthropic" +import * as AnthropicCompatible from "../../src/providers/anthropic-compatible" import { CloudflareAIGateway, CloudflareWorkersAI } from "../../src/providers/cloudflare" import * as Google from "../../src/providers/google" import * as OpenAI from "../../src/providers/openai" @@ -12,12 +13,16 @@ const openAI = OpenAI.configure({ }) const openAIChat = openAI.chat("gpt-4o-mini") const openAIResponses = openAI.responses("gpt-5.5") -const openAIResponsesWebSocket = openAI.responsesWebSocket("gpt-4.1-mini") const anthropic = Anthropic.configure({ apiKey: process.env.ANTHROPIC_API_KEY ?? "fixture", }) const anthropicHaiku = anthropic.model("claude-haiku-4-5-20251001") const anthropicOpus = anthropic.model("claude-opus-4-7") +const minimax = AnthropicCompatible.configure({ + apiKey: process.env.MINIMAX_API_KEY ?? "fixture", + baseURL: "https://api.minimax.io/anthropic/v1", + provider: "minimax", +}).model("MiniMax-M3") const google = Google.configure({ apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY ?? "fixture" }) const gemini = google.model("gemini-2.5-flash") const xai = XAI.configure({ apiKey: process.env.XAI_API_KEY ?? "fixture" }) @@ -89,14 +94,6 @@ describeRecordedGoldenScenarios([ { id: "image-tool-result", temperature: false, maxTokens: 40 }, ], }, - { - name: "OpenAI Responses WebSocket gpt-4.1-mini", - prefix: "openai-responses-websocket", - model: openAIResponsesWebSocket, - transport: "websocket", - requires: ["OPENAI_API_KEY"], - scenarios: ["tool-loop"], - }, { name: "Anthropic Haiku 4.5", prefix: "anthropic-messages", @@ -117,6 +114,15 @@ describeRecordedGoldenScenarios([ { id: "image-tool-result", temperature: false, maxTokens: 40 }, ], }, + { + name: "MiniMax M3 Anthropic-compatible", + prefix: "anthropic-compatible-messages", + protocol: "anthropic-messages", + model: minimax, + requires: ["MINIMAX_API_KEY"], + options: { redact: { allowRequestHeaders: ["anthropic-version"] } }, + scenarios: ["text", "tool-call", "tool-loop"], + }, { name: "Gemini 2.5 Flash", prefix: "gemini", diff --git a/packages/ai/test/provider/google-vertex.test.ts b/packages/ai/test/provider/google-vertex.test.ts new file mode 100644 index 0000000000..395d983ffe --- /dev/null +++ b/packages/ai/test/provider/google-vertex.test.ts @@ -0,0 +1,246 @@ +import { describe, expect } from "bun:test" +import { Effect } from "effect" +import { HttpClientRequest } from "effect/unstable/http" +import { LLM } from "../../src" +import { GoogleVertex, GoogleVertexChat, GoogleVertexMessages, GoogleVertexResponses } from "../../src/providers" +import { LLMClient } from "../../src/route" +import { it } from "../lib/effect" +import { dynamicResponse } from "../lib/http" +import { deltaChunk, finishChunk } from "../lib/openai-chunks" +import { sseEvents } from "../lib/sse" + +describe("Google Vertex providers", () => { + it.effect("sends Gemini requests to the global Vertex endpoint", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + model: GoogleVertex.configure({ + accessToken: "vertex-token", + location: "global", + project: "vertex-project", + }).model("gemini-3.5-flash"), + prompt: "Say hello.", + }), + ).pipe( + Effect.provide( + dynamicResponse((input) => + Effect.gen(function* () { + const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie) + expect(request.url).toBe( + "https://aiplatform.googleapis.com/v1beta1/projects/vertex-project/locations/global/publishers/google/models/gemini-3.5-flash:streamGenerateContent?alt=sse", + ) + expect(request.headers.get("authorization")).toBe("Bearer vertex-token") + expect(yield* Effect.promise(() => request.json())).toMatchObject({ + contents: [{ role: "user", parts: [{ text: "Say hello." }] }], + }) + return input.respond( + sseEvents({ + candidates: [ + { + content: { role: "model", parts: [{ text: "Hello." }] }, + finishReason: "STOP", + }, + ], + }), + { headers: { "content-type": "text/event-stream" } }, + ) + }), + ), + ), + ) + + expect(response.text).toBe("Hello.") + }), + ) + + it.effect("projects Anthropic Messages onto the Vertex raw-predict API", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + model: GoogleVertexMessages.configure({ + accessToken: "vertex-token", + location: "eu", + project: "vertex-project", + }).model("claude-sonnet-4-6"), + prompt: "Say hello.", + }), + ).pipe( + Effect.provide( + dynamicResponse((input) => + Effect.gen(function* () { + const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie) + expect(request.url).toBe( + "https://aiplatform.eu.rep.googleapis.com/v1/projects/vertex-project/locations/eu/publishers/anthropic/models/claude-sonnet-4-6:streamRawPredict", + ) + expect(request.headers.get("authorization")).toBe("Bearer vertex-token") + expect(request.headers.get("anthropic-version")).toBeNull() + const body = yield* Effect.promise(() => request.json()) + expect(body).toMatchObject({ + anthropic_version: "vertex-2023-10-16", + messages: [{ role: "user", content: [{ type: "text", text: "Say hello." }] }], + stream: true, + }) + expect(body).not.toHaveProperty("model") + return input.respond( + sseEvents( + { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } }, + { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Hello." } }, + { type: "content_block_stop", index: 0 }, + { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 2 } }, + { type: "message_stop" }, + ), + { headers: { "content-type": "text/event-stream" } }, + ) + }), + ), + ), + ) + + expect(response.text).toBe("Hello.") + }), + ) + + it.effect("sends MaaS requests through Vertex Chat Completions", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + model: GoogleVertexChat.configure({ + accessToken: "vertex-token", + location: "global", + project: "vertex-project", + }).model("deepseek-ai/deepseek-v3.2-maas"), + prompt: "Say hello.", + }), + ).pipe( + Effect.provide( + dynamicResponse((input) => + Effect.gen(function* () { + const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie) + expect(request.url).toBe( + "https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/endpoints/openapi/chat/completions", + ) + expect(request.headers.get("authorization")).toBe("Bearer vertex-token") + expect(yield* Effect.promise(() => request.json())).toMatchObject({ + model: "deepseek-ai/deepseek-v3.2-maas", + messages: [{ role: "user", content: "Say hello." }], + stream: true, + stream_options: { include_usage: true }, + }) + return input.respond(sseEvents(deltaChunk({ content: "Hello." }), finishChunk("stop")), { + headers: { "content-type": "text/event-stream" }, + }) + }), + ), + ), + ) + + expect(response.text).toBe("Hello.") + }), + ) + + it.effect("sends Grok requests through Vertex Responses", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + model: GoogleVertexResponses.configure({ + accessToken: "vertex-token", + location: "global", + project: "vertex-project", + }).model("xai/grok-4.20-reasoning"), + prompt: "Say hello.", + }), + ).pipe( + Effect.provide( + dynamicResponse((input) => + Effect.gen(function* () { + const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie) + expect(request.url).toBe( + "https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/endpoints/openapi/responses", + ) + expect(request.headers.get("authorization")).toBe("Bearer vertex-token") + expect(yield* Effect.promise(() => request.json())).toMatchObject({ + model: "xai/grok-4.20-reasoning", + input: [{ role: "user", content: [{ type: "input_text", text: "Say hello." }] }], + store: false, + stream: true, + }) + return input.respond( + sseEvents( + { type: "response.output_text.delta", item_id: "msg_1", delta: "Hello." }, + { type: "response.completed", response: { id: "resp_1" } }, + ), + { headers: { "content-type": "text/event-stream" } }, + ) + }), + ), + ), + ) + + expect(response.text).toBe("Hello.") + }), + ) + + it.effect("protects the Vertex Messages API version from body overlays", () => + Effect.gen(function* () { + const error = yield* LLMClient.prepare( + LLM.request({ + model: GoogleVertexMessages.configure({ + accessToken: "vertex-token", + http: { body: { anthropic_version: "wrong" } }, + project: "vertex-project", + }).model("claude-sonnet-4-6"), + prompt: "Say hello.", + }), + ).pipe(Effect.flip) + + expect(error.message).toContain("http.body cannot overlay protocol-owned field(s): anthropic_version") + }), + ) + + it.effect("routes tuned Gemini models through their deployed endpoint", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + model: GoogleVertex.configure({ + accessToken: "vertex-token", + location: "us-central1", + project: "vertex-project", + }).model("endpoints/1234567890"), + prompt: "Say hello.", + }), + ).pipe( + Effect.provide( + dynamicResponse((input) => + Effect.gen(function* () { + const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie) + expect(request.url).toBe( + "https://us-central1-aiplatform.googleapis.com/v1beta1/projects/vertex-project/locations/us-central1/endpoints/1234567890:streamGenerateContent?alt=sse", + ) + return input.respond( + sseEvents({ + candidates: [ + { + content: { role: "model", parts: [{ text: "Hello." }] }, + finishReason: "STOP", + }, + ], + }), + { headers: { "content-type": "text/event-stream" } }, + ) + }), + ), + ), + ) + + expect(response.text).toBe("Hello.") + }), + ) + + it.effect("rejects tuned Gemini models in express mode", () => + Effect.sync(() => { + expect(() => GoogleVertex.configure({ apiKey: "fixture" }).model("endpoints/1234567890")).toThrow( + "Google Vertex tuned models do not support Express Mode API keys", + ) + }), + ) +}) diff --git a/packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts b/packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts new file mode 100644 index 0000000000..32ef7820b8 --- /dev/null +++ b/packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts @@ -0,0 +1,67 @@ +import { describe, expect } from "bun:test" +import { Effect } from "effect" +import { LLM, LLMEvent } from "../../src" +import * as OpenAICompatible from "../../src/providers/openai-compatible" +import * as OpenRouter from "../../src/providers/openrouter" +import { LLMClient } from "../../src/route" +import { recordedTests } from "../recorded-test" + +const cases = [ + { + name: "OpenRouter", + model: OpenRouter.configure({ + apiKey: process.env.OPENROUTER_API_KEY ?? "fixture", + providerOptions: { openrouter: { reasoning: { max_tokens: 1024 } } }, + }).model("anthropic/claude-sonnet-4.6"), + requires: ["OPENROUTER_API_KEY"], + cassette: "openrouter-reasoning", + }, + { + name: "Vercel AI Gateway", + model: OpenAICompatible.configure({ + provider: "vercel-ai-gateway", + baseURL: "https://ai-gateway.vercel.sh/v1", + apiKey: process.env.AI_GATEWAY_API_KEY ?? "fixture", + http: { body: { reasoning: { enabled: true, max_tokens: 1024 } } }, + }).model("anthropic/claude-sonnet-4.6"), + requires: ["AI_GATEWAY_API_KEY"], + cassette: "vercel-ai-gateway-reasoning", + }, +] as const + +for (const item of cases) { + const recorded = recordedTests({ + prefix: "openai-compatible-chat", + provider: item.model.provider, + protocol: "openai-chat", + requires: item.requires, + tags: ["reasoning"], + metadata: { model: item.model.id }, + }) + + describe(`${item.name} reasoning recorded`, () => { + recorded.effect.with( + "streams scalar reasoning", + { cassette: item.cassette }, + () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + model: item.model, + system: "Think through the arithmetic, then reply with only the final integer.", + prompt: "What is 173 multiplied by 219?", + generation: { maxTokens: 1536, temperature: 0 }, + }), + ) + + expect(response.text.replaceAll(",", "").trim()).toBe("37887") + expect(response.reasoning.length).toBeGreaterThan(0) + expect(response.events.some(LLMEvent.is.reasoningDelta)).toBe(true) + expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({ + openai: { reasoningField: "reasoning" }, + }) + }), + 30_000, + ) + }) +} diff --git a/packages/llm/test/provider/openai-chat.test.ts b/packages/ai/test/provider/openai-chat.test.ts similarity index 87% rename from packages/llm/test/provider/openai-chat.test.ts rename to packages/ai/test/provider/openai-chat.test.ts index b736dc9dd3..64f6456445 100644 --- a/packages/llm/test/provider/openai-chat.test.ts +++ b/packages/ai/test/provider/openai-chat.test.ts @@ -98,12 +98,26 @@ describe("OpenAI Chat route", () => { LLM.request({ model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).chat("gpt-4o-mini"), prompt: "think", - providerOptions: { openai: { reasoningEffort: "low" } }, + providerOptions: { openai: { reasoningEffort: "max" } }, }), ) expect(prepared.body.store).toBe(false) - expect(prepared.body.reasoning_effort).toBe("low") + expect(prepared.body.reasoning_effort).toBe("max") + }), + ) + + it.effect("passes through custom OpenAI-compatible reasoning effort strings", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + prompt: "think", + providerOptions: { openai: { reasoningEffort: "experimental" } }, + }), + ) + + expect(prepared.body.reasoning_effort).toBe("experimental") }), ) @@ -526,29 +540,33 @@ describe("OpenAI Chat route", () => { }), ) - it.effect("parses OpenAI-compatible reasoning content deltas", () => + it.effect("parses and replays OpenAI-compatible reasoning fields", () => Effect.gen(function* () { - const body = sseEvents( - { choices: [{ delta: { reasoning_content: "thinking" } }] }, - { choices: [{ delta: { content: "Hello" } }] }, - { choices: [{ delta: {}, finish_reason: "stop" }] }, - ) + const fields = ["reasoning_content", "reasoning", "reasoning_text"] as const + for (const field of fields) { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { choices: [{ delta: { [field]: "thinking" } }] }, + { choices: [{ delta: { content: "Hello" } }] }, + { choices: [{ delta: {}, finish_reason: "stop" }] }, + ), + ), + ), + ) - const response = yield* LLMClient.generate(request).pipe(Effect.provide(fixedResponse(body))) + expect(response.reasoning).toBe("thinking") + expect(response.text).toBe("Hello") + expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({ + openai: { reasoningField: field }, + }) - expect(response.reasoning).toBe("thinking") - expect(response.text).toBe("Hello") - expect(response.events).toMatchObject([ - { type: "step-start", index: 0 }, - { type: "reasoning-start", id: "reasoning-0" }, - { type: "reasoning-delta", id: "reasoning-0", text: "thinking" }, - { type: "reasoning-end", id: "reasoning-0" }, - { type: "text-start", id: "text-0" }, - { type: "text-delta", id: "text-0", text: "Hello" }, - { type: "text-end", id: "text-0" }, - { type: "step-finish", index: 0, reason: "stop" }, - { type: "finish", reason: "stop" }, - ]) + const replay = yield* LLMClient.prepare( + LLM.request({ model, messages: [response.message] }), + ) + expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", [field]: "thinking" }]) + } }), ) @@ -588,7 +606,34 @@ describe("OpenAI Chat route", () => { }), ) - it.effect("does not finalize streamed tool calls without a finish reason", () => + it.effect("preserves a valid parallel call when another call is malformed", () => + Effect.gen(function* () { + const body = sseEvents( + deltaChunk({ + role: "assistant", + tool_calls: [ + { index: 0, id: "call_valid", function: { name: "lookup", arguments: '{"query":"weather"}' } }, + { index: 1, id: "call_malformed", function: { name: "lookup", arguments: '{"query":"partial' } }, + ], + }), + deltaChunk({}, "tool_calls"), + ) + const response = yield* LLMClient.generate( + LLM.updateRequest(request, { + tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + }), + ).pipe(Effect.provide(fixedResponse(body))) + + expect( + response.events.filter((event) => event.type === "tool-call" || event.type === "tool-input-error"), + ).toMatchObject([ + { type: "tool-call", id: "call_valid", input: { query: "weather" } }, + { type: "tool-input-error", id: "call_malformed", raw: '{"query":"partial' }, + ]) + }), + ) + + it.effect("fails a streamed tool call when the provider ends without a finish reason", () => Effect.gen(function* () { const body = sseEvents( deltaChunk({ @@ -600,8 +645,11 @@ describe("OpenAI Chat route", () => { const input = LLM.updateRequest(request, { tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], }) - const events = Array.from( - yield* LLMClient.stream(input).pipe(Stream.runCollect, Effect.provide(fixedResponse(body))), + const events: LLMEvent[] = [] + const streamError = yield* LLMClient.stream(input).pipe( + Stream.runForEach((event) => Effect.sync(() => events.push(event))), + Effect.flip, + Effect.provide(fixedResponse(body)), ) const error = yield* LLMClient.generate(input).pipe(Effect.provide(fixedResponse(body)), Effect.flip) @@ -612,6 +660,8 @@ describe("OpenAI Chat route", () => { { type: "tool-input-delta", id: "call_1", name: "lookup", text: ':"weather"}' }, ]) expect(events.filter(LLMEvent.is.toolCall)).toEqual([]) + expect(streamError.reason).toMatchObject({ _tag: "InvalidProviderOutput" }) + expect(streamError.message).toContain("Provider stream ended without a terminal finish event") expect(error.message).toContain("Provider stream ended without a terminal finish event") }), ) diff --git a/packages/llm/test/provider/openai-compatible-chat.test.ts b/packages/ai/test/provider/openai-compatible-chat.test.ts similarity index 100% rename from packages/llm/test/provider/openai-compatible-chat.test.ts rename to packages/ai/test/provider/openai-compatible-chat.test.ts diff --git a/packages/ai/test/provider/openai-compatible-responses.test.ts b/packages/ai/test/provider/openai-compatible-responses.test.ts new file mode 100644 index 0000000000..a43acb7683 --- /dev/null +++ b/packages/ai/test/provider/openai-compatible-responses.test.ts @@ -0,0 +1,53 @@ +import { describe, expect } from "bun:test" +import { Effect } from "effect" +import { LLM } from "../../src" +import { configure } from "../../src/providers/openai-compatible-responses" +import { OpenAICompatibleResponses } from "../../src/protocols/openai-compatible-responses" +import { OpenAIResponses } from "../../src/protocols/openai-responses" +import { LLMClient } from "../../src/route" +import { it } from "../lib/effect" + +describe("OpenAI-compatible Responses route", () => { + it.effect("reuses the OpenAI Responses protocol for a configured deployment", () => + Effect.gen(function* () { + expect(OpenAICompatibleResponses.route.body).toBe(OpenAIResponses.protocol.body) + expect(OpenAICompatibleResponses.route.transport).toBe(OpenAIResponses.httpTransport) + + const model = configure({ + apiKey: "test-key", + baseURL: "https://responses.example.test/v1", + provider: "example", + }).model("example-model") + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + system: "You are concise.", + prompt: "Say hello.", + }), + ) + + expect(prepared.route).toBe("openai-compatible-responses") + expect(prepared.protocol).toBe("openai-responses") + expect(prepared.model).toMatchObject({ + id: "example-model", + provider: "example", + route: { + id: "openai-compatible-responses", + endpoint: { + baseURL: "https://responses.example.test/v1", + path: "/responses", + }, + }, + }) + expect(prepared.body).toEqual({ + model: "example-model", + input: [ + { role: "system", content: "You are concise." }, + { role: "user", content: [{ type: "input_text", text: "Say hello." }] }, + ], + store: false, + stream: true, + }) + }), + ) +}) diff --git a/packages/llm/test/provider/openai-responses-cache.recorded.test.ts b/packages/ai/test/provider/openai-responses-cache.recorded.test.ts similarity index 100% rename from packages/llm/test/provider/openai-responses-cache.recorded.test.ts rename to packages/ai/test/provider/openai-responses-cache.recorded.test.ts diff --git a/packages/llm/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts similarity index 86% rename from packages/llm/test/provider/openai-responses.test.ts rename to packages/ai/test/provider/openai-responses.test.ts index cd8bad51af..b3bc39128c 100644 --- a/packages/llm/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -69,6 +69,16 @@ describe("OpenAI Responses route", () => { }), ) + it.effect("passes through custom OpenAI reasoning effort strings", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.updateRequest(request, { providerOptions: { openai: { reasoningEffort: "experimental" } } }), + ) + + expect(prepared.body.reasoning).toEqual({ effort: "experimental" }) + }), + ) + it.effect("omits unsupported semantic service tiers", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( @@ -754,6 +764,35 @@ describe("OpenAI Responses route", () => { }), ) + // OpenAI's documented stream orders output text within one message item; no + // provider-valid same-kind overlap is evidenced, so done boundaries close it. + it.effect("closes sequential output messages before starting the next", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { type: "response.output_text.delta", item_id: "msg_1", delta: "First" }, + { type: "response.output_text.done", item_id: "msg_1" }, + { type: "response.output_text.delta", item_id: "msg_2", delta: "Second" }, + { type: "response.output_item.done", item: { type: "message", id: "msg_2" } }, + { type: "response.completed", response: { id: "resp_1" } }, + ), + ), + ), + ) + + expect(response.events.filter((event) => event.type.startsWith("text-"))).toEqual([ + { type: "text-start", id: "msg_1" }, + { type: "text-delta", id: "msg_1", text: "First" }, + { type: "text-end", id: "msg_1" }, + { type: "text-start", id: "msg_2" }, + { type: "text-delta", id: "msg_2", text: "Second" }, + { type: "text-end", id: "msg_2" }, + ]) + }), + ) + it.effect("parses reasoning summary stream fixtures", () => Effect.gen(function* () { const body = sseEvents( @@ -1199,6 +1238,7 @@ describe("OpenAI Responses route", () => { type: "tool-input-end", id: "call_1", name: "lookup", + input: '{"query":"weather"}', providerMetadata: { openai: { itemId: "item_1" } }, }, { @@ -1220,6 +1260,87 @@ describe("OpenAI Responses route", () => { }), ) + it.effect("emits malformed function input when output_item.done arrives without added", () => + Effect.gen(function* () { + const body = sseEvents( + { + type: "response.output_item.done", + item: { + type: "function_call", + id: "item_malformed", + call_id: "call_malformed", + name: "lookup", + arguments: '{"query":"partial', + }, + }, + { type: "response.completed", response: { usage: { input_tokens: 5, output_tokens: 1 } } }, + ) + const response = yield* LLMClient.generate( + LLM.updateRequest(request, { + tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + }), + ).pipe(Effect.provide(fixedResponse(body))) + + expect( + response.events.filter( + (event) => + event.type === "tool-input-start" || event.type === "tool-input-end" || event.type === "tool-input-error", + ), + ).toMatchObject([ + { type: "tool-input-start", id: "call_malformed", name: "lookup" }, + { + type: "tool-input-end", + id: "call_malformed", + name: "lookup", + input: '{"query":"partial', + }, + { + type: "tool-input-error", + id: "call_malformed", + name: "lookup", + raw: '{"query":"partial', + }, + ]) + }), + ) + + it.effect("uses malformed final function input instead of valid streamed deltas", () => + Effect.gen(function* () { + const body = sseEvents( + { + type: "response.output_item.added", + item: { type: "function_call", id: "item_1", call_id: "call_1", name: "lookup", arguments: "" }, + }, + { type: "response.function_call_arguments.delta", item_id: "item_1", delta: '{"query":"valid"}' }, + { + type: "response.output_item.done", + item: { + type: "function_call", + id: "item_1", + call_id: "call_1", + name: "lookup", + arguments: '{"query":"partial', + }, + }, + { type: "response.completed", response: { usage: { input_tokens: 5, output_tokens: 1 } } }, + ) + const response = yield* LLMClient.generate( + LLM.updateRequest(request, { + tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], + }), + ).pipe(Effect.provide(fixedResponse(body))) + + expect(response.events.find((event) => event.type === "tool-input-end")).toMatchObject({ + type: "tool-input-end", + input: '{"query":"partial', + }) + expect(response.events.find((event) => event.type === "tool-input-error")).toMatchObject({ + type: "tool-input-error", + raw: '{"query":"partial', + }) + }), + ) + it.effect("decodes web_search_call as provider-executed tool-call + tool-result", () => Effect.gen(function* () { const item = { @@ -1329,37 +1450,37 @@ describe("OpenAI Responses route", () => { }), ) - it.effect("emits provider-error events for mid-stream provider errors", () => + it.effect("fails with a typed rate limit for provider error frames", () => Effect.gen(function* () { - const response = yield* LLMClient.generate(request).pipe( + const error = yield* LLMClient.generate(request).pipe( Effect.provide(fixedResponse(sseEvents({ type: "error", code: "rate_limit_exceeded", message: "Slow down" }))), + Effect.flip, ) - // Prefix the code so consumers see the failure mode, not just the - // sometimes-generic provider message. The bare message alone meant - // production errors like rate limits were indistinguishable from - // unrelated stream failures. - expect(response.events).toEqual([{ type: "provider-error", message: "rate_limit_exceeded: Slow down" }]) + expect(error).toBeInstanceOf(LLMError) + expect(error.reason).toMatchObject({ _tag: "RateLimit", message: "rate_limit_exceeded: Slow down" }) }), ) it.effect("falls back to error code when no message is present", () => Effect.gen(function* () { - const response = yield* LLMClient.generate(request).pipe( + const error = yield* LLMClient.generate(request).pipe( Effect.provide(fixedResponse(sseEvents({ type: "error", code: "internal_error" }))), + Effect.flip, ) - expect(response.events).toEqual([{ type: "provider-error", message: "internal_error" }]) + expect(error.reason).toMatchObject({ _tag: "ProviderInternal", message: "internal_error" }) }), ) it.effect("falls back to error code when message is empty", () => Effect.gen(function* () { - const response = yield* LLMClient.generate(request).pipe( + const error = yield* LLMClient.generate(request).pipe( Effect.provide(fixedResponse(sseEvents({ type: "error", code: "internal_error", message: "" }))), + Effect.flip, ) - expect(response.events).toEqual([{ type: "provider-error", message: "internal_error" }]) + expect(error.reason).toMatchObject({ _tag: "ProviderInternal", message: "internal_error" }) }), ) @@ -1369,7 +1490,7 @@ describe("OpenAI Responses route", () => { // "OpenAI Responses response failed" string, hiding the real cause. it.effect("surfaces response.failed details from response.error", () => Effect.gen(function* () { - const response = yield* LLMClient.generate(request).pipe( + const error = yield* LLMClient.generate(request).pipe( Effect.provide( fixedResponse( sseEvents({ @@ -1381,15 +1502,19 @@ describe("OpenAI Responses route", () => { }), ), ), + Effect.flip, ) - expect(response.events).toEqual([{ type: "provider-error", message: "server_error: Upstream model unavailable" }]) + expect(error.reason).toMatchObject({ + _tag: "ProviderInternal", + message: "server_error: Upstream model unavailable", + }) }), ) it.effect("surfaces response.failed code when no nested message is present", () => Effect.gen(function* () { - const response = yield* LLMClient.generate(request).pipe( + const error = yield* LLMClient.generate(request).pipe( Effect.provide( fixedResponse( sseEvents({ @@ -1398,20 +1523,21 @@ describe("OpenAI Responses route", () => { }), ), ), + Effect.flip, ) - expect(response.events).toEqual([{ type: "provider-error", message: "invalid_prompt" }]) + expect(error.reason).toMatchObject({ _tag: "InvalidRequest", message: "invalid_prompt" }) }), ) - it.effect("surfaces error event details even when they arrive nested under response.error", () => + it.effect("surfaces error event details nested under response.error", () => Effect.gen(function* () { // Some OpenAI-compatible proxies and older SDK versions wrap the // top-level error fields into a nested `response.error` payload // when they bubble up an HTTP error as an SSE `error` event. Honour // both shapes so the user still sees the underlying cause instead // of the catch-all string. - const response = yield* LLMClient.generate(request).pipe( + const error = yield* LLMClient.generate(request).pipe( Effect.provide( fixedResponse( sseEvents({ @@ -1420,35 +1546,96 @@ describe("OpenAI Responses route", () => { }), ), ), + Effect.flip, ) - expect(response.events).toEqual([ - { - type: "provider-error", - message: "context_length_exceeded: prompt too long", - classification: "context-overflow", - }, - ]) + expect(error.reason).toMatchObject({ + _tag: "InvalidRequest", + message: "context_length_exceeded: prompt too long", + classification: "context-overflow", + }) + }), + ) + + it.effect("surfaces error event details nested under error", () => + Effect.gen(function* () { + const error = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents({ + type: "error", + sequence_number: 2, + error: { + type: "invalid_request_error", + code: "context_length_exceeded", + message: "prompt too long", + param: "input", + }, + }), + ), + ), + Effect.flip, + ) + + expect(error.reason).toMatchObject({ + _tag: "InvalidRequest", + message: "context_length_exceeded: prompt too long", + classification: "context-overflow", + }) + }), + ) + + it.effect("accepts nullable fields in spec-compliant error events", () => + Effect.gen(function* () { + const error = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents({ + type: "error", + code: null, + message: "Something went wrong", + param: null, + sequence_number: 1, + }), + ), + ), + Effect.flip, + ) + + expect(error.reason).toMatchObject({ _tag: "UnknownProvider", message: "Something went wrong" }) + }), + ) + + it.effect("falls back to a stable default when error is null", () => + Effect.gen(function* () { + const error = yield* LLMClient.generate(request).pipe( + Effect.provide(fixedResponse(sseEvents({ type: "error", error: null }))), + Effect.flip, + ) + + expect(error.reason).toMatchObject({ _tag: "UnknownProvider", message: "OpenAI Responses stream error" }) }), ) it.effect("falls back to a stable default when both error and response are absent", () => Effect.gen(function* () { - const response = yield* LLMClient.generate(request).pipe( + const error = yield* LLMClient.generate(request).pipe( Effect.provide(fixedResponse(sseEvents({ type: "error" }))), + Effect.flip, ) - expect(response.events).toEqual([{ type: "provider-error", message: "OpenAI Responses stream error" }]) + expect(error.reason).toMatchObject({ _tag: "UnknownProvider", message: "OpenAI Responses stream error" }) }), ) it.effect("falls back to a stable default when response.failed has no error payload", () => Effect.gen(function* () { - const response = yield* LLMClient.generate(request).pipe( + const error = yield* LLMClient.generate(request).pipe( Effect.provide(fixedResponse(sseEvents({ type: "response.failed", response: { id: "resp_failed_3" } }))), + Effect.flip, ) - expect(response.events).toEqual([{ type: "provider-error", message: "OpenAI Responses response failed" }]) + expect(error.reason).toMatchObject({ _tag: "UnknownProvider", message: "OpenAI Responses response failed" }) }), ) diff --git a/packages/llm/test/provider/openrouter.test.ts b/packages/ai/test/provider/openrouter.test.ts similarity index 100% rename from packages/llm/test/provider/openrouter.test.ts rename to packages/ai/test/provider/openrouter.test.ts diff --git a/packages/llm/test/recorded-golden.ts b/packages/ai/test/recorded-golden.ts similarity index 96% rename from packages/llm/test/recorded-golden.ts rename to packages/ai/test/recorded-golden.ts index 540662b299..404a5028b2 100644 --- a/packages/llm/test/recorded-golden.ts +++ b/packages/ai/test/recorded-golden.ts @@ -28,7 +28,7 @@ type TargetInput = { readonly transport?: Transport readonly prefix?: string readonly tags?: ReadonlyArray - readonly metadata?: Record + readonly metadata?: HttpRecorder.CassetteMetadata readonly options?: HttpRecorder.RecorderOptions readonly scenarios: ReadonlyArray } @@ -43,7 +43,7 @@ const defaultPrefix = (target: TargetInput) => { const metadata = (target: TargetInput) => ({ provider: target.model.provider, - protocol: target.protocol, + ...(target.protocol ? { protocol: target.protocol } : {}), route: target.model.route.id, transport: target.transport ?? "http", model: target.model.id, diff --git a/packages/llm/test/recorded-runner.ts b/packages/ai/test/recorded-runner.ts similarity index 93% rename from packages/llm/test/recorded-runner.ts rename to packages/ai/test/recorded-runner.ts index 97d9b03f54..904a9366ac 100644 --- a/packages/llm/test/recorded-runner.ts +++ b/packages/ai/test/recorded-runner.ts @@ -1,3 +1,4 @@ +import type { HttpRecorder } from "@opencode-ai/http-recorder" import { test, type TestOptions } from "bun:test" import { Effect, type Layer } from "effect" import { testEffect } from "./lib/effect" @@ -11,7 +12,7 @@ export type RecordedGroupOptions = { readonly protocol?: string readonly requires?: ReadonlyArray readonly tags?: ReadonlyArray - readonly metadata?: Record + readonly metadata?: HttpRecorder.CassetteMetadata } export type RecordedCaseOptions = { @@ -21,7 +22,7 @@ export type RecordedCaseOptions = { readonly protocol?: string readonly requires?: ReadonlyArray readonly tags?: ReadonlyArray - readonly metadata?: Record + readonly metadata?: HttpRecorder.CassetteMetadata } export const recordedEffectGroup = < @@ -36,7 +37,7 @@ export const recordedEffectGroup = < readonly layer: (input: { readonly cassette: string readonly tags: ReadonlyArray - readonly metadata: Record + readonly metadata: HttpRecorder.CassetteMetadata readonly recording: boolean readonly options: Options readonly caseOptions: CaseOptions diff --git a/packages/llm/test/recorded-scenarios.ts b/packages/ai/test/recorded-scenarios.ts similarity index 100% rename from packages/llm/test/recorded-scenarios.ts rename to packages/ai/test/recorded-scenarios.ts diff --git a/packages/llm/test/recorded-test.ts b/packages/ai/test/recorded-test.ts similarity index 72% rename from packages/llm/test/recorded-test.ts rename to packages/ai/test/recorded-test.ts index 669b8de5c5..da40085779 100644 --- a/packages/llm/test/recorded-test.ts +++ b/packages/ai/test/recorded-test.ts @@ -1,11 +1,8 @@ -import { NodeFileSystem } from "@effect/platform-node" import { HttpRecorder } from "@opencode-ai/http-recorder" -import { HttpRecorderInternal } from "@opencode-ai/http-recorder/internal" import { Layer } from "effect" -import { FetchHttpClient } from "effect/unstable/http" import * as path from "node:path" import { fileURLToPath } from "node:url" -import { LLMClient, RequestExecutor } from "../src/route" +import { LLMClient, RequestExecutor, WebSocketExecutor } from "../src/route" import type { Service as LLMClientService } from "../src/route/client" import type { Service as RequestExecutorService } from "../src/route/executor" import type { Service as WebSocketExecutorService } from "../src/route/transport/websocket" @@ -14,7 +11,6 @@ import { type RecordedCaseOptions as RunnerCaseOptions, type RecordedGroupOptions, } from "./recorded-runner" -import { webSocketCassetteLayer } from "./recorded-websocket" const __dirname = path.dirname(fileURLToPath(import.meta.url)) const FIXTURES_DIR = path.resolve(__dirname, "fixtures", "recordings") @@ -64,31 +60,27 @@ export const recordedTests = (options: RecordedTestsOptions) => recordedEffectGroup({ duplicateLabel: "recorded cassette", options, - cassetteExists: (cassette) => HttpRecorderInternal.hasCassetteSync(cassette, { directory: FIXTURES_DIR }), + cassetteExists: (cassette) => HttpRecorder.hasCassetteSync(cassette, { directory: FIXTURES_DIR }), layer: ({ cassette, metadata, options, caseOptions, recording }) => { const recorderOptions = mergeOptions(options.options, caseOptions.options) const recorderMetadata = { ...recorderOptions?.metadata, ...metadata, } - const mode = recording ? "record" : "replay" - const cassetteService = HttpRecorderInternal.Cassette.fileSystem({ directory: FIXTURES_DIR }).pipe( - Layer.provide(NodeFileSystem.layer), - ) + if (recording) { + if (process.env.CI !== undefined) throw new Error("Unset CI before recording HTTP cassettes") + HttpRecorder.removeCassetteSync(cassette, { directory: FIXTURES_DIR }) + } const requestExecutor = RequestExecutor.layer.pipe( Layer.provide( - HttpRecorderInternal.recordingLayer(cassette, { - mode, + HttpRecorder.layerFetch(cassette, { + ...recorderOptions, + directory: FIXTURES_DIR, metadata: recorderMetadata, - redactor: HttpRecorderInternal.Redactor.make(recorderOptions?.redact), - match: recorderOptions?.match, - }).pipe(Layer.provide(FetchHttpClient.layer)), + }), ), ) - const deps = Layer.mergeAll( - requestExecutor, - webSocketCassetteLayer(cassette, { metadata: recorderMetadata, mode }), - ) - return Layer.mergeAll(deps, LLMClient.layer.pipe(Layer.provide(deps))).pipe(Layer.provide(cassetteService)) + const deps = Layer.mergeAll(requestExecutor, WebSocketExecutor.layer) + return Layer.mergeAll(deps, LLMClient.layer.pipe(Layer.provide(deps))) }, }) diff --git a/packages/llm/test/recorded-utils.ts b/packages/ai/test/recorded-utils.ts similarity index 100% rename from packages/llm/test/recorded-utils.ts rename to packages/ai/test/recorded-utils.ts diff --git a/packages/llm/test/response.test.ts b/packages/ai/test/response.test.ts similarity index 100% rename from packages/llm/test/response.test.ts rename to packages/ai/test/response.test.ts diff --git a/packages/llm/test/route.test.ts b/packages/ai/test/route.test.ts similarity index 100% rename from packages/llm/test/route.test.ts rename to packages/ai/test/route.test.ts diff --git a/packages/llm/test/schema.test.ts b/packages/ai/test/schema.test.ts similarity index 100% rename from packages/llm/test/schema.test.ts rename to packages/ai/test/schema.test.ts diff --git a/packages/llm/test/tool-runtime.test.ts b/packages/ai/test/tool-runtime.test.ts similarity index 100% rename from packages/llm/test/tool-runtime.test.ts rename to packages/ai/test/tool-runtime.test.ts diff --git a/packages/llm/test/tool-schema-projection.test.ts b/packages/ai/test/tool-schema-projection.test.ts similarity index 100% rename from packages/llm/test/tool-schema-projection.test.ts rename to packages/ai/test/tool-schema-projection.test.ts diff --git a/packages/llm/test/tool-stream.test.ts b/packages/ai/test/tool-stream.test.ts similarity index 68% rename from packages/llm/test/tool-stream.test.ts rename to packages/ai/test/tool-stream.test.ts index b005d2666c..27721952fe 100644 --- a/packages/llm/test/tool-stream.test.ts +++ b/packages/ai/test/tool-stream.test.ts @@ -57,13 +57,61 @@ describe("ToolStream", () => { expect(finished).toEqual({ tools: {}, events: [ - { type: "tool-input-end", id: "call_1", name: "lookup" }, + { type: "tool-input-end", id: "call_1", name: "lookup", input: '{"query":"final"}' }, { type: "tool-call", id: "call_1", name: "lookup", input: { query: "final" } }, ], }) }), ) + it.effect("emits malformed tool input with stable identity and raw arguments", () => + Effect.gen(function* () { + const tools = ToolStream.start(ToolStream.empty(), 0, { + id: "call_1", + name: "lookup", + input: '{"query":"partial', + }) + const finished = yield* ToolStream.finish(ADAPTER, tools, 0) + + expect(finished).toMatchObject({ + tools: {}, + events: [ + { type: "tool-input-end", id: "call_1", name: "lookup" }, + { + type: "tool-input-error", + id: "call_1", + name: "lookup", + raw: '{"query":"partial', + message: "Invalid JSON input for test-route tool call lookup", + }, + ], + }) + }), + ) + + it.effect("preserves valid sibling calls when one input is malformed", () => + Effect.gen(function* () { + const first = ToolStream.start(ToolStream.empty(), 0, { + id: "call_valid", + name: "lookup", + input: '{"query":"weather"}', + }) + const tools = ToolStream.start(first, 1, { + id: "call_malformed", + name: "lookup", + input: '{"query":"partial', + }) + const finished = yield* ToolStream.finishAll(ADAPTER, tools) + + expect( + finished.events.filter((event) => event.type === "tool-call" || event.type === "tool-input-error"), + ).toMatchObject([ + { type: "tool-call", id: "call_valid", input: { query: "weather" } }, + { type: "tool-input-error", id: "call_malformed", raw: '{"query":"partial' }, + ]) + }), + ) + it.effect("preserves providerExecuted and clears all tools", () => Effect.gen(function* () { const first: ToolStream.State = ToolStream.start(ToolStream.empty(), 0, { diff --git a/packages/llm/test/tool.types.ts b/packages/ai/test/tool.types.ts similarity index 100% rename from packages/llm/test/tool.types.ts rename to packages/ai/test/tool.types.ts diff --git a/packages/ai/tsconfig.build.json b/packages/ai/tsconfig.build.json new file mode 100644 index 0000000000..2e9770e8d3 --- /dev/null +++ b/packages/ai/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": false, + "noEmit": false + } +} diff --git a/packages/ai/tsconfig.json b/packages/ai/tsconfig.json new file mode 100644 index 0000000000..dfd9a832c7 --- /dev/null +++ b/packages/ai/tsconfig.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "@tsconfig/bun/tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "noUncheckedIndexedAccess": false + }, + "include": ["src"] +} diff --git a/packages/app/V1_API_MIGRATION.md b/packages/app/V1_API_MIGRATION.md deleted file mode 100644 index 2850f10740..0000000000 --- a/packages/app/V1_API_MIGRATION.md +++ /dev/null @@ -1,220 +0,0 @@ -# V1 API Migration Checklist - -The app is currently hybrid. In this document, V1 refers to the legacy unprefixed server APIs used by `@opencode-ai/sdk/v2`, despite the SDK package name. - -## Events - -- [x] Replace `GET /global/event` with `GET /api/event`. - - `src/context/server-sdk.tsx` -- [x] Reduce current granular session and message events into the existing app projections. - - `src/context/server-session-v2-reducer.ts` - - `src/context/server-session.ts` -- [ ] Remove transitional session event dependencies: `session.created`, `session.updated`, `session.diff`, `session.status`, `session.idle`, and `session.error`. - - `src/context/global-sync/event-reducer.ts` - - `src/context/server-session.ts` - - `src/context/notification.tsx` - - `src/pages/session/usage-exceeded-dialogs.tsx` -- [ ] Remove legacy message event compatibility: `message.updated`, `message.removed`, `message.part.updated`, `message.part.removed`, and `message.part.delta`. - - `src/context/global-sync/event-reducer.ts` - - `src/context/server-session.ts` -- [x] Adapt current permission and question events to the existing request model. - - `src/context/global-sync/event-reducer.ts` - - `src/context/permission.tsx` -- [x] Consume current file watcher events. - - `src/context/file.tsx` -- [x] Consume current VCS events. - - `src/context/global-sync/event-reducer.ts` - - `src/pages/session.tsx` -- [x] Consume current `pty.exited` events. - - `src/context/terminal.tsx` -- [ ] Migrate LSP and reference events. - - `src/context/global-sync/event-reducer.ts` - -## Sessions - -- [x] Replace `GET /session/status` with one server-scoped `GET /api/session/active` snapshot plus V2 execution events. - - `src/context/server-sync.tsx` -- [x] Migrate session listing from `GET /session`. - - `src/context/server-sync.tsx` - - `src/context/directory-sync.ts` - - `src/pages/layout.tsx` -- [x] Migrate the remaining direct session read from `GET /session/:sessionID`. - - `src/components/titlebar.tsx` -- [x] Migrate session updates from `PATCH /session/:sessionID`. - - `src/context/directory-sync.ts` - - `src/context/layout.tsx` - - `src/pages/home.tsx` - - `src/pages/layout.tsx` - - `src/pages/session/timeline/message-timeline.tsx` - - `src/components/titlebar-tab-nav.tsx` - - Renames use `POST /api/session/:sessionID/rename`; archival uses `POST /api/session/:sessionID/archive`. -- [x] Migrate session deletion from `DELETE /session/:sessionID`. - - `src/pages/session/timeline/message-timeline.tsx` -- [x] Remove session diff loading from `GET /session/:sessionID/diff`. - - Historical Session diffs remain unavailable until the current API defines their snapshot semantics. -- [x] Migrate abort from `POST /session/:sessionID/abort`. - - `src/components/prompt-input/submit.ts` - - `src/pages/session/use-session-commands.tsx` - - `src/pages/session.tsx` -- [x] Migrate revert and unrevert from `POST /session/:sessionID/revert` and `POST /session/:sessionID/unrevert`. - - `src/pages/session/use-session-commands.tsx` - - `src/pages/session.tsx` -- [x] Replace `POST /session/:sessionID/summarize` with the current compact API. - - `src/pages/session/use-session-commands.tsx` -- [x] Migrate slash commands from `POST /session/:sessionID/command`. - - `src/components/prompt-input/submit.ts` -- [x] Migrate shell execution from `POST /session/:sessionID/shell`. - - `src/components/prompt-input/submit.ts` -- [x] Migrate session fork from `POST /session/:sessionID/fork`. - - `src/components/dialog-fork.tsx` -- [ ] Migrate sharing from `POST /session/:sessionID/share` and `DELETE /session/:sessionID/share`. - - `src/pages/session/use-session-commands.tsx` - - `src/pages/session/timeline/message-timeline.tsx` - - Blocked: the current API has no sharing contract or implementation. - -## Session Compatibility Fallbacks - -These calls are retained as fallback adapters. The current production path supplies the current session and message APIs. - -- [ ] Remove fallback `GET /session/:sessionID` after compatibility support is unnecessary. - - `src/context/server-session.ts` -- [ ] Remove fallback `GET /session/:sessionID/message` after compatibility support is unnecessary. - - `src/context/server-session.ts` -- [ ] Remove fallback `GET /session/:sessionID/message/:messageID` after compatibility support is unnecessary. - - `src/context/server-session.ts` - -## Filesystem - -- [ ] Migrate file listing from `GET /file`. - - `src/context/file.tsx` -- [ ] Migrate file reads from `GET /file/content`. - - `src/context/file.tsx` - - `src/pages/session/review-tab.tsx` - - `src/pages/session/v2/review-panel-v2.tsx` -- [x] Migrate path discovery from `GET /path` to `GET /api/path`. - - `src/context/global-sync/bootstrap.ts` - - `src/components/dialog-select-directory.tsx` - - `src/components/dialog-select-directory-v2.tsx` - -## Projects And Worktrees - -- [x] Migrate project listing from `GET /project` to `GET /api/project`. - - `src/context/global-sync/bootstrap.ts` -- [x] Migrate the current project lookup from `GET /project/current` to `GET /api/project/current`. - - `src/context/global-sync/bootstrap.ts` -- [ ] Migrate Git initialization from `POST /project/git/init`. - - `src/pages/session.tsx` -- [x] Migrate project updates from `PATCH /project/:projectID` to `PATCH /api/project/:projectID`. - - `src/context/layout.tsx` - - `src/components/edit-project.ts` - - `src/pages/layout.tsx` -- [ ] Migrate experimental worktree listing, creation, removal, and reset from `/experimental/worktree`. - - `src/pages/layout.tsx` - - `src/components/prompt-input/submit.ts` - - Listing now uses `GET /api/project/:projectID/directories`; create, removal, and reset remain. -- [ ] Migrate instance disposal from `POST /instance/dispose`. - - `src/pages/layout.tsx` - -## VCS - -- [x] Migrate repository information from `GET /vcs` to `GET /api/vcs`. - - `src/context/global-sync/bootstrap.ts` -- [x] Migrate diffs from `GET /vcs/diff` to `GET /api/vcs/diff`. - - `src/pages/session.tsx` -- [x] Migrate status from `GET /vcs/status` to `GET /api/vcs/status`. - - `src/pages/layout.tsx` - -## Configuration And Authentication - -- [ ] Migrate global configuration reads from `GET /global/config`. - - `src/context/global-sync/bootstrap.ts` -- [ ] Migrate directory configuration reads from `GET /config`. - - `src/context/global-sync/bootstrap.ts` -- [ ] Migrate global configuration updates from `PATCH /global/config`. - - `src/context/server-sync.tsx` -- [x] Migrate provider authentication method discovery from `GET /provider/auth` to `GET /api/integration/:integrationID`. - - `src/components/dialog-connect-provider.tsx` -- [x] Migrate built-in provider OAuth authorization and callbacks to `/api/integration/:integrationID/connect/oauth/*`. - - `src/components/dialog-connect-provider.tsx` -- [ ] Migrate remaining credentials from `PUT /auth/:providerID` and `DELETE /auth/:providerID`. - - Built-in provider key connections now use `POST /api/integration/:integrationID/connect/key`. - - `src/components/dialog-connect-provider.tsx` - - `src/components/dialog-custom-provider.tsx` - - `src/components/settings-providers.tsx` - - `src/components/settings-v2/providers.tsx` -- [ ] Migrate global disposal from `POST /global/dispose`. - - `src/components/dialog-connect-provider.tsx` - - `src/components/settings-providers.tsx` - - `src/components/settings-v2/providers.tsx` - -## Permissions And Questions - -- [x] Migrate permission listing from `GET /permission` to `GET /api/permission/request`. - - `src/context/global-sync/bootstrap.ts` - - `src/context/permission.tsx` -- [x] Migrate permission responses from `/session/:sessionID/permissions/:permissionID`. - - `src/context/permission.tsx` - - `src/pages/session/composer/session-composer-state.ts` -- [x] Migrate question listing from `GET /question` to `GET /api/question/request`. - - `src/context/global-sync/bootstrap.ts` -- [x] Migrate question replies and rejections from `/question/:requestID/*` to `/api/session/:sessionID/question/:requestID/*`. - - `src/pages/session/composer/session-question-dock.tsx` - -## Commands, MCP, LSP, And References - -- [x] Migrate command listing from `GET /command` to `GET /api/command`. - - `src/context/global-sync/bootstrap.ts` - - `src/context/server-sync.tsx` -- [x] Migrate MCP listing, connection, and disconnection from `/mcp` to `/api/mcp`. - - `src/context/server-sync.tsx` -- [ ] Replace legacy MCP authentication with the Integration OAuth workflow. - - `src/context/server-sync.tsx` -- [x] Migrate experimental resource listing from `GET /experimental/resource` to `GET /api/mcp/resource`. - - `src/context/server-sync.tsx` -- [ ] Migrate LSP status from `GET /lsp`. - - `src/context/server-sync.tsx` -- [x] Move `GET /api/reference` off the legacy generated SDK transport. - - `src/context/global-sync/bootstrap.ts` - -## Search - -- [x] Migrate global session search from `GET /experimental/session` to `GET /api/session`. - - `src/components/command-palette.ts` - - `src/components/dialog-command-palette-v2.tsx` - -## PTY And Terminal - -- [x] Migrate PTY creation, reads, updates, and deletion from `/pty` to `/api/pty`. - - `src/context/terminal.tsx` - - `src/components/terminal.tsx` -- [x] Migrate shell listing from `GET /pty/shells` to `GET /api/pty/shells`. - - `src/components/settings-general.tsx` - - `src/components/settings-v2/general.tsx` -- [x] Migrate connection tokens from `POST /pty/:ptyID/connect-token` to `POST /api/pty/:ptyID/connect-token`. - - `src/components/terminal.tsx` -- [x] Migrate the direct WebSocket connection from `/pty/:ptyID/connect` to `/api/pty/:ptyID/connect`. - - `src/components/terminal.tsx` - -## Legacy Types And Adapters - -These are not V1 network requests, but they keep the UI coupled to V1 data contracts. - -- [ ] Replace the current-session-to-legacy-session adapter. - - `src/utils/session.ts` -- [ ] Replace the current-message-to-legacy-message-and-part adapter. - - `src/utils/session-message.ts` -- [ ] Replace current agent, provider, and model adapters to legacy SDK structures. - - `src/context/global-sync/utils.ts` -- [ ] Replace legacy `Session`, `Message`, `Part`, `PermissionRequest`, `QuestionRequest`, `Project`, `FileNode`, `FileDiffInfo`, and `Event` types throughout app state and rendering. -- [ ] Remove the `@opencode-ai/sdk` runtime dependency after all legacy calls and types are gone. - - `package.json` - -## Test Infrastructure - -- [ ] Replace V1 endpoint mocks with current API mocks. - - `e2e/utils/mock-server.ts` -- [x] Replace `/global/event` and `/event` interception with current event transport handling. - - `e2e/utils/sse-transport.ts` -- [ ] Replace `SessionV1` and legacy SDK fixtures in timeline performance tests. - - `e2e/performance/timeline-stability/fixture.ts` -- [ ] Remove remaining legacy SDK type fixtures from unit and browser tests. diff --git a/packages/app/e2e/performance/timeline-stability/file-matrix.spec.ts b/packages/app/e2e/performance/timeline-stability/file-matrix.spec.ts index e0f0d72233..0690f02706 100644 --- a/packages/app/e2e/performance/timeline-stability/file-matrix.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/file-matrix.spec.ts @@ -20,7 +20,7 @@ const profiles = [ { name: "edit", tool: "edit", input: { filePath: "src/edit.ts" } }, { name: "multi patch", - tool: "apply_patch", + tool: "patch", input: { files: ["src/a.ts", "src/b.ts", "src/old.ts", "src/moved.ts"] }, }, ] as const diff --git a/packages/app/e2e/performance/timeline-stability/file-mutation.spec.ts b/packages/app/e2e/performance/timeline-stability/file-mutation.spec.ts index 798bf0df3b..f411339adb 100644 --- a/packages/app/e2e/performance/timeline-stability/file-mutation.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/file-mutation.spec.ts @@ -25,7 +25,7 @@ test("adds patch files incrementally without resetting outer expansion", async ( userMessage(), assistantMessage( [ - toolPart(patchID, "apply_patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }), + toolPart(patchID, "patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }), textPart(followingID, "Following incremental patch"), ], { completed: false }, @@ -49,7 +49,7 @@ test("adds patch files incrementally without resetting outer expansion", async ( partUpdated( toolPart( patchID, - "apply_patch", + "patch", "running", { files: [first.filePath, second.filePath] }, { metadata: { files: [first, second] } }, @@ -61,7 +61,7 @@ test("adds patch files incrementally without resetting outer expansion", async ( partUpdated( toolPart( patchID, - "apply_patch", + "patch", "completed", { files: [first.filePath, second.filePath, third.filePath] }, { metadata: { files: [first, second, third] } }, diff --git a/packages/app/e2e/performance/timeline-stability/fixture.ts b/packages/app/e2e/performance/timeline-stability/fixture.ts index df67da5a66..5095d95db0 100644 --- a/packages/app/e2e/performance/timeline-stability/fixture.ts +++ b/packages/app/e2e/performance/timeline-stability/fixture.ts @@ -97,7 +97,6 @@ export async function setupTimeline( locale?: string deviceScaleFactor?: number seedHistory?: boolean - protocol?: "v1" | "v2" } = {}, ) { const sessions = input.sessions ?? [session()] @@ -115,7 +114,6 @@ export async function setupTimeline( retry: input.eventRetry ?? 20, }) await mockOpenCodeServer(page, { - protocol: input.protocol, directory, project: project(), provider: provider(), diff --git a/packages/app/e2e/performance/timeline-stability/tools.spec.ts b/packages/app/e2e/performance/timeline-stability/tools.spec.ts index d28fdaa65f..d26968e1cb 100644 --- a/packages/app/e2e/performance/timeline-stability/tools.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/tools.spec.ts @@ -33,11 +33,9 @@ test.describe("timeline tool state stability", () => { } const names = { webfetch: "webfetch", websearch: "websearch", task: "task", skill: "skill", custom: "mcp_probe" } const questionID = "prt_state_question" - const todoID = "prt_state_todo" const initial = [ ...ids.map((id) => toolPart(`prt_state_${id}`, names[id], "pending", inputs[id])), toolPart(questionID, "question", "pending", questionInput()), - toolPart(todoID, "todowrite", "pending", { todos: [{ content: "Hidden", status: "pending" }] }), textPart("prt_state_following", "Following lightweight tools"), ] const childID = "ses_timeline_child" @@ -49,7 +47,6 @@ test.describe("timeline tool state stability", () => { await timeline.send(status("busy"), 120) for (const id of ids) await timeline.waitForPart(`prt_state_${id}`) await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0) - await expect(page.locator(`[data-timeline-part-id="${todoID}"]`)).toHaveCount(0) const regionIDs = [ "prt_state_webfetch", @@ -105,7 +102,6 @@ test.describe("timeline tool state stability", () => { ]), ) await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toContainText("Keep it stable") - await expect(page.locator(`[data-timeline-part-id="${todoID}"]`)).toHaveCount(0) await expect( page.locator(`a[href$="/session/${childID}"]`, { has: page.locator('[data-component="task-tool-card"]') }), ).toBeVisible() diff --git a/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts b/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts index 838af17c93..2a214831da 100644 --- a/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts +++ b/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts @@ -41,12 +41,7 @@ const assistants = Array.from({ length: 14 }, (_, index) => { const messages = [user, ...assistants] const target = fixture.sessions.find((session) => session.id === fixture.targetID)! const lastID = userID -const lastAssistant = assistants.at(-1)! -const lastPart = lastAssistant.parts.at(-1)! -const lastPartID = - lastPart.type === "tool" - ? lastPart.id - : `${lastAssistant.info.id}:${lastPart.type}:${lastAssistant.parts.filter((part) => part.type === lastPart.type).length - 1}` +const lastPartID = assistants.at(-1)!.parts.at(-1)!.id benchmark("hydrates an orphaned latest turn after a cold session click", async ({ browser, report }, testInfo) => { benchmark.setTimeout(180_000) @@ -112,25 +107,9 @@ async function trial(page: Page, mode: ParentHydrationBenchmarkMode) { return { items: items.slice(start, end), cursor: start > 0 ? items[start]!.info.id : undefined } }, }) - await page.route(`**/session/${fixture.targetID}`, (route) => { - const current = new URL(route.request().url()).pathname.startsWith("/api/") - return route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify( - current - ? { - data: { - ...target, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - location: { directory: target.directory }, - }, - } - : target, - ), - }) - }) + await page.route(`**/session/${fixture.targetID}`, (route) => + route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(target) }), + ) await installStressSessionTabs(page, { sessionIDs: [fixture.sourceID] }) await page.goto(stressSessionHref(fixture.sourceID)) await expectSessionTitle(page, fixture.expected.sourceTitle) @@ -165,8 +144,8 @@ async function trial(page: Page, mode: ParentHydrationBenchmarkMode) { parent: requests.filter((request) => request.type === "parent").length, } if (mode === "candidate") { - expect(requestCounts.parent).toBe(0) - expect(historyGates).toBe(0) + expect(requestCounts.parent).toBe(1) + expect(historyGates).toBe(1) } return { metrics, requestCounts, historyGateCount: historyGates } } diff --git a/packages/app/e2e/performance/timeline/session-timeline-benchmark.fixture.ts b/packages/app/e2e/performance/timeline/session-timeline-benchmark.fixture.ts index a86a55cff2..a22d5cc331 100644 --- a/packages/app/e2e/performance/timeline/session-timeline-benchmark.fixture.ts +++ b/packages/app/e2e/performance/timeline/session-timeline-benchmark.fixture.ts @@ -295,7 +295,7 @@ function performanceTurn(index: number) { messageID: assistantID, type: "tool", callID: `call_0000_${suffix}_patch`, - tool: "apply_patch", + tool: "patch", state: { status: "completed", input: { patchText: realisticPatch(index) }, diff --git a/packages/app/e2e/performance/timeline/session-timeline-stress.fixture.ts b/packages/app/e2e/performance/timeline/session-timeline-stress.fixture.ts index 529081a1d9..2e20d98415 100644 --- a/packages/app/e2e/performance/timeline/session-timeline-stress.fixture.ts +++ b/packages/app/e2e/performance/timeline/session-timeline-stress.fixture.ts @@ -131,7 +131,7 @@ function toolPart( ): MessagePart { const metadata = metadataOverride ?? - (tool === "apply_patch" + (tool === "patch" ? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] } : tool === "edit" || tool === "write" ? { @@ -219,7 +219,7 @@ function turn(index: number): Message[] { ? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)] : []), ...(index % 8 === 0 - ? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)] + ? [toolPart(index, 8, "patch", { files: [`src/generated/patch-${index}.ts`] }, 620)] : []), ...(index % 7 === 0 ? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)] @@ -269,7 +269,6 @@ const childMessages = Array.from({ length: 4 }, (_, index) => [ ]).flat() function renderable(part: MessagePart) { - if (part.type === "tool" && part.tool === "todowrite") return false if (part.type === "text") return !!part.text.trim() if (part.type === "reasoning") return !!part.text.trim() return part.type !== "step-start" && part.type !== "step-finish" && part.type !== "patch" diff --git a/packages/app/e2e/regression/cross-server-tab-close.spec.ts b/packages/app/e2e/regression/cross-server-tab-close.spec.ts index f09a2c7b63..9fd7991437 100644 --- a/packages/app/e2e/regression/cross-server-tab-close.spec.ts +++ b/packages/app/e2e/regression/cross-server-tab-close.spec.ts @@ -1,6 +1,5 @@ import { expect, test, type Page, type Route } from "@playwright/test" import { base64Encode } from "@opencode-ai/core/util/encode" -import { currentSession } from "../utils/mock-server" const serverA = "http://127.0.0.1:4096" const serverB = "http://127.0.0.1:4097" @@ -34,7 +33,7 @@ test("closing the active server's last tab opens the remaining server tab", asyn await tabA.locator('[data-slot="tab-close"] button').click() await expect(page).toHaveURL(new RegExp(`${hrefB.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`)) - await expect.poll(() => requests.some((url) => url.startsWith(`${serverB}/api/session/${sessionB.id}`))).toBe(true) + await expect.poll(() => requests.some((url) => url.startsWith(`${serverB}/session/${sessionB.id}`))).toBe(true) await expect(page.getByText(sessionB.title).first()).toBeVisible() const sessionBRequests = requests.filter((url) => url.includes(`/session/${sessionB.id}`)) expect(sessionBRequests.every((url) => url.startsWith(serverB))).toBe(true) @@ -85,21 +84,17 @@ async function mockServers(page: Page, requests: string[]) { const current = url.origin === serverA ? sessionA : sessionB const directory = url.searchParams.get("directory") if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500) - if (url.pathname === "/global/event" || url.pathname === "/event" || url.pathname === "/api/event") - return sse(route) - if (url.pathname === "/global/health") return json(route, {}, 404) - if (url.pathname === "/api/health") return json(route, { pid: 1 }) - if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} }) - if (url.pathname === "/api/session/active") return json(route, { data: {} }) - if (url.pathname === `/api/session/${current.id}`) return json(route, { data: currentSession(current) }) - if (url.pathname === `/api/session/${current.id}/message`) return json(route, { data: [], cursor: {} }) + if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route) + if (url.pathname === "/global/health") return json(route, { healthy: true }) + if (url.pathname === "/session") return json(route, [current]) if (url.pathname === `/session/${current.id}`) return json(route, current) if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) if (url.pathname === `/session/${current.id}/message`) return json(route, []) - if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, []) + if (/^\/session\/[^/]+\/(children|diff)$/.test(url.pathname)) return json(route, []) if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname)) return json(route, []) - if (["/global/config", "/config", "/provider/auth", "/mcp"].includes(url.pathname)) return json(route, {}) + if (["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"].includes(url.pathname)) + return json(route, {}) if (url.pathname === "/provider") return json(route, { all: [], connected: [], default: { providerID: "", modelID: "" } }) if (url.pathname === "/agent") return json(route, [{ name: "build", mode: "primary" }]) @@ -121,20 +116,7 @@ async function mockServers(page: Page, requests: string[]) { directory: current.directory, home: current.directory, }) - if (url.pathname === "/api/path") - return json(route, { - state: current.directory, - config: current.directory, - worktree: current.directory, - directory: current.directory, - home: current.directory, - }) if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" }) - if (url.pathname === "/api/vcs") - return json(route, { - location: { directory: current.directory }, - data: { branch: "main", defaultBranch: "main" }, - }) return json(route, {}) }) } diff --git a/packages/app/e2e/regression/open-file-expand-folder.spec.ts b/packages/app/e2e/regression/open-file-expand-folder.spec.ts deleted file mode 100644 index 37739d2fbb..0000000000 --- a/packages/app/e2e/regression/open-file-expand-folder.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { base64Encode } from "@opencode-ai/core/util/encode" -import { expect, test } from "@playwright/test" -import { mockOpenCodeServer } from "../utils/mock-server" -import { expectSessionTitle } from "../utils/waits" - -const directory = "C:/OpenCode/OpenFileExpand" -const projectID = "proj_open_file_expand" -const sessionID = "ses_open_file_expand" -const title = "Open file expand" -const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}` - -test.use({ viewport: { width: 1440, height: 900 } }) - -test("expands a folder whose path has a trailing Windows separator", async ({ page }) => { - await mockOpenCodeServer(page, { - directory, - project: { - id: projectID, - worktree: directory, - vcs: "git", - name: "open-file-expand", - time: { created: 1700000000000, updated: 1700000000000 }, - sandboxes: [], - }, - provider: { - all: [ - { - id: "opencode", - name: "OpenCode", - models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } }, - }, - ], - connected: ["opencode"], - default: { providerID: "opencode", modelID: "test" }, - }, - sessions: [ - { - id: sessionID, - slug: sessionID, - projectID, - directory, - title, - version: "dev", - time: { created: 1700000000000, updated: 1700000000000 }, - }, - ], - vcsDiff: [], - fileList: (path) => { - if (path === "frontend\\" || path === "frontend") { - return [ - { - name: "app.ts", - path: "frontend\\app.ts", - absolute: `${directory}/frontend/app.ts`, - type: "file" as const, - ignored: false, - }, - ] - } - if (path) return [] - return [ - { - name: "frontend", - path: "frontend\\", - absolute: `${directory}/frontend`, - type: "directory" as const, - ignored: false, - }, - { - name: "README.md", - path: "README.md", - absolute: `${directory}/README.md`, - type: "file" as const, - ignored: false, - }, - ] - }, - fileContent: (path) => ({ type: "text", content: `contents:${path}` }), - pageMessages: () => ({ items: [] }), - }) - - await page.addInitScript( - ({ directory, server, sessionID }) => { - localStorage.setItem( - "settings.v3", - JSON.stringify({ general: { newLayoutDesigns: true, shouldDisplayTabsToast: false } }), - ) - localStorage.setItem( - "opencode.global.dat:server", - JSON.stringify({ - projects: { local: [{ worktree: directory, expanded: true }] }, - lastProject: { local: directory }, - }), - ) - localStorage.setItem( - "opencode.global.dat:layout", - JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }), - ) - localStorage.setItem( - "opencode.global.dat:review-panel-v2", - JSON.stringify({ sidebarOpened: true, sidebarWidth: 240, expandMode: "collapse" }), - ) - localStorage.setItem( - "opencode.window.browser.dat:tabs", - JSON.stringify([{ type: "session", server, sessionId: sessionID }]), - ) - }, - { directory, server, sessionID }, - ) - - await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`) - await expectSessionTitle(page, title) - - const panel = page.locator("#review-panel") - await panel.getByRole("button", { name: "Open file" }).click() - await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "") - - const sidebar = panel.locator('[data-component="session-review-v2-sidebar-root"]') - await expect(sidebar).toBeVisible() - - const frontendRow = panel.locator('[data-slot="file-tree-v2-row"][data-path="frontend"]') - await expect(frontendRow).toBeVisible() - await expect(frontendRow).toHaveAttribute("aria-expanded", "false") - await frontendRow.click() - await expect(frontendRow).toHaveAttribute("aria-expanded", "true") - - const appRow = panel.locator('[data-slot="file-tree-v2-row"][data-path="frontend/app.ts"]') - await expect(appRow).toBeVisible() - await appRow.click() - await expect(panel.getByRole("tab", { name: "app.ts" })).toHaveAttribute("data-selected", "") - await expect(panel.getByText("contents:frontend/app.ts", { exact: true })).toBeVisible() -}) diff --git a/packages/app/e2e/regression/project-picker-recent-search.spec.ts b/packages/app/e2e/regression/project-picker-recent-search.spec.ts deleted file mode 100644 index 2cdb0b4a03..0000000000 --- a/packages/app/e2e/regression/project-picker-recent-search.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { expect, test } from "@playwright/test" -import type { Page } from "@playwright/test" -import { fixture, pageMessages } from "../smoke/session-timeline.fixture" -import { mockOpenCodeServer } from "../utils/mock-server" -import { expectAppVisible } from "../utils/waits" - -const NAMES = ["alpha-service", "bravo-web", "charlie-api", "delta-tools", "echo-infra", "foxtrot-docs"] -const worktrees = NAMES.map((name) => `/opencode-demo/${name}`) - -// The sixth project sits outside the five-item recent cap, so it is only reachable if the -// dialog hands every recent project to the list filter instead of a pre-truncated slice. -const OUTSIDE_CAP = "foxtrot-docs" - -// Dialog rows carry data-directory-path; the sidebar project list does not, so this -// scopes assertions to the picker instead of matching the sidebar entry of the same name. -const rows = (page: Page) => page.locator("[data-directory-path]") -const row = (page: Page, name: string) => page.locator(`[data-directory-path*="${name}"]`) - -async function openProjectDialog(page: Page) { - await mockOpenCodeServer(page, { - sessions: fixture.sessions, - provider: fixture.provider, - directory: fixture.directory, - project: fixture.project, - pageMessages, - fileList: () => [], - findFiles: () => [], - }) - await page.addInitScript((dirs) => { - localStorage.setItem( - "opencode.global.dat:server", - JSON.stringify({ - projects: { local: dirs.map((worktree: string) => ({ worktree, expanded: false })) }, - lastProject: {}, - }), - ) - }, worktrees) - await page.goto("/") - const add = page.getByRole("button", { name: "Add project" }).first() - await expectAppVisible(add) - await add.click() - await expect(rows(page)).toHaveCount(5) - return page.getByRole("textbox").last() -} - -test("searches every recent project, not just the five most recent", async ({ page }) => { - const search = await openProjectDialog(page) - await expect(row(page, OUTSIDE_CAP)).toHaveCount(0) - - await search.fill("foxtrot") - - await expect(row(page, OUTSIDE_CAP)).toHaveCount(1) -}) - -test("still caps the idle recent list at five projects", async ({ page }) => { - await openProjectDialog(page) - - await expect(row(page, NAMES[4])).toHaveCount(1) - await expect(row(page, OUTSIDE_CAP)).toHaveCount(0) -}) diff --git a/packages/app/e2e/regression/prompt-input-v2-command-draft.spec.ts b/packages/app/e2e/regression/prompt-input-v2-command-draft.spec.ts deleted file mode 100644 index 50dcc8820b..0000000000 --- a/packages/app/e2e/regression/prompt-input-v2-command-draft.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { expect, test } from "@playwright/test" -import { base64Encode } from "@opencode-ai/core/util/encode" -import { mockOpenCodeServer } from "../utils/mock-server" -import { expectAppVisible } from "../utils/waits" - -const directory = "C:/OpenCode/PromptInputV2Editing" -const projectID = "proj_prompt_input_v2_editing" -const sessionID = "ses_prompt_input_v2_editing" - -test("preserves the draft when a populated command menu triggers a built-in", async ({ page }) => { - await mockOpenCodeServer(page, { - directory, - project: { - id: projectID, - worktree: directory, - vcs: "git", - name: "prompt-input-v2-editing", - time: { created: 1700000000000, updated: 1700000000000 }, - sandboxes: [], - }, - provider: { all: [], connected: [], default: {} }, - sessions: [ - { - id: sessionID, - slug: "prompt-input-v2-editing", - projectID, - directory, - title: "Prompt input V2 editing", - version: "dev", - time: { created: 1700000000000, updated: 1700000000000 }, - }, - ], - pageMessages: () => ({ items: [] }), - }) - await page.addInitScript(() => { - localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) - }) - - await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) - const composer = page.locator('[data-component="prompt-input-v2"]') - const input = composer.locator('[data-component="prompt-input"]') - await expectAppVisible(composer) - - await input.fill("keep me") - await composer.getByRole("button", { name: "Add images and files" }).click() - await page.getByRole("menuitem", { name: "Commands" }).click() - await page.locator('[data-suggestion-id="model.choose"]').click() - - await expect(input).toHaveText("keep me") -}) diff --git a/packages/app/e2e/regression/prompt-thinking-level.spec.ts b/packages/app/e2e/regression/prompt-thinking-level.spec.ts index 9315c347c0..4219699f28 100644 --- a/packages/app/e2e/regression/prompt-thinking-level.spec.ts +++ b/packages/app/e2e/regression/prompt-thinking-level.spec.ts @@ -54,15 +54,18 @@ test("shows the V2 thinking level control while relevant", async ({ page }) => { }) await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) - const composer = page.locator('[data-component="prompt-input-v2"]') + const composer = page.locator('[data-component="session-composer"]') const input = composer.locator('[data-component="prompt-input"]') - const control = composer.getByRole("button", { name: "Choose model variant" }) + const control = composer.locator('[data-component="prompt-variant-control"]') await expectAppVisible(composer) await idleComposer(page) + await expect(control).toBeHidden() + + await composer.hover() await expect(control).toBeVisible() - await control.click() + await control.locator('[data-action="prompt-model-variant"]').click() const high = page.getByRole("menuitemradio", { name: "high" }) await expect(high).toBeVisible() await page.mouse.move(0, 0) diff --git a/packages/app/e2e/regression/remote-session-settings.spec.ts b/packages/app/e2e/regression/remote-session-settings.spec.ts index 35a0aa44cd..c17ae5c1c6 100644 --- a/packages/app/e2e/regression/remote-session-settings.spec.ts +++ b/packages/app/e2e/regression/remote-session-settings.spec.ts @@ -1,7 +1,6 @@ import { base64Encode } from "@opencode-ai/core/util/encode" import { expect, test, type Page, type Route } from "@playwright/test" import { installSseTransport } from "../utils/sse-transport" -import { currentSession } from "../utils/mock-server" const serverA = "http://127.0.0.1:4096" const serverB = "http://127.0.0.1:4097" @@ -18,7 +17,7 @@ test("session settings use the remote server context", async ({ page }) => { await page.goto(`/server/${base64Encode(serverB)}/session/${sessionB.id}`) await expect(page.getByText(sessionB.title).first()).toBeVisible() - await page.keyboard.press("Control+,") + await page.keyboard.press(process.platform === "darwin" ? "Meta+," : "Control+,") const dialog = page.locator(".settings-v2-dialog") const autoAccept = dialog.locator('[data-action="settings-auto-accept-permissions"]') @@ -59,7 +58,7 @@ test("auto-accept responds for an unfocused server session", async ({ page }) => const hrefB = `/server/${base64Encode(serverB)}/session/${sessionB.id}` await page.goto(`/server/${base64Encode(serverA)}/session/${sessionA.id}`) await expect(page.getByText(sessionA.title).first()).toBeVisible() - await page.keyboard.press("Control+,") + await page.keyboard.press(process.platform === "darwin" ? "Meta+," : "Control+,") const autoAccept = page.locator(".settings-v2-dialog").locator('[data-action="settings-auto-accept-permissions"]') await autoAccept.locator('[data-slot="switch-control"]').click() await expect(autoAccept.getByRole("switch")).toBeChecked() @@ -181,36 +180,10 @@ async function mockServers(page: Page, permissionRequests: string[], permissionR return json(route, true) } if (requestDirectory && requestDirectory !== directory) return json(route, { name: "InvalidDirectory" }, 500) - if (url.pathname === "/global/event" || url.pathname === "/event" || url.pathname === "/api/event") - return sse(route) + if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route) if (url.pathname === "/global/health") return json(route, { healthy: true }) - if (url.pathname === "/api/provider" || url.pathname === "/api/model" || url.pathname === "/api/agent") - return json(route, { data: [] }) - if (url.pathname === "/api/model/default") return json(route, { data: null }) - if (["/api/command", "/api/reference", "/api/permission/request", "/api/question/request"].includes(url.pathname)) - return json(route, { location: { directory }, data: [] }) - if (url.pathname === "/api/mcp") return json(route, { location: { directory }, data: [] }) - if (url.pathname === "/api/mcp/resource") - return json(route, { location: { directory }, data: { resources: [], templates: [] } }) - if (url.pathname === "/api/project") { - return json(route, [ - { - id: remote ? sessionB.projectID : "project-server-a", - worktree: directory, - vcs: "git", - time: { created: 1, updated: 1 }, - sandboxes: [], - }, - ]) - } - if (url.pathname === "/api/project/current") - return json(route, { id: remote ? sessionB.projectID : "project-server-a", directory }) - if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} }) - if (url.pathname === "/api/session/active") return json(route, { data: {} }) - const currentSessionInfo = sessions.find((session) => url.pathname === `/api/session/${session.id}`) - if (currentSessionInfo) return json(route, { data: currentSession(currentSessionInfo) }) - if (sessions.some((session) => url.pathname === `/api/session/${session.id}/message`)) - return json(route, { data: [], cursor: {} }) + if (url.pathname === "/session/status") return json(route, {}) + if (url.pathname === "/session") return json(route, sessions) const current = sessions.find((session) => url.pathname === `/session/${session.id}`) if (current) return json(route, current) if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) @@ -243,12 +216,7 @@ async function mockServers(page: Page, permissionRequests: string[], permissionR directory, home: directory, }) - if (url.pathname === "/api/path") - return json(route, { state: directory, config: directory, worktree: directory, directory, home: directory }) if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" }) - if (url.pathname === "/api/vcs") - return json(route, { location: { directory }, data: { branch: "main", defaultBranch: "main" } }) - if (url.pathname === "/api/pty/shells") return json(route, { location: { directory }, data: [] }) return json(route, {}) }) } diff --git a/packages/app/e2e/regression/remote-tab-busy.spec.ts b/packages/app/e2e/regression/remote-tab-busy.spec.ts index 2d9b1e2349..ad7e2e1d99 100644 --- a/packages/app/e2e/regression/remote-tab-busy.spec.ts +++ b/packages/app/e2e/regression/remote-tab-busy.spec.ts @@ -1,6 +1,5 @@ import { expect, test, type Page, type Route } from "@playwright/test" import { base64Encode } from "@opencode-ai/core/util/encode" -import { currentSession } from "../utils/mock-server" const serverA = "http://127.0.0.1:4096" const serverB = "http://127.0.0.1:4097" @@ -58,19 +57,15 @@ async function mockServers(page: Page) { const current = url.origin === serverA ? sessionA : sessionB const directory = url.searchParams.get("directory") if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500) - if (url.pathname === "/global/event" || url.pathname === "/event" || url.pathname === "/api/event") - return sse(route, url.pathname === "/api/event") - if (url.pathname === "/global/health") return json(route, {}, 404) - if (url.pathname === "/api/health") return json(route, { pid: 1 }) - if (url.pathname === "/api/session/active") - return json(route, { data: url.origin === serverB ? { [sessionB.id]: { type: "running" } } : {} }) - if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} }) - if (url.pathname === `/api/session/${current.id}`) return json(route, { data: currentSession(current) }) - if (url.pathname === `/api/session/${current.id}/message`) return json(route, { data: [], cursor: {} }) + if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route) + if (url.pathname === "/global/health") return json(route, { healthy: true }) + if (url.pathname === "/session/status") + return json(route, url.origin === serverB ? { [sessionB.id]: { type: "busy" } } : {}) + if (url.pathname === "/session") return json(route, [current]) if (url.pathname === `/session/${current.id}`) return json(route, current) if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) if (url.pathname === `/session/${current.id}/message`) return json(route, []) - if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, []) + if (/^\/session\/[^/]+\/(children|diff)$/.test(url.pathname)) return json(route, []) if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname)) return json(route, []) if (["/global/config", "/config", "/provider/auth", "/mcp"].includes(url.pathname)) return json(route, {}) @@ -95,20 +90,7 @@ async function mockServers(page: Page) { directory: current.directory, home: current.directory, }) - if (url.pathname === "/api/path") - return json(route, { - state: current.directory, - config: current.directory, - worktree: current.directory, - directory: current.directory, - home: current.directory, - }) if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" }) - if (url.pathname === "/api/vcs") - return json(route, { - location: { directory: current.directory }, - data: { branch: "main", defaultBranch: "main" }, - }) return json(route, {}) }) } @@ -122,10 +104,6 @@ function json(route: Route, body: unknown, status = 200) { }) } -function sse(route: Route, current: boolean) { - return route.fulfill({ - status: 200, - contentType: "text/event-stream", - body: current ? 'data: {"id":"evt_connected","type":"server.connected","data":{}}\n\n' : ": ok\n\n", - }) +function sse(route: Route) { + return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" }) } diff --git a/packages/app/e2e/regression/review-line-comment.spec.ts b/packages/app/e2e/regression/review-line-comment.spec.ts index 7850f7820a..042f926c53 100644 --- a/packages/app/e2e/regression/review-line-comment.spec.ts +++ b/packages/app/e2e/regression/review-line-comment.spec.ts @@ -84,7 +84,6 @@ test("stages a submitted line comment in the prompt context", async ({ page }) = async function openReview(page: Page) { await page.setViewportSize({ width: 700, height: 900 }) await mockOpenCodeServer(page, { - protocol: "v2", directory, project: { id: "proj_review_line_comment_regression", @@ -144,9 +143,9 @@ async function openReview(page: Page) { await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await expectSessionTitle(page, title) - const diffResponse = page.waitForResponse((response) => new URL(response.url()).pathname === "/api/vcs/diff") + const diffResponse = page.waitForResponse((response) => new URL(response.url()).pathname === "/vcs/diff") await page.getByRole("tab", { name: "Changes" }).click() - expect((await (await diffResponse).json()).data).toHaveLength(1) + expect(await (await diffResponse).json()).toHaveLength(1) const review = page.locator('[data-component="session-review"]') await expectAppVisible(review) diff --git a/packages/app/e2e/regression/review-open-file.spec.ts b/packages/app/e2e/regression/review-open-file.spec.ts index 04e6d2cced..25ebd3a37a 100644 --- a/packages/app/e2e/regression/review-open-file.spec.ts +++ b/packages/app/e2e/regression/review-open-file.spec.ts @@ -133,7 +133,7 @@ test("opens and searches project files inline", async ({ page }) => { await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveCount(1) await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "") await expect(sidebarToggle).toBeDisabled() - await panel.locator("#session-side-panel-review-tab").click() + await panel.getByRole("tab", { name: /Review/ }).click() await expect(sidebarToggle).toBeEnabled() await panel.getByRole("tab", { name: "Open file" }).click() await page.keyboard.press("Control+w") diff --git a/packages/app/e2e/regression/review-state-persistence.spec.ts b/packages/app/e2e/regression/review-state-persistence.spec.ts index 0d6756201e..aa42f1bb51 100644 --- a/packages/app/e2e/regression/review-state-persistence.spec.ts +++ b/packages/app/e2e/regression/review-state-persistence.spec.ts @@ -46,7 +46,7 @@ test("restores review mode and selected file per session", async ({ page }) => { async function selectMode(page: Page, current: string, next: string) { await page.getByRole("button", { name: current }).click() - await page.getByRole("option", { name: next }).dispatchEvent("click") + await page.getByRole("option", { name: next }).click() } async function selectFile(page: Page, file: string) { @@ -65,7 +65,6 @@ async function switchSession(page: Page, title: string) { async function setup(page: Page) { await mockOpenCodeServer(page, { - protocol: "v1", directory, project: { id: projectID, diff --git a/packages/app/e2e/regression/review-terminal-stacked.spec.ts b/packages/app/e2e/regression/review-terminal-stacked.spec.ts index 79b564820e..154bab48c4 100644 --- a/packages/app/e2e/regression/review-terminal-stacked.spec.ts +++ b/packages/app/e2e/regression/review-terminal-stacked.spec.ts @@ -20,12 +20,10 @@ const branchDiffs = [ test("keeps the review tree and terminal sized when both panels are open", async ({ page }) => { test.setTimeout(120_000) const events: Array<{ directory: string; payload: Record }> = [] - const sessionStatus = { [sessionID]: { type: "idle" as "busy" | "idle" } } let detailVersion = 1 let detailFailures = 1 await page.setViewportSize({ width: 1400, height: 900 }) await mockOpenCodeServer(page, { - protocol: "v1", directory, project: { id: projectID, @@ -57,7 +55,7 @@ test("keeps the review tree and terminal sized when both panels are open", async time: { created: 1700000000000, updated: 1700000000000 }, }, ], - sessionStatus: () => sessionStatus, + sessionStatus: { [sessionID]: { type: "idle" } }, pageMessages: () => ({ items: [] }), events: () => events.splice(0, 1), eventRetry: 16, @@ -66,10 +64,7 @@ test("keeps the review tree and terminal sized when both panels are open", async route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ - branch: "review-pane-performance", - default_branch: "dev", - }), + body: JSON.stringify({ branch: "review-pane-performance", default_branch: "dev" }), }), ) await page.route("**/vcs/diff**", (route) => { @@ -91,51 +86,15 @@ test("keeps the review tree and terminal sized when both panels are open", async ), }) }) - await page.route("**/pty*", (route) => + await page.route("**/pty", (route) => route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ - location: { directory, project: { id: projectID, directory } }, - data: { - id: "pty_review_terminal", - title: "Terminal 1", - command: "cmd.exe", - args: [], - cwd: directory, - status: "running", - pid: 1, - }, - }), + body: JSON.stringify({ id: "pty_review_terminal", title: "Terminal 1" }), }), ) - await page.route("**/pty/pty_review_terminal*", (route) => - route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - location: { directory, project: { id: projectID, directory } }, - data: { - id: "pty_review_terminal", - title: "Terminal 1", - command: "cmd.exe", - args: [], - cwd: directory, - status: "running", - pid: 1, - }, - }), - }), - ) - await page.route("**/pty/pty_review_terminal/connect-token*", (route) => - route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - location: { directory, project: { id: projectID, directory } }, - data: { ticket: "e2e-ticket", expires_in: 60 }, - }), - }), + await page.route("**/pty/pty_review_terminal", (route) => + route.fulfill({ status: 200, contentType: "application/json", body: "{}" }), ) await page.routeWebSocket("**/pty/pty_review_terminal/connect", () => undefined) await page.addInitScript(() => { @@ -184,7 +143,6 @@ test("keeps the review tree and terminal sized when both panels are open", async const preview = page.locator('[data-slot="session-review-v2-diff-scroll"]') await expect(preview).toContainText("after-1") detailVersion = 2 - sessionStatus[sessionID] = { type: "busy" } events.push(statusEvent("busy")) await expect(page.getByRole("button", { name: "Stop" })).toBeVisible() const refreshedDiff = page.waitForRequest((request) => { @@ -194,7 +152,6 @@ test("keeps the review tree and terminal sized when both panels are open", async url.searchParams.get("directory")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true ) }) - sessionStatus[sessionID] = { type: "idle" } events.push(statusEvent("idle")) await refreshedDiff await expect(preview).toContainText("after-2") diff --git a/packages/app/e2e/regression/session-list-path-loading.spec.ts b/packages/app/e2e/regression/session-list-path-loading.spec.ts index 3319514df6..4a3855122a 100644 --- a/packages/app/e2e/regression/session-list-path-loading.spec.ts +++ b/packages/app/e2e/regression/session-list-path-loading.spec.ts @@ -16,8 +16,8 @@ test("shows loaded sessions before the directory path request resolves", async ( const pathBlocked = new Promise((resolve) => { releasePath = resolve }) - await page.route("**/api/path?*", async (route) => { - if (!new URL(route.request().url()).searchParams.has("location[directory]")) return route.fallback() + await page.route("**/path?*", async (route) => { + if (!new URL(route.request().url()).searchParams.has("directory")) return route.fallback() await pathBlocked return route.fallback() }) diff --git a/packages/app/e2e/regression/session-request-docks.spec.ts b/packages/app/e2e/regression/session-request-docks.spec.ts index 5ea9d4f761..714d6ca96f 100644 --- a/packages/app/e2e/regression/session-request-docks.spec.ts +++ b/packages/app/e2e/regression/session-request-docks.spec.ts @@ -42,8 +42,7 @@ test("shows a pending question dock", async ({ page }) => { const rejectRequests: string[] = [] page.on("request", (request) => { if (request.method() !== "POST") return - if (new URL(request.url()).pathname === `/api/session/${sessionID}/question/question-request/reject`) - rejectRequests.push(request.url()) + if (new URL(request.url()).pathname === "/question/question-request/reject") rejectRequests.push(request.url()) }) await question.locator('[data-component="icon-button"][data-icon="chevron-down"]').click() @@ -65,9 +64,7 @@ test("shows a pending question dock", async ({ page }) => { await question.getByRole("radio", { name: /Minimal/ }).click() const reply = page.waitForRequest( - (request) => - request.method() === "POST" && - new URL(request.url()).pathname === `/api/session/${sessionID}/question/question-request/reply`, + (request) => request.method() === "POST" && new URL(request.url()).pathname === "/question/question-request/reply", ) await question.getByRole("button", { name: "Submit" }).click() expect((await reply).postDataJSON()).toEqual({ answers: [["Minimal"]] }) @@ -100,8 +97,8 @@ test("shows a pending permission dock", async ({ page }) => { const reply = page.waitForRequest((request) => request.method() === "POST") await permission.getByRole("button", { name: "Allow once" }).click() const request = await reply - expect(new URL(request.url()).pathname).toBe(`/api/session/${sessionID}/permission/permission-request/reply`) - expect(request.postDataJSON()).toEqual({ reply: "once" }) + expect(new URL(request.url()).pathname).toBe(`/session/${sessionID}/permissions/permission-request`) + expect(request.postDataJSON()).toEqual({ response: "once" }) }) test("restores the draft caret before typing after a request dock closes", async ({ page }) => { @@ -173,7 +170,6 @@ async function mockServer( }, ) { await mockOpenCodeServer(page, { - protocol: "v2", directory, project: { id: projectID, diff --git a/packages/app/e2e/regression/session-timeline-file-projection.spec.ts b/packages/app/e2e/regression/session-timeline-file-projection.spec.ts index f07da121c6..a591ff9470 100644 --- a/packages/app/e2e/regression/session-timeline-file-projection.spec.ts +++ b/packages/app/e2e/regression/session-timeline-file-projection.spec.ts @@ -24,7 +24,7 @@ test("renders a completed single-file patch", async ({ page }) => { assistantMessage([ toolPart( id, - "apply_patch", + "patch", "completed", { files: ["src/a.ts"] }, { diff --git a/packages/app/e2e/regression/session-timeline-file-state.spec.ts b/packages/app/e2e/regression/session-timeline-file-state.spec.ts index cb228c13c7..f0871a0da3 100644 --- a/packages/app/e2e/regression/session-timeline-file-state.spec.ts +++ b/packages/app/e2e/regression/session-timeline-file-state.spec.ts @@ -35,7 +35,7 @@ test("preserves nested patch file state through outer collapse and reopen", asyn assistantMessage([ toolPart( patchID, - "apply_patch", + "patch", "completed", { files: files.map((file) => file.filePath) }, { metadata: { files } }, diff --git a/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts b/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts index b303071c87..3e2b171bca 100644 --- a/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts +++ b/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts @@ -32,23 +32,6 @@ for (const expanded of [false, true]) { }) } -test("shows and expands a running shell command without shimmering it", async ({ page }) => { - const id = "prt_shell_running_command" - const command = "sleep 10 && echo done" - await setupTimeline(page, { - messages: [userMessage(), assistantMessage([shell(id, "running", "still running", command)], { completed: false })], - settings: { shellToolPartsExpanded: false }, - }) - - const tool = page.locator(`[data-timeline-part-id="${id}"]`) - await expect(tool.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "true") - await expect(tool.locator('[data-component="shell-submessage"]')).toHaveText(command) - await expect(tool.locator('[data-component="shell-submessage"] [data-component="text-shimmer"]')).toHaveCount(0) - await tool.locator('[data-slot="collapsible-trigger"]').click() - await expect(tool.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true") - await expect(tool.locator('[data-slot="bash-pre"]')).toContainText("still running") -}) - test("transitions thinking and hidden reasoning through busy to idle", async ({ page }) => { const reasoningID = "prt_reasoning_hidden" const assistant = assistantMessage([reasoningPart(reasoningID, "## Inspecting stability")], { completed: false }) diff --git a/packages/app/e2e/regression/session-timeline-projection.spec.ts b/packages/app/e2e/regression/session-timeline-projection.spec.ts index b1aabcc32c..1ebda43478 100644 --- a/packages/app/e2e/regression/session-timeline-projection.spec.ts +++ b/packages/app/e2e/regression/session-timeline-projection.spec.ts @@ -35,7 +35,6 @@ test.describe("session timeline projection", () => { editPart("prt_edit"), toolPart("prt_write", "write", "completed", { filePath: "src/new.ts", content: "export const stable = true\n" }), patchPart("prt_patch"), - toolPart("prt_todo", "todowrite", "completed", { todos: [{ content: "Hidden", status: "pending" }] }), toolPart( "prt_question", "question", @@ -65,7 +64,6 @@ test.describe("session timeline projection", () => { ]) { await expect(page.locator(`[data-timeline-part-id="${id}"]`).first(), id).toBeVisible() } - await expect(page.locator('[data-timeline-part-id="prt_todo"]')).toHaveCount(0) }) test("projects gaps, dividers, assistant parts, and errors together", async ({ page }) => { @@ -249,7 +247,7 @@ function editPart(id: string) { function patchPart(id: string) { return toolPart( id, - "apply_patch", + "patch", "completed", { files: ["src/a.ts", "src/b.ts"] }, { diff --git a/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts b/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts index 99f1acf270..4c2c1c4ead 100644 --- a/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts +++ b/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts @@ -8,7 +8,7 @@ import { } from "../performance/timeline-stability/fixture" test("renders every tool error outcome without leaking hidden tools", async ({ page }) => { - const ordinary = ["bash", "edit", "write", "apply_patch", "webfetch", "websearch", "task", "skill", "mcp_probe"] + const ordinary = ["bash", "edit", "write", "patch", "webfetch", "websearch", "task", "skill", "mcp_probe"] const parts = ordinary.map((tool, index) => toolPart(`prt_error_${index}`, tool, "error", errorInput(tool), { error: `${tool} failed visibly` }), ) @@ -17,13 +17,11 @@ test("renders every tool error outcome without leaking hidden tools", async ({ p error: "The user dismissed this question", }), toolPart("prt_question_error", "question", "error", questionInput(), { error: "Question transport failed" }), - toolPart("prt_todo_error", "todowrite", "error", { todos: [] }, { error: "Hidden todo failure" }), ) await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] }) await expect(page.locator('[data-kind="tool-error-card"]')).toHaveCount(ordinary.length + 1) await expect(page.getByText(/dismissed/i)).toBeVisible() - await expect(page.locator('[data-timeline-part-id="prt_todo_error"]')).toHaveCount(0) for (let index = 0; index < ordinary.length; index++) { await expect(page.locator(`[data-timeline-part-id="prt_error_${index}"]`)).toBeVisible() } @@ -90,7 +88,7 @@ function questionInput() { function errorInput(tool: string) { if (tool === "bash") return { command: "exit 1" } if (["edit", "write"].includes(tool)) return { filePath: "src/error.ts", content: "" } - if (tool === "apply_patch") return { files: ["src/error.ts"] } + if (tool === "patch") return { files: ["src/error.ts"] } if (tool === "webfetch") return { url: "https://example.com" } if (tool === "websearch") return { query: "failure" } if (tool === "task") return { description: "Fail task", subagent_type: "explore" } diff --git a/packages/app/e2e/regression/session-timeline-transport.spec.ts b/packages/app/e2e/regression/session-timeline-transport.spec.ts index 778ff3a3af..850e966d0b 100644 --- a/packages/app/e2e/regression/session-timeline-transport.spec.ts +++ b/packages/app/e2e/regression/session-timeline-transport.spec.ts @@ -89,8 +89,8 @@ test("reconnects after a stream error", async ({ page }) => { expect((await timeline.transport.connections())[0]?.endedBy).toBe("error") }) -test("does not request replay when reconnecting the volatile V2 event stream", async ({ page }) => { - const timeline = await setupTimeline(page, { eventRetry: 10, protocol: "v2" }) +test("records event IDs and reconnect Last-Event-ID headers", async ({ page }) => { + const timeline = await setupTimeline(page, { eventRetry: 10 }) const first = await timeline.transport.send(partUpdated(textPart("prt_transport_id", "event with id")), { id: "timeline-event-7", }) @@ -100,7 +100,7 @@ test("does not request replay when reconnecting the volatile V2 event stream", a const connection = await timeline.transport.waitForConnection({ after: first.connectionID }) expect(first.eventID).toBe("timeline-event-7") - expect(connection.headers["last-event-id"]).toBeUndefined() + expect(connection.headers["last-event-id"]).toBe("timeline-event-7") }) test("passes through non-event fetches", async ({ page }) => { diff --git a/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts b/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts deleted file mode 100644 index 55e7121275..0000000000 --- a/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { base64Encode } from "@opencode-ai/core/util/encode" -import { expect, test, type Page } from "@playwright/test" -import { mockOpenCodeServer } from "../utils/mock-server" -import { expectSessionTitle } from "../utils/waits" - -const directory = "C:/OpenCode/TodoDockNavigation" -const projectID = "proj_todo_dock_navigation" -const sourceID = "ses_todo_dock_source" -const otherID = "ses_todo_dock_other" -const sourceTitle = "Todo dock animation" -const otherTitle = "Separate session" - -const activeTodos = [ - { id: "todo-1", content: "Receive todos in the active session", status: "completed", priority: "high" }, - { id: "todo-2", content: "Keep the dock visible across tabs", status: "completed", priority: "high" }, - { id: "todo-3", content: "Close after the final todo", status: "in_progress", priority: "high" }, -] - -type EventPayload = { - directory: string - payload: Record -} - -test.use({ viewport: { width: 1440, height: 900 }, reducedMotion: "no-preference" }) - -test("animates todo lifecycle without replaying it across session tabs", async ({ page }) => { - test.setTimeout(90_000) - const events: EventPayload[] = [] - const todos: Record = { [sourceID]: [], [otherID]: [] } - const sessionStatus: Record = {} - - await mockOpenCodeServer(page, { - directory, - project: { - id: projectID, - worktree: directory, - vcs: "git", - name: "todo-dock-navigation", - time: { created: 1700000000000, updated: 1700000000000 }, - sandboxes: [], - }, - provider: { - all: [ - { - id: "opencode", - name: "OpenCode", - models: { - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - limit: { context: 200_000 }, - }, - }, - }, - ], - connected: ["opencode"], - default: { providerID: "opencode", modelID: "claude-opus-4-6" }, - }, - sessions: [session(sourceID, sourceTitle, 1700000000000), session(otherID, otherTitle, 1700000001000)], - sessionStatus: { [sourceID]: { type: "busy" } }, - pageMessages: () => ({ items: [] }), - events: () => events.splice(0, 1), - eventRetry: 16, - sessionStatus: () => sessionStatus, - todos: (sessionID) => todos[sessionID] ?? [], - }) - await configurePage(page) - - await page.goto(sessionHref(sourceID)) - await expectSessionTitle(page, sourceTitle) - const dock = page.locator('[data-component="session-todo-dock"]') - await expect(dock).toHaveCount(0) - - sessionStatus[sourceID] = { type: "busy" } - events.push(statusEvent(sourceID, "busy")) - await expect(page.getByRole("button", { name: "Stop" })).toBeVisible() - - await page.waitForTimeout(700) - const opening = sampleDock(page, 1_000) - todos[sourceID] = activeTodos - events.push(todoEvent(sourceID, activeTodos)) - await expect(dock).toBeVisible() - await expect(dock.locator('[data-state="in_progress"]')).toHaveCount(1) - expect((await opening).some((sample) => sample.opacity > 0.05 && sample.opacity < 0.95)).toBe(true) - - await switchSession(page, otherID, otherTitle) - await expect(dock).toHaveCount(0) - - const returningOpen = sampleDock(page, 700) - await switchSession(page, sourceID, sourceTitle) - const openSamples = (await returningOpen).filter((sample) => sample.present) - expect(openSamples.length).toBeGreaterThan(0) - expect(openSamples[0]!.opacity).toBeGreaterThan(0.98) - expect(openSamples[0]!.height).toBeGreaterThan(70) - await expect(dock.locator('[data-state="in_progress"]')).toHaveCount(1) - - const completedTodos = activeTodos.map((todo) => ({ ...todo, status: "completed" })) - const closing = sampleDock(page, 1_000) - todos[sourceID] = completedTodos - events.push(todoEvent(sourceID, completedTodos)) - await expect(dock).toHaveCount(0) - expect((await closing).some((sample) => sample.opacity > 0.05 && sample.opacity < 0.95)).toBe(true) - todos[sourceID] = [] - events.push(todoEvent(sourceID, [])) - - await switchSession(page, otherID, otherTitle) - const returningEmpty = sampleDock(page, 700) - await switchSession(page, sourceID, sourceTitle) - await expect(dock).toHaveCount(0) - expect((await returningEmpty).every((sample) => !sample.present)).toBe(true) -}) - -function session(id: string, title: string, created: number) { - return { - id, - slug: id, - projectID, - directory, - title, - version: "dev", - time: { created, updated: created }, - } -} - -function statusEvent(sessionID: string, type: "busy" | "idle"): EventPayload { - return { - directory, - payload: { type: "session.status", properties: { sessionID, status: { type } } }, - } -} - -function todoEvent(sessionID: string, next: typeof activeTodos): EventPayload { - return { - directory, - payload: { type: "todo.updated", properties: { sessionID, todos: next } }, - } -} - -async function configurePage(page: Page) { - const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}` - await page.addInitScript( - ({ directory, dirBase64, server, sessionIDs }) => { - localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) - localStorage.setItem( - "opencode.global.dat:server", - JSON.stringify({ - projects: { local: [{ worktree: directory, expanded: true }] }, - lastProject: { local: directory }, - }), - ) - localStorage.setItem( - "opencode.window.browser.dat:tabs", - JSON.stringify(sessionIDs.map((sessionId) => ({ type: "session", server, dirBase64, sessionId }))), - ) - }, - { directory, dirBase64: base64Encode(directory), server, sessionIDs: [sourceID, otherID] }, - ) -} - -function sessionHref(sessionID: string) { - const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}` - return `/server/${base64Encode(server)}/session/${sessionID}` -} - -async function switchSession(page: Page, sessionID: string, title: string) { - const href = sessionHref(sessionID) - const tab = page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first() - await expect(tab).toBeVisible() - await tab.click() - await expectSessionTitle(page, title) -} - -function sampleDock(page: Page, duration: number) { - return page.evaluate(async (duration) => { - const samples: { present: boolean; height: number; opacity: number }[] = [] - const start = performance.now() - while (performance.now() - start < duration) { - const dock = document.querySelector('[data-component="session-todo-dock"]') - const clip = dock?.parentElement?.parentElement - const label = dock?.querySelector('[data-action="session-todo-toggle"] span[aria-label]') - samples.push({ - present: !!dock, - height: clip?.getBoundingClientRect().height ?? 0, - opacity: label ? Number.parseFloat(getComputedStyle(label).opacity) : 0, - }) - await new Promise(requestAnimationFrame) - } - return samples - }, duration) -} diff --git a/packages/app/e2e/regression/subagent-child-navigation.spec.ts b/packages/app/e2e/regression/subagent-child-navigation.spec.ts index 019cc156ec..19d2c29af0 100644 --- a/packages/app/e2e/regression/subagent-child-navigation.spec.ts +++ b/packages/app/e2e/regression/subagent-child-navigation.spec.ts @@ -1,6 +1,6 @@ import { base64Encode } from "@opencode-ai/core/util/encode" import { expect, test, type Page } from "@playwright/test" -import { currentSession, mockOpenCodeServer } from "../utils/mock-server" +import { mockOpenCodeServer } from "../utils/mock-server" import { expectSessionTitle } from "../utils/waits" const directory = "C:/OpenCode/SubagentNavigation" @@ -72,19 +72,16 @@ async function setup(page: Page, events?: () => EventPayload[]) { events, eventRetry: events ? 16 : undefined, }) - // The child session resolves by ID but is absent from the session list, + // The child session resolves via /session/:id but is absent from the /session list, // matching a subagent session that has not been loaded into the list cache yet. await page.route( - (url) => url.pathname === "/api/session" && url.port === (process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"), + (url) => url.pathname === "/session" && url.port === (process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"), (route) => route.fulfill({ status: 200, contentType: "application/json", headers: { "access-control-allow-origin": "*" }, - body: JSON.stringify({ - data: [currentSession(session(parentID, parentTitle, 1700000000000))], - cursor: {}, - }), + body: JSON.stringify([session(parentID, parentTitle, 1700000000000)]), }), ) await configurePage(page) diff --git a/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts b/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts index b969b590d8..6bc417af80 100644 --- a/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts +++ b/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts @@ -1,12 +1,9 @@ import { expect, test, type Page, type Route } from "@playwright/test" import { base64Encode } from "@opencode-ai/core/util/encode" -import { currentSession } from "../utils/mock-server" const server = "http://127.0.0.1:4096" const sessionA = session("ses_tab_a", "Tab A session") const sessionB = session("ses_tab_b", "Tab B session") -const sessionC = session("ses_tab_c", "Tab C session") -const unresolvedSessionID = "ses_tab_unresolved" test("pressing mouse down on a tab navigates before mouse up", async ({ page }) => { await mockServer(page) @@ -42,34 +39,6 @@ test("pressing mouse down on a tab navigates before mouse up", async ({ page }) await expect(page).toHaveURL(new RegExp(`${hrefB.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`)) }) -test("keyboard navigation follows the visible tab order", async ({ page }) => { - await mockServer(page) - await page.addInitScript( - ({ server, sessionA, unresolved, sessionC }) => { - localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) - localStorage.setItem( - "opencode.window.browser.dat:tabs", - JSON.stringify([ - { type: "session", server, sessionId: sessionA }, - { type: "session", server, sessionId: unresolved }, - { type: "session", server, sessionId: sessionC }, - ]), - ) - }, - { server, sessionA: sessionA.id, unresolved: unresolvedSessionID, sessionC: sessionC.id }, - ) - - const hrefA = `/server/${base64Encode(server)}/session/${sessionA.id}` - const hrefC = `/server/${base64Encode(server)}/session/${sessionC.id}` - await page.goto(hrefA) - await expect(page.locator("[data-titlebar-tab-slot]:visible")).toHaveCount(2) - await expect(page.locator(`[data-titlebar-tab-slot]:has(a[href="${hrefC}"])`)).toBeVisible() - - await page.keyboard.press("Control+Alt+ArrowRight") - - await expect(page).toHaveURL(new RegExp(`${hrefC.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`)) -}) - function session(id: string, title: string) { return { id, @@ -83,29 +52,22 @@ function session(id: string, title: string) { } async function mockServer(page: Page) { - const sessions = [sessionA, sessionB, sessionC] + const sessions = [sessionA, sessionB] await page.route("**/*", async (route) => { const url = new URL(route.request().url()) if (url.origin !== server) return route.fallback() - if ([`/api/session/${unresolvedSessionID}`, `/session/${unresolvedSessionID}`].includes(url.pathname)) - return new Promise(() => {}) - if (url.pathname === "/global/event" || url.pathname === "/event" || url.pathname === "/api/event") - return sse(route) + if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route) if (url.pathname === "/global/health") return json(route, { healthy: true }) - if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} }) - if (url.pathname === "/api/session/active") return json(route, { data: {} }) - const currentSessionInfo = sessions.find((item) => url.pathname === `/api/session/${item.id}`) - if (currentSessionInfo) return json(route, { data: currentSession(currentSessionInfo) }) - if (sessions.some((item) => url.pathname === `/api/session/${item.id}/message`)) - return json(route, { data: [], cursor: {} }) + if (url.pathname === "/session") return json(route, sessions) const byId = sessions.find((item) => url.pathname === `/session/${item.id}`) if (byId) return json(route, byId) if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) if (/^\/session\/[^/]+\/message$/.test(url.pathname)) return json(route, []) - if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, []) + if (/^\/session\/[^/]+\/(children|diff)$/.test(url.pathname)) return json(route, []) if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname)) return json(route, []) - if (["/global/config", "/config", "/provider/auth", "/mcp"].includes(url.pathname)) return json(route, {}) + if (["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"].includes(url.pathname)) + return json(route, {}) if (url.pathname === "/provider") return json(route, { all: [], connected: [], default: { providerID: "", modelID: "" } }) if (url.pathname === "/agent") return json(route, [{ name: "build", mode: "primary" }]) @@ -127,20 +89,7 @@ async function mockServer(page: Page) { directory: sessionA.directory, home: sessionA.directory, }) - if (url.pathname === "/api/path") - return json(route, { - state: sessionA.directory, - config: sessionA.directory, - worktree: sessionA.directory, - directory: sessionA.directory, - home: sessionA.directory, - }) if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" }) - if (url.pathname === "/api/vcs") - return json(route, { - location: { directory: sessionA.directory }, - data: { branch: "main", defaultBranch: "main" }, - }) return json(route, {}) }) } diff --git a/packages/app/e2e/regression/terminal-composer-focus.spec.ts b/packages/app/e2e/regression/terminal-composer-focus.spec.ts index 99bf689085..f672602782 100644 --- a/packages/app/e2e/regression/terminal-composer-focus.spec.ts +++ b/packages/app/e2e/regression/terminal-composer-focus.spec.ts @@ -13,7 +13,6 @@ test.use({ viewport: { width: 1440, height: 900 } }) test.beforeEach(async ({ page }) => { await mockOpenCodeServer(page, { - protocol: "v2", directory, project: { id: projectID, @@ -47,30 +46,25 @@ test.beforeEach(async ({ page }) => { ], pageMessages: () => ({ items: [] }), }) - await page.route("**/api/pty*", (route) => { - expect(new URL(route.request().url()).searchParams.get("location[directory]")).toBe(directory) - return route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ location: ptyLocation(), data: ptyInfo(ptyID, "Terminal 1") }), - }) - }) - await page.route(`**/api/pty/${ptyID}*`, (route) => + await page.route("**/pty", (route) => route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ location: ptyLocation(), data: ptyInfo(ptyID, "Terminal 1") }), + body: JSON.stringify({ id: ptyID, title: "Terminal 1" }), }), ) - await page.route(`**/api/pty/${ptyID}/connect-token*`, (route) => + await page.route(`**/pty/${ptyID}`, (route) => + route.fulfill({ status: 200, contentType: "application/json", body: "{}" }), + ) + await page.route(`**/pty/${ptyID}/connect-token*`, (route) => route.fulfill({ status: 200, contentType: "application/json", headers: { "access-control-allow-origin": "*" }, - body: JSON.stringify({ location: ptyLocation(), data: { ticket: "e2e-ticket", expires_in: 60 } }), + body: JSON.stringify({ ticket: "e2e-ticket" }), }), ) - await page.routeWebSocket(new RegExp(`/api/pty/${ptyID}/connect`), () => undefined) + await page.routeWebSocket(new RegExp(`/pty/${ptyID}/connect`), () => undefined) await page.addInitScript(() => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) }) @@ -101,12 +95,12 @@ test("keeps composer focus when a cached terminal finishes mounting", async ({ p const ghostty = Promise.withResolvers() const release = Promise.withResolvers() const created = { count: 0 } - await page.route("**/api/pty*", (route) => { + await page.route("**/pty", (route) => { created.count += 1 return route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ location: ptyLocation(), data: ptyInfo(ptyID, "Terminal 1") }), + body: JSON.stringify({ id: ptyID, title: "Terminal 1" }), }) }) await page.route(/ghostty-web/, async (route) => { @@ -161,31 +155,27 @@ test("keeps newer composer focus while an explicit terminal open finishes", asyn test("focuses a terminal created from the new-terminal button", async ({ page }) => { const created = { count: 0 } - await page.route("**/api/pty*", (route) => { + await page.route("**/pty", (route) => { created.count += 1 - const next = created.count === 1 ? ptyInfo(ptyID, "Terminal 1") : ptyInfo(newPtyID, "Terminal 2") + const next = created.count === 1 ? { id: ptyID, title: "Terminal 1" } : { id: newPtyID, title: "Terminal 2" } return route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ location: ptyLocation(), data: next }), + body: JSON.stringify(next), }) }) - await page.route(`**/api/pty/${newPtyID}*`, (route) => - route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ location: ptyLocation(), data: ptyInfo(newPtyID, "Terminal 2") }), - }), + await page.route(`**/pty/${newPtyID}`, (route) => + route.fulfill({ status: 200, contentType: "application/json", body: "{}" }), ) - await page.route(`**/api/pty/${newPtyID}/connect-token*`, (route) => + await page.route(`**/pty/${newPtyID}/connect-token*`, (route) => route.fulfill({ status: 200, contentType: "application/json", headers: { "access-control-allow-origin": "*" }, - body: JSON.stringify({ location: ptyLocation(), data: { ticket: "e2e-ticket", expires_in: 60 } }), + body: JSON.stringify({ ticket: "e2e-ticket" }), }), ) - await page.routeWebSocket(new RegExp(`/api/pty/${newPtyID}/connect`), () => undefined) + await page.routeWebSocket(new RegExp(`/pty/${newPtyID}/connect`), () => undefined) await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await expectSessionTitle(page, "Terminal composer focus") @@ -217,11 +207,3 @@ function seedCachedTerminal(page: Page) { { terminalKey: `${base64Encode(directory)}/terminal.v1`, ptyID }, ) } - -function ptyLocation() { - return { directory, project: { id: projectID, directory } } -} - -function ptyInfo(id: string, title: string) { - return { id, title, command: "cmd.exe", args: [], cwd: directory, status: "running", pid: 1 } -} diff --git a/packages/app/e2e/regression/terminal-hidden.spec.ts b/packages/app/e2e/regression/terminal-hidden.spec.ts index 8e08d60ff2..73821580af 100644 --- a/packages/app/e2e/regression/terminal-hidden.spec.ts +++ b/packages/app/e2e/regression/terminal-hidden.spec.ts @@ -10,7 +10,6 @@ const title = "Hidden terminal regression" test("unmounts the terminal panel while it is hidden", async ({ page }) => { await page.setViewportSize({ width: 1400, height: 900 }) await mockOpenCodeServer(page, { - protocol: "v2", directory, project: { id: projectID, @@ -44,53 +43,17 @@ test("unmounts the terminal panel while it is hidden", async ({ page }) => { ], pageMessages: () => ({ items: [] }), }) - await page.route("**/api/pty*", (route) => + await page.route("**/pty", (route) => route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ - location: { directory, project: { id: projectID, directory } }, - data: { - id: "pty_hidden_terminal", - title: "Terminal 1", - command: "cmd.exe", - args: [], - cwd: directory, - status: "running", - pid: 1, - }, - }), + body: JSON.stringify({ id: "pty_hidden_terminal", title: "Terminal 1" }), }), ) - await page.route("**/api/pty/pty_hidden_terminal*", (route) => - route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - location: { directory, project: { id: projectID, directory } }, - data: { - id: "pty_hidden_terminal", - title: "Terminal 1", - command: "cmd.exe", - args: [], - cwd: directory, - status: "running", - pid: 1, - }, - }), - }), + await page.route("**/pty/pty_hidden_terminal", (route) => + route.fulfill({ status: 200, contentType: "application/json", body: "{}" }), ) - await page.route("**/api/pty/pty_hidden_terminal/connect-token*", (route) => - route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - location: { directory, project: { id: projectID, directory } }, - data: { ticket: "e2e-ticket", expires_in: 60 }, - }), - }), - ) - await page.routeWebSocket("**/api/pty/pty_hidden_terminal/connect", () => undefined) + await page.routeWebSocket("**/pty/pty_hidden_terminal/connect", () => undefined) await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await expectSessionTitle(page, title) diff --git a/packages/app/e2e/regression/terminal-tab-switch.spec.ts b/packages/app/e2e/regression/terminal-tab-switch.spec.ts index 165920753c..cbb72958ad 100644 --- a/packages/app/e2e/regression/terminal-tab-switch.spec.ts +++ b/packages/app/e2e/regression/terminal-tab-switch.spec.ts @@ -29,10 +29,6 @@ test("keeps the terminal session alive when switching session tabs in a workspac const terminal = page.locator('[data-component="terminal"]') await expect(terminal).toBeVisible() await expect.poll(() => connections.length).toBe(1) - const connection = new URL(connections[0]!) - expect(connection.pathname).toBe(`/api/pty/${ptyID}/connect`) - expect(connection.searchParams.get("location[directory]")).toBe(directory) - expect(connection.searchParams.get("ticket")).toBeNull() await writeProbe(page) await switchTab(page, titleB) @@ -66,7 +62,6 @@ async function readProbe(page: Page) { async function setup(page: Page) { await mockOpenCodeServer(page, { - protocol: "v2", directory, project: { id: projectID, @@ -90,33 +85,26 @@ async function setup(page: Page) { sessions: [session(sessionA, titleA, 1700000000000), session(sessionB, titleB, 1700000001000)], pageMessages: () => ({ items: [] }), }) - await page.route("**/api/pty*", (route) => + await page.route("**/pty", (route) => route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ location: ptyLocation(), data: ptyInfo() }), + body: JSON.stringify({ id: ptyID, title: "Terminal 1" }), }), ) - await page.route(`**/api/pty/${ptyID}*`, (route) => + await page.route(`**/pty/${ptyID}`, (route) => + route.fulfill({ status: 200, contentType: "application/json", body: "{}" }), + ) + await page.route(`**/pty/${ptyID}/connect-token*`, (route) => route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ location: ptyLocation(), data: ptyInfo() }), - }), - ) - await page.route(`**/api/pty/${ptyID}/connect-token*`, (route) => { - expect(route.request().headers()["x-opencode-ticket"]).toBe("1") - const url = new URL(route.request().url()) - expect(url.searchParams.get("location[directory]")).toBe(directory) - return route.fulfill({ status: 200, contentType: "application/json", headers: { "access-control-allow-origin": "*" }, - body: JSON.stringify({ location: ptyLocation(), data: { ticket: "e2e-ticket", expires_in: 60 } }), - }) - }) + body: JSON.stringify({ ticket: "e2e-ticket" }), + }), + ) const connections: string[] = [] - await page.routeWebSocket(new RegExp(`/api/pty/${ptyID}/connect`), (ws) => { + await page.routeWebSocket(new RegExp(`/pty/${ptyID}/connect`), (ws) => { connections.push(ws.url()) }) @@ -155,11 +143,3 @@ function session(id: string, title: string, created: number) { function sessionHref(sessionID: string) { return `/server/${base64Encode(server)}/session/${sessionID}` } - -function ptyLocation() { - return { directory, project: { id: projectID, directory } } -} - -function ptyInfo() { - return { id: ptyID, title: "Terminal 1", command: "cmd.exe", args: [], cwd: directory, status: "running", pid: 1 } -} diff --git a/packages/app/e2e/smoke/session-timeline.fixture.ts b/packages/app/e2e/smoke/session-timeline.fixture.ts index 3dce37cafd..beb2d7cf75 100644 --- a/packages/app/e2e/smoke/session-timeline.fixture.ts +++ b/packages/app/e2e/smoke/session-timeline.fixture.ts @@ -120,7 +120,7 @@ function toolPart( outputLength = 160, ): MessagePart { const metadata = - tool === "apply_patch" + tool === "patch" ? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] } : tool === "edit" || tool === "write" ? { @@ -199,7 +199,7 @@ function turn(index: number): Message[] { ? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)] : []), ...(index % 8 === 0 - ? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)] + ? [toolPart(index, 8, "patch", { files: [`src/generated/patch-${index}.ts`] }, 620)] : []), ...(index % 7 === 0 ? [toolPart(index, 4, "bash", { command: "bun typecheck" }, 620)] : []), ...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []), @@ -229,7 +229,6 @@ const sourceMessages = Array.from({ length: 12 }, (_, index) => [ ]).flat() function renderable(part: MessagePart) { - if (part.type === "tool" && part.tool === "todowrite") return false if (part.type === "text") return !!part.text.trim() if (part.type === "reasoning") return !!part.text.trim() return part.type !== "step-start" && part.type !== "step-finish" && part.type !== "patch" diff --git a/packages/app/e2e/smoke/session-timeline.spec.ts b/packages/app/e2e/smoke/session-timeline.spec.ts index bdf3f55bdc..a73cc0ccdd 100644 --- a/packages/app/e2e/smoke/session-timeline.spec.ts +++ b/packages/app/e2e/smoke/session-timeline.spec.ts @@ -736,5 +736,5 @@ async function switchTitlebarSession(page: Page, sessionID: string, title: strin } async function expectSessionReady(page: Page) { - await expectAppVisible(page.getByRole("textbox", { name: "Prompt" })) + await expectAppVisible(page.getByRole("textbox", { name: /Ask anything/i })) } diff --git a/packages/app/e2e/user-story/model-selection-flow.spec.ts b/packages/app/e2e/user-story/model-selection-flow.spec.ts deleted file mode 100644 index 22b8bb41fe..0000000000 --- a/packages/app/e2e/user-story/model-selection-flow.spec.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { expect, test } from "@playwright/test" -import { mockOpenCodeServer } from "../utils/mock-server" -import { expectAppVisible } from "../utils/waits" - -const directory = "C:/OpenCode/NewProject" - -test("creates a session in a new project, connects OpenCode Go, and selects its model", async ({ page }) => { - let connectedGo = false - let pendingGo = false - const connections: Array<{ integrationID: string; body: unknown }> = [] - - await mockOpenCodeServer(page, { - directory, - project: { - id: "proj_model_selection_flow", - worktree: directory, - vcs: "git", - name: "NewProject", - time: { created: 1_700_000_000_000, updated: 1_700_000_000_000 }, - sandboxes: [], - }, - provider: () => ({ - all: [ - { - id: "opencode", - name: "OpenCode", - models: { - "free-model": { - id: "free-model", - name: "Free Model", - cost: { input: 0, output: 0 }, - limit: { context: 200_000 }, - }, - }, - }, - { - id: "opencode-go", - name: "OpenCode Go", - models: { - "go-model-1": { - id: "go-model-1", - name: "Go Model 1", - cost: { input: 1, output: 1 }, - limit: { context: 200_000 }, - }, - }, - }, - ], - connected: connectedGo ? ["opencode", "opencode-go"] : ["opencode"], - default: { providerID: "opencode", modelID: "free-model" }, - }), - integrationMethods: { "opencode-go": [{ type: "api", label: "API key" }] }, - onConnectKey: (input) => { - connections.push(input) - if (input.integrationID === "opencode-go") pendingGo = true - }, - onInstanceDispose: () => { - if (pendingGo) connectedGo = true - }, - sessions: [], - pageMessages: () => ({ items: [] }), - fileList: (path) => - path ? [] : [{ name: "NewProject", path: "NewProject", absolute: directory, type: "directory", ignored: false }], - findFiles: () => ["NewProject"], - }) - await page.addInitScript(() => { - localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) - localStorage.setItem("opencode.global.dat:server", JSON.stringify({ projects: { local: [] } })) - }) - - await page.goto("/") - const addProject = page.locator('[data-action="home-add-project-row"]') - await expectAppVisible(addProject) - await addProject.click() - await page.locator("[data-directory-path]").click() - - await page.locator('[data-action="home-new-session"]').click() - await expectAppVisible(page.locator('[data-component="prompt-input-v2"]')) - - const modelControl = page.locator('[data-action="prompt-model"]') - await modelControl.click() - await expect(page.locator('[data-section="free-models"]')).toContainText("Free models provided by OpenCode") - - await page.locator('[data-provider-id="opencode-go"]').click() - await page.locator('[data-input="provider-api-key"]').fill("mock-go-api-key") - await page.locator('[data-action="provider-connect-submit"]').click() - await expect(page.locator('[data-component="dialog-v2"]')).toHaveCount(0) - expect(connections).toEqual([{ integrationID: "opencode-go", body: { type: "api", key: "mock-go-api-key" } }]) - - await expect(modelControl).toHaveAttribute("data-control-type", "popover") - await modelControl.click() - const goModel = page.locator('[data-option-key="opencode-go:go-model-1"]') - await expect(goModel).toBeVisible() - await goModel.click() - - await expect(modelControl).toContainText("Go Model 1") -}) diff --git a/packages/app/e2e/utils/mock-server.ts b/packages/app/e2e/utils/mock-server.ts index 76987421b6..34c60ba7f4 100644 --- a/packages/app/e2e/utils/mock-server.ts +++ b/packages/app/e2e/utils/mock-server.ts @@ -4,11 +4,7 @@ const emptyList = new Set(["/skill", "/command", "/lsp", "/formatter", "/vcs/sta const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp", "/experimental/resource"]) export interface MockServerConfig { - protocol?: "v1" | "v2" - provider: unknown | (() => unknown) - integrationMethods?: Record - onConnectKey?: (input: { integrationID: string; body: unknown }) => void - onInstanceDispose?: () => void + provider: unknown directory: string project: unknown sessions: ({ id: string } & Record)[] @@ -21,19 +17,19 @@ export interface MockServerConfig { onMessage?: (input: { sessionID: string; messageID: string }) => void events?: () => unknown[] eventRetry?: number - todos?: (sessionID: string) => unknown[] permissions?: unknown[] | (() => unknown[]) questions?: unknown[] | (() => unknown[]) fileList?: (path: string) => unknown | Promise fileContent?: (path: string) => unknown | Promise findFiles?: (input: { query: string; dirs?: string; limit?: number }) => unknown - sessionStatus?: Record | (() => Record) + sessionStatus?: unknown } export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { const cursors = new Map() let nextCursor = 0 const staticRoutes: Record = { + "/provider": config.provider, "/path": { state: config.directory, config: config.directory, @@ -57,47 +53,14 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { if (url.port !== targetPort && url.port !== appPort) return route.fallback() const path = url.pathname - if (path === "/global/event" || path === "/event" || path === "/api/event") { - const events = config.events?.() - return sse( - route, - path === "/api/event" - ? [{ id: "evt_mock_connected", type: "server.connected", data: {} }, ...(events?.map(currentEvent) ?? [])] - : [ - ...(path === "/global/event" - ? [{ payload: { id: "evt_mock_connected", type: "server.connected", properties: {} } }] - : []), - ...(events ?? []), - ], - config.eventRetry, - ) - } - if (path === "/global/health") - return config.protocol === "v2" ? json(route, {}, undefined, 404) : json(route, { healthy: true }) - if (path === "/api/health" && config.protocol === "v2") - return json(route, { healthy: true, version: "2.0.0", pid: 1 }) + if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry) + if (path === "/global/health") return json(route, { healthy: true }) if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: true }) - if (path === "/provider") - return json(route, typeof config.provider === "function" ? config.provider() : config.provider) - if (path === "/provider/auth") return json(route, config.integrationMethods ?? {}) - const legacyAuth = path.match(/^\/auth\/([^/]+)$/)?.[1] - if (legacyAuth && route.request().method() === "PUT") { - config.onConnectKey?.({ integrationID: legacyAuth, body: route.request().postDataJSON() }) - return json(route, true) - } - if (path === "/instance/dispose" && route.request().method() === "POST") { - config.onInstanceDispose?.() - return json(route, true) - } if (path === "/permission") return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? [])) if (path === "/question") return json(route, typeof config.questions === "function" ? config.questions() : (config.questions ?? [])) - if (path === "/session/status") - return json( - route, - typeof config.sessionStatus === "function" ? config.sessionStatus() : (config.sessionStatus ?? {}), - ) + if (path === "/session/status") return json(route, config.sessionStatus ?? {}) if (path === "/vcs/diff" && config.vcsDiff) return json(route, config.vcsDiff) if (path === "/file" && config.fileList) return json(route, await config.fileList(url.searchParams.get("path") ?? "")) @@ -120,138 +83,10 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { }, data: [], }) - if (path === "/api/agent") - return json(route, { - location: location(config), - data: [ - { - id: "build", - name: "Build", - mode: "primary", - hidden: false, - request: { settings: {}, headers: {}, body: {} }, - permissions: [], - }, - ], - }) - if (path === "/api/command") return json(route, { location: location(config), data: [] }) - if (path === "/api/mcp") return json(route, { location: location(config), data: [] }) - if (path === "/api/mcp/resource") - return json(route, { location: location(config), data: { resources: [], templates: [] } }) - const integration = path.match(/^\/api\/integration\/([^/]+)$/)?.[1] - if (integration && route.request().method() === "GET") - return json(route, { - location: location(config), - data: { id: integration, name: integration, methods: [{ type: "key", label: "API key" }], connections: [] }, - }) - const integrationConnect = path.match(/^\/api\/integration\/([^/]+)\/connect\/key$/)?.[1] - if (integrationConnect && route.request().method() === "POST") { - config.onConnectKey?.({ integrationID: integrationConnect, body: route.request().postDataJSON() }) - return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) - } - if (path === "/api/project") return json(route, [config.project]) - if (path === "/api/project/current") - return json(route, { id: (config.project as { id?: string }).id, directory: config.directory }) - if (path.startsWith("/api/project/") && route.request().method() === "PATCH") return json(route, config.project) - if (path === "/api/path") - return json(route, { - state: config.directory, - config: config.directory, - worktree: config.directory, - directory: config.directory, - home: "C:/OpenCode", - }) - if (path === "/api/permission/request") - return json(route, { - location: location(config), - data: (typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? [])).map( - currentPermission, - ), - }) - if (path === "/api/question/request") - return json(route, { - location: location(config), - data: typeof config.questions === "function" ? config.questions() : (config.questions ?? []), - }) - if (path === "/api/vcs") - return json(route, { location: location(config), data: { branch: "main", defaultBranch: "main" } }) - if (path === "/api/vcs/status") return json(route, { location: location(config), data: [] }) - if (path === "/api/vcs/diff") return json(route, { location: location(config), data: config.vcsDiff ?? [] }) - if (path === "/api/pty/shells") return json(route, { location: location(config), data: [] }) - if (/^\/api\/pty\/[^/]+\/connect-token$/.test(path)) - return json(route, { location: location(config), data: { ticket: "e2e-ticket", expires_in: 60 } }) if (emptyObject.has(path)) return json(route, {}) if (emptyList.has(path)) return json(route, []) - if (path === "/api/session") { - const directory = url.searchParams.get("directory") - const parentID = url.searchParams.get("parentID") - const limit = Number(url.searchParams.get("limit") ?? 50) - const offset = Number(url.searchParams.get("cursor") ?? 0) - const sessions = config.sessions - .filter((session) => !directory || session.directory === directory) - .filter((session) => parentID !== "null" || session.parentID === undefined) - .filter((session) => { - const search = url.searchParams.get("search")?.toLowerCase() - return ( - !search || - String(session.title ?? "") - .toLowerCase() - .includes(search) - ) - }) - const ordered = url.searchParams.get("order") === "asc" ? sessions.toReversed() : sessions - const data = ordered.slice(offset, offset + limit) - const next = offset + limit < ordered.length ? String(offset + limit) : undefined - return json(route, { - data: data.map((session) => currentSession(session, config.directory)), - cursor: { next }, - }) - } - if (path === "/api/session/active") { - const statuses = (config.sessionStatus ?? {}) as Record - return json(route, { - data: Object.fromEntries( - Object.entries(statuses).flatMap(([id, status]) => - status.type === "idle" ? [] : [[id, { type: "running" }]], - ), - ), - }) - } - if (/^\/api\/session\/[^/]+\/shell$/.test(path) && route.request().method() === "POST") { - return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) - } - if (/^\/api\/session\/[^/]+\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST") { - return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) - } - if (/^\/api\/session\/[^/]+\/permission\/[^/]+\/reply$/.test(path) && route.request().method() === "POST") { - return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) - } - if (/^\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST") { - return json(route, true) - } - if (/^\/session\/[^/]+\/permissions\/[^/]+$/.test(path) && route.request().method() === "POST") { - return json(route, true) - } - if ( - /^\/api\/session\/[^/]+\/(archive|rename|interrupt|revert\/clear|revert\/commit)$/.test(path) && - route.request().method() === "POST" - ) { - return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) - } - if (/^\/api\/session\/[^/]+$/.test(path) && route.request().method() === "DELETE") { - return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) - } if (path in staticRoutes) return json(route, staticRoutes[path]) - const currentSessionMatch = path.match(/^\/api\/session\/([^/]+)$/) - if (currentSessionMatch) { - const session = config.sessions.find((item) => item.id === currentSessionMatch[1]) - if (!session) return json(route, { error: "Session not found" }, undefined, 404) - return json(route, { - data: currentSession(session, config.directory), - }) - } - const sessionMatch = path.match(/^\/session\/([^/]+)$/) if (sessionMatch) { const session = config.sessions.find((s) => s.id === sessionMatch[1]) @@ -270,28 +105,8 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { return json(route, message) } - const todoMatch = path.match(/^\/session\/([^/]+)\/todo$/) - if (todoMatch) return json(route, config.todos?.(todoMatch[1]!) ?? []) if (/^\/session\/[^/]+\/(children|diff)$/.test(path)) return json(route, []) - const currentMessagesMatch = path.match(/^\/api\/session\/([^/]+)\/message$/) - if (currentMessagesMatch) { - const token = url.searchParams.get("cursor") ?? undefined - const before = token ? cursors.get(token) : undefined - if (token && !before) return json(route, { error: "Invalid cursor" }, undefined, 400) - config.onMessages?.({ sessionID: currentMessagesMatch[1], before, phase: "start" }) - await config.beforeMessagesResponse?.({ sessionID: currentMessagesMatch[1]!, before }) - if (config.messageDelay !== undefined) await new Promise((resolve) => setTimeout(resolve, config.messageDelay)) - const pageData = config.pageMessages(currentMessagesMatch[1], Number(url.searchParams.get("limit") ?? 50), before) - config.onMessages?.({ sessionID: currentMessagesMatch[1], before, phase: "end" }) - const cursor = pageData.cursor ? `cursor_${++nextCursor}` : undefined - if (cursor) cursors.set(cursor, pageData.cursor!) - return json(route, { - data: pageData.items.map(currentMessage).reverse(), - cursor: { next: cursor }, - }) - } - const messagesMatch = path.match(/^\/session\/([^/]+)\/message$/) if (messagesMatch) { const token = url.searchParams.get("before") ?? undefined @@ -314,115 +129,6 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { }) } -function location(config: MockServerConfig) { - return { - directory: config.directory, - project: { id: (config.project as { id?: string }).id, directory: config.directory }, - } -} - -function currentPermission(value: unknown) { - const permission = value as Record - if (permission.action) return permission - const tool = permission.tool as { messageID?: string; callID?: string } | undefined - return { - id: permission.id, - sessionID: permission.sessionID, - action: permission.permission, - resources: permission.patterns ?? [], - save: permission.always, - metadata: permission.metadata, - source: - tool?.messageID && tool.callID ? { type: "tool", messageID: tool.messageID, callID: tool.callID } : undefined, - } -} - -export function currentSession(session: { id: string } & Record, fallbackDirectory?: string) { - const time = session.time && typeof session.time === "object" ? session.time : {} - return { - id: session.id, - parentID: session.parentID, - projectID: session.projectID ?? "project", - agent: session.agent ?? "build", - model: session.model ?? { id: "mock-model", providerID: "mock-provider" }, - cost: session.cost ?? 0, - tokens: session.tokens ?? { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { - created: "created" in time && typeof time.created === "number" ? time.created : 0, - updated: "updated" in time && typeof time.updated === "number" ? time.updated : 0, - ...(session.time && typeof session.time === "object" && "archived" in session.time - ? { archived: session.time.archived } - : {}), - }, - title: session.title ?? session.id, - location: { - directory: typeof session.directory === "string" ? session.directory : fallbackDirectory, - ...(typeof session.workspaceID === "string" ? { workspaceID: session.workspaceID } : {}), - }, - subpath: session.path, - revert: session.revert, - } -} - -function currentMessage(value: unknown) { - const item = value as { - info: Record & { id: string; role: "user" | "assistant"; time: { created: number } } - parts: Array & { type: string }> - } - if (item.info.role === "user") { - return { - id: item.info.id, - type: "user", - time: item.info.time, - text: item.parts - .flatMap((part) => (part.type === "text" && typeof part.text === "string" ? [part.text] : [])) - .join("\n"), - } - } - return { - id: item.info.id, - type: "assistant", - time: item.info.time, - agent: item.info.agent ?? "build", - model: { id: item.info.modelID ?? "model", providerID: item.info.providerID ?? "provider" }, - cost: item.info.cost, - tokens: item.info.tokens, - error: item.info.error, - content: item.parts.flatMap((part) => { - if (part.type === "text" || part.type === "reasoning") return [{ type: part.type, text: part.text ?? "" }] - if (part.type !== "tool") return [] - const state = part.state as Record - return [ - { - type: "tool", - id: part.id, - name: part.tool, - time: state.time ?? { created: item.info.time.created }, - state: - state.status === "pending" - ? { status: "streaming", input: state.raw ?? JSON.stringify(state.input ?? {}) } - : state.status === "completed" - ? { - status: "completed", - input: state.input ?? {}, - structured: state.metadata ?? {}, - content: [{ type: "text", text: state.output ?? "" }], - } - : state.status === "error" - ? { - status: "error", - input: state.input ?? {}, - structured: state.metadata ?? {}, - content: [], - error: { type: "ToolError", message: state.error ?? "Tool failed" }, - } - : { status: "running", input: state.input ?? {}, structured: state.metadata ?? {}, content: [] }, - }, - ] - }), - } -} - function json(route: Route, body: unknown, headers?: Record, status = 200) { return route.fulfill({ status, @@ -443,18 +149,3 @@ function sse(route: Route, events?: unknown[], retry?: number) { body: `${retry === undefined ? "" : `retry: ${retry}\n\n`}${events?.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") || ": ok\n\n"}`, }) } - -function currentEvent(input: unknown) { - if (!input || typeof input !== "object" || !("payload" in input)) return input - const envelope = input as { directory?: string; payload?: unknown } - if (!envelope.payload || typeof envelope.payload !== "object") return input - const payload = envelope.payload as { id?: string; type?: string; properties?: unknown } - if (!payload.type) return input - return { - id: payload.id ?? `evt_mock_${Date.now()}`, - created: Date.now(), - type: payload.type, - data: payload.properties ?? {}, - location: envelope.directory && envelope.directory !== "global" ? { directory: envelope.directory } : undefined, - } -} diff --git a/packages/app/e2e/utils/sse-transport.ts b/packages/app/e2e/utils/sse-transport.ts index b0e3b74c6d..55420485f3 100644 --- a/packages/app/e2e/utils/sse-transport.ts +++ b/packages/app/e2e/utils/sse-transport.ts @@ -3,7 +3,7 @@ import type { Page } from "@playwright/test" export type SseConnectionRecord = { id: number url: string - path: "/global/event" | "/event" | "/api/event" + path: "/global/event" | "/event" headers: Record openedAt: number endedAt?: number @@ -93,21 +93,6 @@ export async function installSseTransport( eventOptions.retry === undefined ? "" : `retry: ${eventOptions.retry}\n`, `data: ${JSON.stringify(payload)}\n\n`, ].join("") - const currentEvent = (input: unknown) => { - if (!input || typeof input !== "object" || !("payload" in input)) return input - const envelope = input as { directory?: string; payload?: unknown } - if (!envelope.payload || typeof envelope.payload !== "object") return input - const payload = envelope.payload as { id?: string; type?: string; properties?: unknown } - if (!payload.type) return input - return { - id: payload.id ?? `evt_mock_${Date.now()}`, - created: Date.now(), - type: payload.type, - data: payload.properties ?? {}, - location: - envelope.directory && envelope.directory !== "global" ? { directory: envelope.directory } : undefined, - } - } const acknowledge = ( connection: Connection, bytes: number, @@ -155,13 +140,15 @@ export async function installSseTransport( output.forEach((chunk) => connection.controller.enqueue(chunk)) return acknowledge(connection, input.bytes.length, output.length) } - const encoded = input.deliveries.map((delivery) => { - const payload = connection.path === "/api/event" ? currentEvent(delivery.payload) : delivery.payload - return { delivery, payload, bytes: encoder.encode(frame(payload, delivery.options)) } - }) + const encoded = input.deliveries.map((delivery) => ({ + delivery, + bytes: encoder.encode(frame(delivery.payload, delivery.options)), + })) encoded.forEach((item) => marker(item.delivery.options?.marker)) if (input.burst) { - const bytes = encoder.encode(encoded.map((item) => frame(item.payload, item.delivery.options)).join("")) + const bytes = encoder.encode( + encoded.map((item) => frame(item.delivery.payload, item.delivery.options)).join(""), + ) connection.controller.enqueue(bytes) return encoded.map((item) => acknowledge(connection, item.bytes.byteLength, 1, item.delivery.options?.id)) } @@ -174,10 +161,7 @@ export async function installSseTransport( const fetch = (input: RequestInfo | URL, init?: RequestInit) => { const request = new Request(input, init) const url = new URL(request.url) - if ( - url.origin !== server || - (url.pathname !== "/global/event" && url.pathname !== "/event" && url.pathname !== "/api/event") - ) + if (url.origin !== server || (url.pathname !== "/global/event" && url.pathname !== "/event")) return originalFetch(request) const id = ++nextConnectionID @@ -193,18 +177,6 @@ export async function installSseTransport( record.controller = controller connections.push(record) if (retry !== undefined) controller.enqueue(encoder.encode(`retry: ${retry}\n\n`)) - if (url.pathname === "/api/event") - controller.enqueue( - encoder.encode(frame({ id: `evt_mock_connected_${id}`, type: "server.connected", data: {} })), - ) - if (url.pathname === "/global/event") - controller.enqueue( - encoder.encode( - frame({ - payload: { id: `evt_mock_connected_${id}`, type: "server.connected", properties: {} }, - }), - ), - ) request.signal.addEventListener( "abort", () => { diff --git a/packages/app/package.json b/packages/app/package.json index 2dd4a05d86..482fe7ff85 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.18.11", + "version": "1.18.3", "description": "", "type": "module", "exports": { @@ -19,9 +19,9 @@ "build": "vite build", "serve": "vite preview", "test": "bun run test:unit && bun run test:browser", - "test:unit": "bun test --conditions=solid --only-failures --preload ./happydom.ts ./src", + "test:unit": "bun test --only-failures --preload ./happydom.ts ./src", "test:browser": "bun test --conditions=browser --preload ./happydom.ts ./test-browser", - "test:unit:watch": "bun test --conditions=solid --watch --preload ./happydom.ts ./src", + "test:unit:watch": "bun test --watch --preload ./happydom.ts ./src", "test:e2e": "playwright test", "test:e2e:local": "playwright test", "test:e2e:ui": "playwright test --ui", @@ -53,7 +53,6 @@ "@dnd-kit/helpers": "0.5.0", "@dnd-kit/solid": "0.5.0", "@kobalte/core": "catalog:", - "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "workspace:*", @@ -82,7 +81,7 @@ "diff": "catalog:", "effect": "catalog:", "fuzzysort": "catalog:", - "ghostty-web": "github:anomalyco/ghostty-web#83c0a07b8628b748aed073b232cb4b52a6ca11c1", + "ghostty-web": "github:anomalyco/ghostty-web#513463a6f1190253057e8a3f0dac8f6ee8393553", "luxon": "catalog:", "marked": "catalog:", "marked-shiki": "catalog:", diff --git a/packages/app/src/addons/serialize.test.ts b/packages/app/src/addons/serialize.test.ts index d45af34c43..6828e60f84 100644 --- a/packages/app/src/addons/serialize.test.ts +++ b/packages/app/src/addons/serialize.test.ts @@ -37,14 +37,6 @@ function writeAndWait(term: Terminal, data: string): Promise { } describe("SerializeAddon", () => { - test("preserves color scheme reporting mode", async () => { - const { term, addon } = createTerminal() - await writeAndWait(term, "\x1b[?2031h") - - expect(addon.serialize().startsWith("\x1b[?2031h")).toBe(true) - expect(addon.serialize({ excludeModes: true }).startsWith("\x1b[?2031h")).toBe(false) - }) - describe("ANSI color preservation", () => { test("should preserve text attributes (bold, italic, underline)", async () => { const { term, addon } = createTerminal() diff --git a/packages/app/src/addons/serialize.ts b/packages/app/src/addons/serialize.ts index 515153488c..3823fb443a 100644 --- a/packages/app/src/addons/serialize.ts +++ b/packages/app/src/addons/serialize.ts @@ -89,13 +89,6 @@ const getTerminalBuffers = (value: ITerminalCore): TerminalBuffers | undefined = return { active, normal, alternate } } -const getTerminalMode = (value: ITerminalCore, mode: number) => { - if (!isRecord(value)) return false - const terminal = value.wasmTerm - if (!isRecord(terminal) || typeof terminal.getMode !== "function") return false - return terminal.getMode(mode) === true -} - // ============================================================================ // Types // ============================================================================ @@ -551,8 +544,7 @@ export class SerializeAddon implements ITerminalAddon { return "" } - let content = !options?.excludeModes && getTerminalMode(this._terminal, 2031) ? "\u001b[?2031h" : "" - content += options?.range + let content = options?.range ? this._serializeBufferByRange(normalBuffer, options.range, true) : this._serializeBufferByScrollback(normalBuffer, options?.scrollback) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index f47c432e42..5592e2f4ea 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -9,16 +9,7 @@ import { Font } from "@opencode-ai/ui/font" import { Splash } from "@opencode-ai/ui/logo" import { ThemeProvider } from "@opencode-ai/ui/theme/context" import { MetaProvider } from "@solidjs/meta" -import { - type BaseRouterProps, - Navigate, - Route, - Router, - useLocation, - useNavigate, - useParams, - useSearchParams, -} from "@solidjs/router" +import { type BaseRouterProps, Navigate, Route, Router, useNavigate, useParams, useSearchParams } from "@solidjs/router" import { QueryClient, QueryClientProvider } from "@tanstack/solid-query" import { Effect } from "effect" import { base64Encode } from "@opencode-ai/core/util/encode" @@ -38,7 +29,6 @@ import { Show, } from "solid-js" import { Dynamic } from "solid-js/web" -import { makeEventListener } from "@solid-primitives/event-listener" import { CommandProvider, useCommand, type CommandOption } from "@/context/command" import { CommentsProvider } from "@/context/comments" import { FileProvider } from "@/context/file" @@ -67,8 +57,7 @@ import { legacySessionHref, legacySessionServer, requireServerKey, sessionHref } import { createSessionLineage } from "@/pages/session/session-lineage" import { SessionPage, SessionRouteErrorBoundary, TargetSessionRouteContent } from "@/pages/session" -import { NewHome } from "@/pages/home" -import { LegacyHome } from "@/pages/home/legacy-home" +import { NewHome, LegacyHome } from "@/pages/home" const NewSession = lazy(() => import("@/pages/new-session")) @@ -237,30 +226,6 @@ function UiI18nBridge(props: ParentProps) { return {props.children} } -function LayoutCompatibility(props: ParentProps) { - const global = useGlobal() - const navigate = useNavigate() - const server = useServer() - const settings = useSettings() - - createEffect(() => { - if (settings.general.newLayoutDesigns()) return - const current = server.current - if (!current) return - const protocol = global.ensureServerCtx(current).sdk.protocolKind() - if (protocol !== "v2") return - const next = global.servers.list().find((s) => { - if (ServerConnection.key(s) === ServerConnection.key(current)) return false - return global.ensureServerCtx(s).sdk.protocolKind() !== "v2" - }) - if (!next) return - navigate("/") - queueMicrotask(() => server.setActive(ServerConnection.key(next))) - }) - - return <>{props.children} -} - declare global { interface Window { __OPENCODE__?: { diff --git a/packages/app/src/components/command-palette.ts b/packages/app/src/components/command-palette.ts index 8014ea1c43..59d3cbd1da 100644 --- a/packages/app/src/components/command-palette.ts +++ b/packages/app/src/components/command-palette.ts @@ -1,6 +1,5 @@ import { getFilename } from "@opencode-ai/core/util/path" -import type { Project } from "@opencode-ai/sdk/v2/client" -import type { SessionInfo } from "@opencode-ai/client/promise" +import type { GlobalSession, Project } from "@opencode-ai/sdk/v2/client" import { useDialog } from "@opencode-ai/ui/context/dialog" import { createMemo, onCleanup } from "solid-js" import { commandPaletteOptions, useCommand, type CommandOption } from "@/context/command" @@ -14,7 +13,6 @@ import { useTabs } from "@/context/tabs" import { displayName, projectForSession } from "@/pages/layout/helpers" import { createSessionTabs } from "@/pages/session/helpers" import { useSessionLayout } from "@/pages/session/session-layout" -import { normalizeSessionInfo } from "@/utils/session" export type CommandPaletteEntry = { id: string @@ -146,7 +144,8 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on server: ServerConnection.key(serverSDK.server), opened: serverCtx.projects.list, stored: () => serverCtx.sync.data.project, - load: (search, signal) => serverSDK.api.session.list({ parentID: null, search, limit: 50 }, { signal }), + load: (search, signal) => + serverSDK.client.experimental.session.list({ roots: true, search, limit: 50 }, { signal }), untitled: () => language.t("command.session.new"), category: () => language.t("command.category.session"), }) @@ -220,7 +219,7 @@ export function createServerSessionEntries(props: { server: ServerConnection.Key opened: () => LocalProject[] stored: () => Project[] - load: (search: string, signal: AbortSignal) => Promise<{ data: SessionInfo[] }> + load: (search: string, signal: AbortSignal) => Promise<{ data?: GlobalSession[] }> untitled: () => string category: () => string }) { @@ -256,8 +255,7 @@ export function createServerSessionEntries(props: { return props .load(search, current.signal) .then((result) => - result.data - .map(normalizeSessionInfo) + (result.data ?? []) .filter((session) => !session.time.archived) .map((session) => { const project = @@ -266,7 +264,7 @@ export function createServerSessionEntries(props: { id: `session:${props.server}:${session.id}`, type: "session" as const, title: session.title || props.untitled(), - description: project ? displayName(project) : getFilename(session.directory), + description: project ? displayName(project) : session.project?.name || getFilename(session.directory), category: props.category(), directory: session.directory, sessionID: session.id, diff --git a/packages/app/src/components/debug-bar.tsx b/packages/app/src/components/debug-bar.tsx index adadeda303..e55e128b82 100644 --- a/packages/app/src/components/debug-bar.tsx +++ b/packages/app/src/components/debug-bar.tsx @@ -5,7 +5,6 @@ import { makeEventListener } from "@solid-primitives/event-listener" import { Tooltip } from "@opencode-ai/ui/tooltip" import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" import { useLanguage } from "@/context/language" -import { usePlatform } from "@/context/platform" type Mem = Performance & { memory?: { @@ -66,7 +65,7 @@ function Cell(props: {
-
- {props.label} -
-
- {props.value} -
+ {props.label} +
+
+ {props.value}
) @@ -116,55 +107,8 @@ function Cell(props: { ) } -function FocusCell(props: { active: boolean; inline?: boolean; onClick: () => void }) { - const content = () => ( - - ) - - if (props.inline) { - return ( - - {content()} - - ) - } - - return ( - - {content()} - - ) -} - export function DebugBar(props: { inline?: boolean } = {}) { const language = useLanguage() - const platform = usePlatform() const location = useLocation() const routing = useIsRouting() const [state, setState] = createStore({ @@ -172,7 +116,6 @@ export function DebugBar(props: { inline?: boolean } = {}) { delay: undefined as number | undefined, fps: undefined as number | undefined, gap: undefined as number | undefined, - focus: false, heap: { limit: undefined as number | undefined, used: undefined as number | undefined, @@ -199,16 +142,6 @@ export function DebugBar(props: { inline?: boolean } = {}) { } const longv = () => (state.long.count === undefined ? na() : `${time(state.long.block) ?? na()}/${state.long.count}`) const navv = () => (state.nav.pending ? "..." : (time(state.nav.dur) ?? na())) - const toggleFocus = async () => { - if (!platform.setForceFocus) return - const enabled = !state.focus - await platform.setForceFocus(enabled) - setState("focus", enabled) - } - - onCleanup(() => { - if (state.focus) void platform.setForceFocus?.(false).catch(() => undefined) - }) let prev = "" let start = 0 @@ -557,11 +490,8 @@ export function DebugBar(props: { inline?: boolean } = {}) { bad={bad(heap(), 0.8)} dim={state.heap.used === undefined} inline={props.inline} - wide={!platform.setForceFocus} + wide /> - {platform.setForceFocus && ( - void toggleFocus()} /> - )} ) diff --git a/packages/app/src/components/dialog-command-palette-v2.tsx b/packages/app/src/components/dialog-command-palette-v2.tsx index e996fd0be7..85ca44ae69 100644 --- a/packages/app/src/components/dialog-command-palette-v2.tsx +++ b/packages/app/src/components/dialog-command-palette-v2.tsx @@ -79,7 +79,8 @@ export function DialogHomeCommandPaletteV2(props: { server: ServerConnection.key(props.server), opened: serverCtx.projects.list, stored: () => serverCtx.sync.data.project, - load: (search, signal) => serverCtx.sdk.api.session.list({ parentID: null, search, limit: 50 }, { signal }), + load: (search, signal) => + serverCtx.sdk.client.experimental.session.list({ roots: true, search, limit: 50 }, { signal }), untitled: () => language.t("command.session.new"), category: () => language.t("command.category.session"), }) diff --git a/packages/app/src/components/dialog-connect-provider.stories.tsx b/packages/app/src/components/dialog-connect-provider.stories.tsx deleted file mode 100644 index 3aec6c9de3..0000000000 --- a/packages/app/src/components/dialog-connect-provider.stories.tsx +++ /dev/null @@ -1,73 +0,0 @@ -// @ts-nocheck -import { Button } from "@opencode-ai/ui/button" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { QueryClient, QueryClientProvider } from "@tanstack/solid-query" -import { mockProviderAuth } from "@/context/server-sync" -import { onCleanup, onMount } from "solid-js" -import { DialogConnectProvider, useProviderConnectController } from "./dialog-connect-provider" - -function ConnectProviderDialogStory() { - const dialog = useDialog() - const open = () => dialog.show(() => ) - - onMount(open) - - return ( - - ) -} - -function ProviderConnectionDialogStory(props) { - onCleanup(mockProviderAuth(props.provider, props.methods)) - const dialog = useDialog() - const controller = useProviderConnectController() - controller.select(props.provider) - const open = () => dialog.show(() => ) - - onMount(open) - - return ( - - ) -} - -function renderConnection(provider, methods) { - return () => ( - - - - ) -} - -export default { - title: "App/Dialogs/Connect Provider", - id: "app-dialog-connect-provider", -} - -export const V2 = { - render: () => ( - - - - ), -} - -export const ApiKey = { - render: renderConnection("openrouter", [{ type: "api", label: "API key" }]), -} - -export const OpenCodeZen = { - render: renderConnection("opencode", [{ type: "api", label: "API key" }]), -} - -export const LoginMethods = { - render: renderConnection("openai", [ - { type: "oauth", label: "ChatGPT Pro/Plus (browser)" }, - { type: "oauth", label: "ChatGPT Pro/Plus (headless)" }, - { type: "api", label: "API key" }, - ]), -} diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 0267b4b4ef..6499642b38 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -1,4 +1,4 @@ -import type { IntegrationMethod, IntegrationOauthConnectOutput } from "@opencode-ai/client/promise" +import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2/client" import { Button } from "@opencode-ai/ui/button" import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" @@ -9,9 +9,6 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { Spinner } from "@opencode-ai/ui/spinner" import { Tag } from "@opencode-ai/ui/tag" import { TextField } from "@opencode-ai/ui/text-field" -import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" -import { DialogBody, DialogHeader, DialogTitle, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2" -import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" import { showToast } from "@/utils/toast" import { type Accessor, @@ -19,8 +16,6 @@ import { createEffect, createMemo, createResource, - createUniqueId, - For, Match, onCleanup, onMount, @@ -28,18 +23,14 @@ import { Switch, } from "solid-js" import { createStore, produce } from "solid-js/store" -import { useParams } from "@solidjs/router" -import { ExternalLink } from "@/components/external-link" +import { Link } from "@/components/link" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" -import { useSettings } from "@/context/settings" import { popularProviders, useProviders } from "@/hooks/use-providers" import { CustomProviderForm } from "./dialog-custom-provider" -import { decode64 } from "@/utils/base64" const CUSTOM_ID = "_custom" -type ConnectMethod = Extract export function useProviderConnectController(options: { onBack?: () => void } = {}) { const [store, setStore] = createStore({ selected: undefined as string | undefined }) @@ -59,22 +50,32 @@ export const DialogConnectProvider: Component<{ const fallback = useProviderConnectController() const controller = props.controller ?? fallback const language = useLanguage() - const settings = useSettings() - const newLayout = settings.general.newLayoutDesigns const reset = controller.back const back = { current: reset } - let focusHost: HTMLDivElement | undefined - const holdFocus = () => focusHost?.focus({ preventScroll: true }) const select = (provider?: string) => { back.current = reset controller.select(provider) } - function Content() { - return ( + return ( + + back.current()} + aria-label={language.t("common.goBack")} + /> + + } + > - + {(provider) => ( @@ -87,77 +88,15 @@ export const DialogConnectProvider: Component<{ )} - + - ) - } - - return ( - - back.current()} - aria-label={language.t("common.goBack")} - /> - - } - > - - - } - > - - - {language.t("command.provider.connect")}} - > - - - - -
- -
-
-
- + ) } -function ProviderPicker(props: { - directory?: Accessor - onSelect: (provider: string) => void - onPrepare?: () => void -}) { - const settings = useSettings() - if (settings.general.newLayoutDesigns()) - return - const providers = useProviders(() => props.directory?.()) +function ProviderPicker(props: { directory?: Accessor; onSelect: (provider: string) => void }) { + const providers = useProviders(props.directory) const language = useLanguage() const popularGroup = () => language.t("dialog.provider.group.popular") const otherGroup = () => language.t("dialog.provider.group.other") @@ -224,157 +163,6 @@ function ProviderPicker(props: { ) } -function ProviderPickerV2(props: { - directory?: Accessor - onSelect: (provider: string) => void - onPrepare?: () => void -}) { - const providers = useProviders(() => props.directory?.()) - const language = useLanguage() - const [store, setStore] = createStore({ - filter: "", - active: undefined as string | undefined, - connecting: undefined as string | undefined, - }) - const featured = ["opencode", "opencode-go", "anthropic", "openai", "google", "openrouter", "vercel"] - const custom = () => ({ id: CUSTOM_ID, name: language.t("dialog.provider.custom.label") }) - const all = createMemo(() => { - language.locale() - const query = store.filter.trim().toLowerCase() - const values = [custom(), ...providers.all().values()] - if (!query) return values - return values.filter((provider) => `${provider.id} ${provider.name}`.toLowerCase().includes(query)) - }) - const popular = createMemo(() => - all() - .filter((provider) => featured.includes(provider.id)) - .sort((a, b) => featured.indexOf(a.id) - featured.indexOf(b.id)), - ) - const other = createMemo(() => - all() - .filter((provider) => !featured.includes(provider.id)) - .sort((a, b) => { - if (a.id === CUSTOM_ID) return -1 - if (b.id === CUSTOM_ID) return 1 - return a.name.localeCompare(b.name) - }), - ) - const rows = createMemo(() => [...popular(), ...other()]) - let picker: HTMLDivElement | undefined - let search: HTMLInputElement | undefined - - onMount(() => search?.focus({ preventScroll: true })) - - const connect = (provider: string) => { - props.onPrepare?.() - props.onSelect(provider) - } - - const move = (event: KeyboardEvent, direction: number) => { - const items = rows() - if (items.length === 0) return - const index = items.findIndex((provider) => provider.id === store.active) - const next = index < 0 ? (direction > 0 ? 0 : items.length - 1) : (index + direction + items.length) % items.length - setStore("active", items[next].id) - picker - ?.querySelector(`[data-provider-id="${CSS.escape(items[next].id)}"]`) - ?.focus({ preventScroll: true }) - event.preventDefault() - } - - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === "ArrowDown") return move(event, 1) - if (event.key === "ArrowUp") return move(event, -1) - if (event.key !== "Enter" || !store.active) return - connect(store.active) - event.preventDefault() - } - - return ( -
-
- } - placeholder={language.t("dialog.provider.search.placeholder")} - value={store.filter} - onInput={(event) => { - setStore({ filter: event.currentTarget.value, active: undefined }) - }} - /> -
-
-
- - {(group) => ( - 0}> -
-
- {group.title} -
- - {(provider) => ( - - )} - -
-
- )} -
- -
- {language.t("dialog.provider.empty")} -
-
-
-
-
-
- ) -} - function ProviderConnection(props: { provider: string directory?: Accessor @@ -384,16 +172,8 @@ function ProviderConnection(props: { const dialog = useDialog() const serverSync = useServerSync() const serverSDK = useServerSDK() - const params = useParams() const language = useLanguage() - const settings = useSettings() - const newLayout = settings.general.newLayoutDesigns - const providers = useProviders(() => props.directory?.()) - const directory = () => props.directory?.() ?? decode64(params.dir) - const location = () => { - const value = directory() - return value ? { directory: value } : undefined - } + const providers = useProviders(props.directory) const alive = { value: true } const timer = { current: undefined as ReturnType | undefined } @@ -408,32 +188,28 @@ function ProviderConnection(props: { const provider = createMemo( () => providers.all().get(props.provider) ?? serverSync().data.provider.all.get(props.provider)!, ) - const fallback = createMemo(() => [ + const fallback = createMemo(() => [ { - type: "key" as const, + type: "api" as const, label: language.t("provider.connect.method.apiKey"), }, ]) - const [integration] = createResource( - () => ({ provider: props.provider, directory: directory() }), - (input) => - serverSDK() - .api.integration.get({ - integrationID: input.provider, - location: input.directory ? { directory: input.directory } : undefined, - }) - .then((result) => result.data), + const [auth] = createResource( + () => props.provider, + async () => { + const cached = serverSync().data.provider_auth[props.provider] + if (cached) return cached + const res = await serverSDK().client.provider.auth() + if (!alive.value) return fallback() + serverSync().set("provider_auth", res.data ?? {}) + return res.data?.[props.provider] ?? fallback() + }, ) - const loading = createMemo(() => integration.loading) - const methods = createMemo(() => { - const values = integration.latest?.methods.filter( - (method): method is ConnectMethod => method.type === "key" || method.type === "oauth", - ) - return values?.length ? values : fallback() - }) + const loading = createMemo(() => auth.loading && !serverSync().data.provider_auth[props.provider]) + const methods = createMemo(() => auth.latest ?? serverSync().data.provider_auth[props.provider] ?? fallback()) const [store, setStore] = createStore({ methodIndex: undefined as undefined | number, - authorization: undefined as undefined | IntegrationOauthConnectOutput["data"], + authorization: undefined as undefined | ProviderAuthAuthorization, promptInputs: undefined as undefined | Record, state: "pending" as undefined | "pending" | "complete" | "error" | "prompt", error: undefined as string | undefined, @@ -445,7 +221,7 @@ function ProviderConnection(props: { | { type: "auth.prompt" } | { type: "auth.inputs"; inputs: Record } | { type: "auth.pending" } - | { type: "auth.complete"; authorization: IntegrationOauthConnectOutput["data"] } + | { type: "auth.complete"; authorization: ProviderAuthAuthorization } | { type: "auth.error"; error: string } function dispatch(action: Action) { @@ -499,20 +275,10 @@ function ProviderConnection(props: { const methodLabel = (value?: { type?: string; label?: string }) => { if (!value) return "" - if (value.type === "key") return language.t("provider.connect.method.apiKey") + if (value.type === "api") return language.t("provider.connect.method.apiKey") return value.label ?? "" } - const methodDetails = (value?: { type?: string; label?: string }) => { - const label = methodLabel(value) - const suffix = value?.label?.match(/\s+\((browser|headless)\)$/i) - const hint = suffix?.[1] - return { - label: suffix ? label.slice(0, -suffix[0].length) : label, - hint: hint ? hint[0].toUpperCase() + hint.slice(1) : value?.type === "key" ? "Browser" : undefined, - } - } - function formatError(value: unknown, fallback: string): string { if (value && typeof value === "object" && "data" in value) { const data = (value as { data?: { message?: unknown } }).data @@ -540,22 +306,46 @@ function ProviderConnection(props: { const method = methods()[index] dispatch({ type: "method.select", index }) + if (method.type === "api" && method.prompts?.length) { + if (!inputs) { + dispatch({ type: "auth.prompt" }) + return + } + dispatch({ type: "auth.inputs", inputs }) + return + } + if (method.type === "oauth") { if (method.prompts?.length && !inputs) { dispatch({ type: "auth.prompt" }) return } dispatch({ type: "auth.pending" }) + const start = Date.now() await serverSDK() - .api.integration.oauth.connect({ - integrationID: props.provider, - methodID: method.id, - inputs: inputs ?? {}, - location: location(), - }) + .client.provider.oauth.authorize( + { + providerID: props.provider, + method: index, + inputs, + }, + { throwOnError: true }, + ) .then((x) => { if (!alive.value) return - dispatch({ type: "auth.complete", authorization: x.data }) + const elapsed = Date.now() - start + const delay = 1000 - elapsed + + if (delay > 0) { + if (timer.current !== undefined) clearTimeout(timer.current) + timer.current = setTimeout(() => { + timer.current = undefined + if (!alive.value) return + dispatch({ type: "auth.complete", authorization: x.data! }) + }, delay) + return + } + dispatch({ type: "auth.complete", authorization: x.data! }) }) .catch((e) => { if (!alive.value) return @@ -570,9 +360,9 @@ function ProviderConnection(props: { index: 0, }) - const prompts = createMemo(() => { + const prompts = createMemo>(() => { const value = method() - return value?.type === "oauth" ? (value.prompts ?? []) : [] + return value?.prompts ?? [] }) const matches = (prompt: NonNullable[number]>, value: Record) => { if (!prompt.when) return true @@ -603,6 +393,10 @@ function ProviderConnection(props: { setFormStore("index", next) return } + if (method()?.type === "api") { + dispatch({ type: "auth.inputs", inputs: value }) + return + } await selectMethod(store.methodIndex, value) } @@ -704,9 +498,7 @@ function ProviderConnection(props: { }) async function complete() { - await serverSync() - .refreshProviders() - .catch(() => undefined) + await serverSDK().client.global.dispose() dialog.close() showToast({ variant: "success", @@ -727,37 +519,6 @@ function ProviderConnection(props: { props.setBack(goBack) function MethodSelection() { - if (newLayout()) - return ( -
-
- {language.t("provider.connect.selectMethod", { provider: provider().name })} -
-
- - {(item, index) => { - const details = () => methodDetails(item) - return ( - - ) - }} - -
-
- ) - return ( <>
@@ -770,7 +531,7 @@ function ProviderConnection(props: { listRef = ref }} items={methods} - key={(m) => m?.label ?? m?.type} + key={(m) => m?.label} onSelect={async (selected, index) => { if (!selected) return void selectMethod(index) @@ -791,18 +552,11 @@ function ProviderConnection(props: { } function ApiAuthView() { - let apiKey: HTMLInputElement | undefined - const errorID = createUniqueId() const [formStore, setFormStore] = createStore({ value: "", error: undefined as string | undefined, }) - onMount(() => { - if (!newLayout()) return - apiKey?.focus({ preventScroll: true }) - }) - async function handleSubmit(e: SubmitEvent) { e.preventDefault() @@ -816,67 +570,17 @@ function ProviderConnection(props: { } setFormStore("error", undefined) - await serverSDK().api.integration.connect.key({ - integrationID: props.provider, - location: location(), - key: apiKey, + await serverSDK().client.auth.set({ + providerID: props.provider, + auth: { + type: "api", + key: apiKey, + ...(store.promptInputs ? { metadata: store.promptInputs } : {}), + }, }) await complete() } - if (newLayout()) - return ( -
- -
-
{language.t("provider.connect.opencodeZen.line1")}
-
{language.t("provider.connect.opencodeZen.line2")}
-
- {language.t("provider.connect.opencodeZen.visit.prefix")} - - {language.t("provider.connect.opencodeZen.visit.link")} - - {language.t("provider.connect.opencodeZen.visit.suffix")} -
-
-
-
- - - {(error) => ( - - )} - - - {language.t("common.continue")} - -
-
- ) - return (
@@ -886,9 +590,9 @@ function ProviderConnection(props: {
{language.t("provider.connect.opencodeZen.line2")}
{language.t("provider.connect.opencodeZen.visit.prefix")} - + {language.t("provider.connect.opencodeZen.visit.link")} - + {language.t("provider.connect.opencodeZen.visit.suffix")}
@@ -901,8 +605,7 @@ function ProviderConnection(props: {
{ - if (!newLayout()) return - codeInput?.focus({ preventScroll: true }) - }) - async function handleSubmit(e: SubmitEvent) { e.preventDefault() @@ -947,13 +643,12 @@ function ProviderConnection(props: { setFormStore("error", undefined) const result = await serverSDK() - .api.integration.oauth.complete({ - integrationID: props.provider, - attemptID: store.authorization!.attemptID, - location: location(), + .client.provider.oauth.callback({ + providerID: props.provider, + method: store.methodIndex, code, }) - .then(() => ({ ok: true as const })) + .then((value) => (value.error ? { ok: false as const, error: value.error } : { ok: true as const })) .catch((error) => ({ ok: false as const, error })) if (result.ok) { await complete() @@ -962,59 +657,16 @@ function ProviderConnection(props: { setFormStore("error", formatError(result.error, language.t("provider.connect.oauth.code.invalid"))) } - if (newLayout()) - return ( -
-
- {language.t("provider.connect.oauth.code.visit.prefix")} - - {language.t("provider.connect.oauth.code.visit.link")} - - {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })} -
- - - - {(error) => ( - - )} - - - {language.t("common.continue")} - - -
- ) - return (
{language.t("provider.connect.oauth.code.visit.prefix")} - - {language.t("provider.connect.oauth.code.visit.link")} - + {language.t("provider.connect.oauth.code.visit.link")} {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
{ - const poll = async () => { - const authorization = store.authorization - if (!authorization || !alive.value) return + void (async () => { const result = await serverSDK() - .api.integration.oauth.status({ - integrationID: props.provider, - attemptID: authorization.attemptID, - location: location(), + .client.provider.oauth.callback({ + providerID: props.provider, + method: store.methodIndex, }) - .then((value) => ({ ok: true as const, status: value.data })) + .then((value) => (value.error ? { ok: false as const, error: value.error } : { ok: true as const })) .catch((error) => ({ ok: false as const, error })) + if (!alive.value) return + if (!result.ok) { - dispatch({ type: "auth.error", error: formatError(result.error, language.t("common.requestFailed")) }) + const message = formatError(result.error, language.t("common.requestFailed")) + dispatch({ type: "auth.error", error: message }) return } - if (result.status.status === "complete") { - await complete() - return - } - if (result.status.status === "failed") { - dispatch({ type: "auth.error", error: result.status.message }) - return - } - if (result.status.status === "expired") { - dispatch({ type: "auth.error", error: language.t("common.requestFailed") }) - return - } - timer.current = setTimeout(poll, 1_000) - } - void poll() + + await complete() + })() }) return (
{language.t("provider.connect.oauth.auto.visit.prefix")} - - {language.t("provider.connect.oauth.auto.visit.link")} - + {language.t("provider.connect.oauth.auto.visit.link")} {language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })}
-
- -
+
+
+ +
{language.t("provider.connect.title.anthropicProMax")} @@ -1121,12 +750,8 @@ function ProviderConnection(props: {
-
-
+
+
@@ -1158,15 +783,15 @@ function ProviderConnection(props: {
- + - + - + diff --git a/packages/app/src/components/dialog-custom-provider.tsx b/packages/app/src/components/dialog-custom-provider.tsx index e34e4c39b8..fb5aa04260 100644 --- a/packages/app/src/components/dialog-custom-provider.tsx +++ b/packages/app/src/components/dialog-custom-provider.tsx @@ -8,7 +8,7 @@ import { TextField } from "@opencode-ai/ui/text-field" import { showToast } from "@/utils/toast" import { batch, For } from "solid-js" import { createStore, produce } from "solid-js/store" -import { ExternalLink } from "@/components/external-link" +import { Link } from "@/components/link" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" @@ -40,7 +40,7 @@ export function DialogCustomProvider(props: Props) { ) } -export function CustomProviderForm(props: { autofocus?: boolean } = {}) { +export function CustomProviderForm() { const dialog = useDialog() const serverSync = useServerSync() const serverSDK = useServerSDK() @@ -131,7 +131,6 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) { const saveMutation = useMutation(() => ({ mutationFn: async (result: NonNullable>) => { - if ((await serverSDK().protocol) !== "v1") throw new Error("Custom providers are unavailable on this server") const disabledProviders = serverSync().data.config.disabled_providers ?? [] const nextDisabled = disabledProviders.filter((id) => id !== result.providerID) @@ -178,22 +177,22 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) { return (
- +
{language.t("provider.custom.title")}

{language.t("provider.custom.description.prefix")} - + {language.t("provider.custom.description.link")} - + {language.t("provider.custom.description.suffix")}

{ const dir = base64Encode(sdk().directory) sdk() - .api.session.fork({ sessionID, messageID: item.id }) + .client.session.fork({ sessionID, messageID: item.id }) .then((forked) => { + if (!forked.data) { + showToast({ title: language.t("common.requestFailed") }) + return + } dialog.close() - prompt.set(restored, undefined, { dir, id: forked.id }) - navigate(`/${dir}/session/${forked.id}`) + prompt.set(restored, undefined, { dir, id: forked.data.id }) + navigate(`/${dir}/session/${forked.data.id}`) }) .catch((err: unknown) => { const message = err instanceof Error ? err.message : String(err) diff --git a/packages/app/src/components/dialog-select-directory-v2.tsx b/packages/app/src/components/dialog-select-directory-v2.tsx index f3376ad35f..69d46ddcb6 100644 --- a/packages/app/src/components/dialog-select-directory-v2.tsx +++ b/packages/app/src/components/dialog-select-directory-v2.tsx @@ -8,7 +8,6 @@ import { createEffect, createMemo, createResource, createSignal, For, onCleanup, import { useGlobal } from "@/context/global" import { useLanguage } from "@/context/language" import { ServerConnection } from "@/context/server" -import type { Path } from "@opencode-ai/sdk/v2/client" import { absoluteTreePath, activeTreeNavigation, @@ -29,7 +28,6 @@ import { } from "./directory-picker-domain" import "./dialog-select-directory-v2.css" import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2" -import { getFilename } from "@opencode-ai/core/util/path" interface DialogSelectDirectoryV2Props { title?: string @@ -69,13 +67,11 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) { const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory)) const [fallbackPath] = createResource( () => (missingBase() ? true : undefined), - async (): Promise => { - if ((await sdk.protocol) !== "v1") return - return sdk.client.path + () => + sdk.client.path .get() .then((result) => result.data) - .catch(() => undefined) - }, + .catch(() => undefined), { initialValue: undefined }, ) const home = createMemo(() => sync.data.path.home || fallbackPath()?.home || "") @@ -89,26 +85,18 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) { ) const search = createDirectorySearch({ sdk, home, base: () => root() || start() }) const [suggestions] = createResource(input, async (value) => { - const cleaned = cleanPickerInput(value) - const typed = cleaned.replace(/\/+$/, "") + const typed = cleanPickerInput(value).replace(/\/+$/, "") const current = displayPickerPath(root(), value, home()).replace(/\/+$/, "") - if (!cleaned || (root() && typed === current)) return { query: value, items: [] } + if (!typed || typed === current) return { query: value, items: [] } const directories = (await search(value)).map((absolute) => ({ absolute, type: "directory" as const })) if (!policy.includeFiles) return { query: value, items: directories.slice(0, 5) } - const base = pickerRoot(cleaned) || root() || start() - if (!base) return { query: value, items: directories.slice(0, 5) } - const files = await sdk.api.file - .find({ - location: { directory: base }, - query: pickerFileSearchQuery(base, value, home()), - type: "file", - limit: 20, - }) - .then((result) => result.data) + const files = await sdk.client.find + .files({ directory: root(), query: pickerFileSearchQuery(root(), value, home()), type: "file", limit: 20 }) + .then((result) => result.data ?? []) .catch(() => []) const results = [ ...directories, - ...files.map((entry) => ({ absolute: absoluteTreePath(base, entry.path), type: "file" as const })), + ...files.map((path) => ({ absolute: absoluteTreePath(root(), path), type: "file" as const })), ] return { query: value, @@ -127,14 +115,9 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) { existing ?? loads.schedule(`${generation}:${key}`, eager ? "background" : "user", () => { if (!activeTreeNavigation(generation, navigation)) return Promise.resolve(undefined) - return sdk.api.file - .list({ location: { directory: absolute } }) - .then((result) => - result.data.map((entry) => ({ - name: getFilename(entry.path.replace(/[\\/]+$/, "")), - type: entry.type, - })), - ) + return sdk.client.file + .list({ directory: absolute, path: "" }) + .then((result) => result.data ?? []) .catch(() => undefined) }) listings.set(key, request) @@ -329,7 +312,6 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) { {(suggestion, index) => ( - ) -} - -export default { - title: "App/Dialogs/Select Model", - id: "app-dialog-select-model", -} - -export const WithoutProviders = { - render: () => , -} diff --git a/packages/app/src/components/dialog-select-model-unpaid-v2.tsx b/packages/app/src/components/dialog-select-model-unpaid-v2.tsx index 2d0c3a7305..0ce9f18c16 100644 --- a/packages/app/src/components/dialog-select-model-unpaid-v2.tsx +++ b/packages/app/src/components/dialog-select-model-unpaid-v2.tsx @@ -1,26 +1,23 @@ import { DialogBody, DialogHeader, DialogTitle, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2" import { Icon } from "@opencode-ai/ui/v2/icon" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import { ScrollView } from "@opencode-ai/ui/scroll-view" import { Tag } from "@opencode-ai/ui/v2/badge-v2" import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" import { useDialog } from "@opencode-ai/ui/context/dialog" -import { useTheme } from "@opencode-ai/ui/theme" import { createMemo, onCleanup, onMount, type Component, For, Show } from "solid-js" import { useLocal } from "@/context/local" -import { useProviders } from "@/hooks/use-providers" +import { popularProviders, useProviders } from "@/hooks/use-providers" import { decode64 } from "@/utils/base64" import { useLanguage } from "@/context/language" import { ModelTooltip } from "./model-tooltip" type ModelState = ReturnType["model"] -const featuredProviders = ["opencode", "opencode-go", "openai", "anthropic", "google", "github-copilot"] -const displayModelName = (name: string) => name.replace(/\s+(?:\(free\)|free)$/i, "") export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (props) => { const local = useLocal() const model = props.model ?? local.model const dialog = useDialog() - const theme = useTheme() const directory = () => decode64(local.slug()) const providers = useProviders(directory) const language = useLanguage() @@ -31,7 +28,6 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro }) const isFree = (item: ReturnType[number]) => item.provider.id === "opencode" && (!item.cost || item.cost.input === 0) - const freeModels = createMemo(() => model.list().filter(isFree)) const openProviders = (provider?: string) => { void import("./dialog-connect-provider").then((x) => { @@ -66,110 +62,111 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro }) return ( - + {language.t("dialog.model.select.title")} - -
-
-
-
- {language.t("dialog.model.unpaid.freeModels.title")} -
-
- - {(item) => ( - - } - > - - - )} - -
- -
-
-
-
- {language.t("dialog.model.unpaid.addMore.title")} +
+ + +
+
+
+
+ {language.t("dialog.model.unpaid.freeModels.title")}
-
- featuredProviders.includes(provider.id)) - .sort((a, b) => featuredProviders.indexOf(a.id) - featuredProviders.indexOf(b.id))} - > - {(provider) => ( + + {(item) => ( + } + > + + )} + +
+ +
+
+
+
+ {language.t("dialog.model.unpaid.addMore.title")} +
+
+
+ { + if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) { + return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) + } + return a.name.localeCompare(b.name) + })} + > + {(provider) => ( + - )} - - + + )} + + +
-
+
) diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index 9066f72434..4fb7891ec9 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -1,5 +1,15 @@ import { Popover as Kobalte } from "@kobalte/core/popover" -import { Component, ComponentProps, createEffect, createMemo, For, JSX, Show } from "solid-js" +import { + Component, + ComponentProps, + createEffect, + createMemo, + For, + JSX, + onCleanup, + Show, + ValidComponent, +} from "solid-js" import { createStore } from "solid-js/store" import { useLocal } from "@/context/local" import { useDialog } from "@opencode-ai/ui/context/dialog" @@ -19,7 +29,6 @@ import { ModelTooltip } from "./model-tooltip" import { useLanguage } from "@/context/language" import { decode64 } from "@/utils/base64" import { handleDocumentSearchKeydown } from "@/utils/search-keydown" -import { createMenuDismissController } from "@/utils/menu-dismiss-controller" import { createEventListener } from "@solid-primitives/event-listener" import { matchesModelSearch } from "./dialog-select-model-search" @@ -113,13 +122,14 @@ const ModelList: Component<{ } type ModelSelectorTriggerProps = Omit, "as" | "ref"> -type ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => JSX.Element type Dismiss = "escape" | "outside" | "select" | "manage" | "provider" export function ModelSelectorPopover(props: { provider?: string model?: ModelState - trigger: ModelSelectorTrigger + children?: JSX.Element + triggerAs?: ValidComponent + triggerProps?: ModelSelectorTriggerProps onClose?: (cause: "escape" | "select") => void }) { const [store, setStore] = createStore<{ @@ -164,7 +174,9 @@ export function ModelSelectorPopover(props: { placement="top-start" gutter={4} > - + + {props.children} + void }) { + const model = props.model ?? useLocal().model + const language = useLanguage() const dialog = useDialog() - const controller = createModelSelectorController({ - model: props.model, - provider: () => props.provider, - onSelect: () => props.onClose?.(), - }) + const [store, setStore] = createStore({ open: false, search: "", active: "" }) + let searchRef: HTMLInputElement | undefined + let contentRef: HTMLDivElement | undefined + let restoreTrigger = true - return ( - { - void import("./dialog-manage-models").then((module) => { - void dialog.show(() => ) - }) - }} - onClose={() => props.onClose?.()} - /> - ) -} - -function createModelSelectorController(input: { - provider: () => string | undefined - model?: ModelState - onSelect: () => void -}) { - const model = input.model ?? useLocal().model const allModels = createMemo(() => model .list() .filter((item) => model.visible({ modelID: item.id, providerID: item.provider.id })) - .filter((item) => (input.provider() ? item.provider.id === input.provider() : true)), + .filter((item) => (props.provider ? item.provider.id === props.provider : true)), ) + const models = createMemo(() => { + const search = store.search.trim() + const filtered = search + ? allModels().filter((item) => matchesModelSearch(search, [item.name, item.id, item.provider.name])) + : allModels() - return { - models: (search: string) => { - const query = search.trim() - const filtered = query - ? allModels().filter((item) => matchesModelSearch(query, [item.name, item.id, item.provider.name])) - : allModels() - return [...filtered].sort((a, b) => a.name.localeCompare(b.name)) - }, - groups: (models: ModelItem[]) => { - const byProvider = new Map() - for (const item of models) { - byProvider.set(item.provider.id, [...(byProvider.get(item.provider.id) ?? []), item]) - } - return Array.from(byProvider, ([category, items]) => ({ category, items })).sort(sortModelGroups) - }, - current: () => { - const value = model.current() - return value ? modelKey(value) : undefined - }, - select: (item: ModelItem) => { - model.set({ modelID: item.id, providerID: item.provider.id }, { recent: true }) - input.onSelect() - }, - } -} - -function ModelSelectorPopoverV2View(props: { - trigger: ModelSelectorTrigger - models: (search: string) => ModelItem[] - groups: (models: ModelItem[]) => { category: string; items: ModelItem[] }[] - current: () => string | undefined - select: (item: ModelItem) => void - onManage: () => void - onClose: () => void -}) { - const language = useLanguage() - const [store, setStore] = createStore({ open: false, search: "", active: "" }) - let searchRef: HTMLInputElement | undefined - let contentRef: HTMLDivElement | undefined - const dismiss = createMenuDismissController(() => contentRef) - - const models = createMemo(() => props.models(store.search)) - const groups = createMemo(() => props.groups(models())) + return [...filtered].sort((a, b) => a.name.localeCompare(b.name)) + }) + const groups = createMemo(() => { + const byProvider = new Map() + for (const item of models()) { + byProvider.set(item.provider.id, [...(byProvider.get(item.provider.id) ?? []), item]) + } + return Array.from(byProvider, ([category, items]) => ({ category, items })).sort(sortModelGroups) + }) const keys = () => [...models().map(modelKey), manageKey] + const current = () => { + const value = model.current() + return value ? `${value.provider.id}:${value.id}` : undefined + } const initialActive = () => { - const selected = props.current() + const selected = current() const options = keys() if (selected && options.includes(selected)) return selected return options[0] ?? "" } const activeItem = () => store.active ? contentRef?.querySelector(`[data-option-key="${CSS.escape(store.active)}"]`) : undefined + const afterClose = (callback: () => void) => { + const complete = () => { + if (contentRef?.isConnected) { + requestAnimationFrame(complete) + return + } + requestAnimationFrame(() => requestAnimationFrame(callback)) + } + requestAnimationFrame(complete) + } const setOpen = (open: boolean) => { if (open) { - dismiss.allowTriggerRestore() + restoreTrigger = true setStore({ open: true, active: initialActive() }) setTimeout(() => requestAnimationFrame(() => { @@ -331,15 +308,23 @@ function ModelSelectorPopoverV2View(props: { } setStore({ open: false, search: "", active: "" }) } + const select = (item: ModelItem) => { + model.set({ modelID: item.id, providerID: item.provider.id }, { recent: true }) + props.onClose?.() + } const selectModel = (item: ModelItem) => { - dismiss.preventTriggerRestore() + restoreTrigger = false setOpen(false) - dismiss.afterClose(() => props.select(item)) + afterClose(() => select(item)) } const manage = () => { - dismiss.preventTriggerRestore() + restoreTrigger = false setOpen(false) - dismiss.afterClose(props.onManage) + afterClose(() => { + void import("./dialog-manage-models").then((x) => { + dialog.show(() => ) + }) + }) } const selectActive = () => { const item = models().find((item) => modelKey(item) === store.active) @@ -358,7 +343,10 @@ function ModelSelectorPopoverV2View(props: { queueMicrotask(() => activeItem()?.scrollIntoView({ block: "nearest" })) } const setSearch = (value: string) => { - const first = props.models(value)[0] + const search = value.trim() + const first = [...allModels()] + .sort((a, b) => a.name.localeCompare(b.name)) + .find((item) => matchesModelSearch(search, [item.name, item.id, item.provider.name])) setStore({ search: value, active: first ? modelKey(first) : manageKey }) } @@ -374,14 +362,18 @@ function ModelSelectorPopoverV2View(props: { return ( - + + {props.children} + (contentRef = element)} + ref={(el: HTMLDivElement) => (contentRef = el)} class="w-[284px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 !p-0 shadow-[var(--v2-elevation-floating)] focus:outline-none" - onPointerDownOutside={dismiss.preventTriggerRestore} - onFocusOutside={dismiss.preventTriggerRestore} - onCloseAutoFocus={dismiss.onCloseAutoFocus} + onPointerDownOutside={() => (restoreTrigger = false)} + onFocusOutside={() => (restoreTrigger = false)} + onCloseAutoFocus={(event) => { + if (!restoreTrigger) event.preventDefault() + }} >
@@ -401,9 +393,9 @@ function ModelSelectorPopoverV2View(props: { event.stopPropagation() if (event.key === "Escape") { event.preventDefault() - dismiss.preventTriggerRestore() + restoreTrigger = false setOpen(false) - dismiss.afterClose(props.onClose) + afterClose(() => props.onClose?.()) return } if (event.altKey || event.metaKey) return @@ -453,7 +445,7 @@ function ModelSelectorPopoverV2View(props: { {group.items[0].provider.name} - + {(item) => ( { diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index aa16976228..0876906890 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -16,7 +16,6 @@ import { useGlobal } from "@/context/global" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server" -import { detectServerProtocol } from "@/utils/server-protocol" import { type ServerHealth, useCheckServerHealth } from "@/utils/server-health" import { useSettings } from "@/context/settings" import { useTabs } from "@/context/tabs" @@ -264,13 +263,6 @@ export function useServerManagementController(options: { onSelect?: () => void; setStore("addServer", { error: language.t("dialog.server.add.error") }) return } - if ( - !settings.general.newLayoutDesigns() && - (await detectServerProtocol(conn.http, platform.fetch ?? globalThis.fetch)) === "v2" - ) { - setStore("addServer", { error: language.t("dialog.server.add.error") }) - return - } resetAdd() if (options.navigateOnAdd === false) { @@ -315,13 +307,6 @@ export function useServerManagementController(options: { onSelect?: () => void; setStore("editServer", { error: language.t("dialog.server.add.error") }) return } - if ( - !settings.general.newLayoutDesigns() && - (await detectServerProtocol(conn.http, platform.fetch ?? globalThis.fetch)) === "v2" - ) { - setStore("editServer", { error: language.t("dialog.server.add.error") }) - return - } if (normalized === input.original.http.url) { server.add(conn) } else { @@ -359,10 +344,7 @@ export function useServerManagementController(options: { onSelect?: () => void; ) const sortedItems = createMemo(() => { - const raw = items() - const list = settings.general.newLayoutDesigns() - ? raw - : raw.filter((x) => global.ensureServerCtx(x).sdk.protocolKind() !== "v2") + const list = items() if (!list.length) return list const active = current() const order = new Map(list.map((url, index) => [url, index] as const)) diff --git a/packages/app/src/components/dialog-usage-exceeded.tsx b/packages/app/src/components/dialog-usage-exceeded.tsx index bf5da751e2..e428d4c2bb 100644 --- a/packages/app/src/components/dialog-usage-exceeded.tsx +++ b/packages/app/src/components/dialog-usage-exceeded.tsx @@ -17,7 +17,7 @@ export function DialogUsageExceeded(props: DialogGoUpsellProps) { const platform = usePlatform() const runAction = () => { - if (props.link) platform.openExternal(props.link) + if (props.link) platform.openLink(props.link) props.onClose?.() dialog.close() } diff --git a/packages/app/src/components/directory-picker-domain.test.ts b/packages/app/src/components/directory-picker-domain.test.ts index 1bc9af0833..5746410610 100644 --- a/packages/app/src/components/directory-picker-domain.test.ts +++ b/packages/app/src/components/directory-picker-domain.test.ts @@ -133,10 +133,10 @@ test("scopes file autocomplete to the current browser root", () => { test("resolves directory autocomplete from the current browser root", async () => { const directories: string[] = [] const sdk = { - api: { - file: { - find: (input: { location?: { directory?: string } }) => { - directories.push(input.location?.directory ?? "") + client: { + find: { + files: (input: { directory: string }) => { + directories.push(input.directory) return Promise.resolve({ data: [] }) }, }, @@ -152,29 +152,6 @@ test("resolves directory autocomplete from the current browser root", async () = expect(directories).toEqual(["/repo", "/repo/src"]) }) -test("searches from an absolute root without a default base", async () => { - const directories: string[] = [] - const sdk = { - api: { - file: { - list: (input: { location?: { directory?: string } }) => { - directories.push(input.location?.directory ?? "") - return Promise.resolve({ - data: [ - { path: "Users/", type: "directory" }, - { path: "tmp/", type: "directory" }, - ], - }) - }, - }, - }, - } as unknown as Parameters[0]["sdk"] - const search = createDirectorySearch({ sdk, home: () => "", base: () => undefined }) - - expect(await search("/")).toEqual(["/Users", "/tmp"]) - expect(directories).toEqual(["/"]) -}) - test("identifies the next directory level to preload", () => { expect( preloadTreeDirectories("src/", [ diff --git a/packages/app/src/components/directory-picker-domain.ts b/packages/app/src/components/directory-picker-domain.ts index 9539ae1d01..9900265962 100644 --- a/packages/app/src/components/directory-picker-domain.ts +++ b/packages/app/src/components/directory-picker-domain.ts @@ -326,15 +326,15 @@ export function createDirectorySearch(args: { sdk: ServerSDK; base: () => string let current = 0 const scoped = (value: string) => { - const raw = normalizePickerDrive(value) - const root = pickerRoot(raw) - if (root) return { directory: trimPickerPath(root), path: raw.slice(root.length) } const base = args.base() if (!base) return + const raw = normalizePickerDrive(value) if (!raw) return { directory: trimPickerPath(base), path: "" } const home = args.home() if (raw === "~") return { directory: trimPickerPath(home || base), path: "" } if (raw.startsWith("~/")) return { directory: trimPickerPath(home || base), path: raw.slice(2) } + const root = pickerRoot(raw) + if (root) return { directory: trimPickerPath(root), path: raw.slice(root.length) } return { directory: trimPickerPath(base), path: raw } } @@ -342,17 +342,14 @@ export function createDirectorySearch(args: { sdk: ServerSDK; base: () => string const key = trimPickerPath(directory) const existing = cache.get(key) if (existing) return existing - const request = args.sdk.api.file - .list({ location: { directory: key } }) - .then((result) => result.data) + const request = args.sdk.client.file + .list({ directory: key, path: "" }) + .then((result) => result.data ?? []) .catch(() => []) .then((nodes) => nodes .filter((node) => node.type === "directory") - .map((node) => { - const relative = trimPickerPath(normalizePickerDrive(node.path)) - return { name: getFilename(relative), absolute: joinPickerPath(key, relative) } - }), + .map((node) => ({ name: node.name, absolute: trimPickerPath(normalizePickerDrive(node.absolute)) })), ) cache.set(key, request) return request @@ -374,9 +371,9 @@ export function createDirectorySearch(args: { sdk: ServerSDK; base: () => string const pathInput = raw.startsWith("~") || !!pickerRoot(raw) || raw.includes("/") const query = normalizePickerDrive(input.path) if (!pathInput) { - const results = await args.sdk.api.file - .find({ location: { directory: input.directory }, query, type: "directory", limit: 50 }) - .then((result) => result.data.map((entry) => entry.path)) + const results = await args.sdk.client.find + .files({ directory: input.directory, query, type: "directory", limit: 50 }) + .then((result) => result.data ?? []) .catch(() => []) if (!active()) return [] return results.map((path) => joinPickerPath(input.directory, path)).slice(0, 50) diff --git a/packages/app/src/components/edit-project.ts b/packages/app/src/components/edit-project.ts index 0c5d576e93..3ec999da06 100644 --- a/packages/app/src/components/edit-project.ts +++ b/packages/app/src/components/edit-project.ts @@ -1,7 +1,6 @@ import { getFilename } from "@opencode-ai/core/util/path" import { useDialog } from "@opencode-ai/ui/context/dialog" import { useMutation } from "@tanstack/solid-query" -import { normalizeProjectInfo } from "@/context/global-sync/utils" import { createMemo } from "solid-js" import { createStore } from "solid-js/store" import { useGlobal } from "@/context/global" @@ -71,26 +70,13 @@ export function createEditProjectModel(props: { project: LocalProject; server: S const start = store.startup.trim() if (props.project.id && props.project.id !== "global") { - if ((await serverCtx().sdk.protocol) !== "v1") return - const project = await serverCtx() - .sdk.client.project.update({ - projectID: props.project.id, - directory: props.project.worktree, - name, - icon: { color: store.color || "", override: store.iconOverride || "" }, - commands: { start }, - }) - .then((result) => result.data) - if (!project) return - // const project = await serverCtx().sdk.api.project.update({ - // projectID: props.project.id, - // name, - // icon: { color: store.color || "", override: store.iconOverride || "" }, - // commands: { start }, - // }) - serverCtx().sync.set("project", (items) => - items.map((item) => (item.id === project.id ? normalizeProjectInfo(project) : item)), - ) + await serverCtx().sdk.client.project.update({ + projectID: props.project.id, + directory: props.project.worktree, + name, + icon: { color: store.color || "", override: store.iconOverride || "" }, + commands: { start }, + }) serverCtx().sync.project.icon(props.project.worktree, store.iconOverride || undefined) dialog.close() return diff --git a/packages/app/src/components/external-link.tsx b/packages/app/src/components/external-link.tsx deleted file mode 100644 index 133e752eab..0000000000 --- a/packages/app/src/components/external-link.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { ComponentProps, splitProps } from "solid-js" - -export interface ExternalLinkProps extends Omit, "href"> { - href: string -} - -export function ExternalLink(props: ExternalLinkProps) { - const [local, rest] = splitProps(props, ["href", "children", "class", "target", "rel"]) - - return ( - - {local.children} - - ) -} diff --git a/packages/app/src/components/help-button.tsx b/packages/app/src/components/help-button.tsx index 18dd727084..23a955a837 100644 --- a/packages/app/src/components/help-button.tsx +++ b/packages/app/src/components/help-button.tsx @@ -16,7 +16,6 @@ export function TabsInfoPopup() { const settings = useSettings() const platform = usePlatform() const [drawerOpen, setDrawerOpen] = createSignal(false) - const windows = () => platform.platform === "desktop" && platform.os === "windows" return ( @@ -71,40 +70,12 @@ export function TabsInfoPopup() {
- - - } - class="absolute top-[10px] left-[-36px]" - /> - -
+ +

July 14

- + , "href"> { + href: string +} + +export function Link(props: LinkProps) { + const platform = usePlatform() + const [local, rest] = splitProps(props, ["href", "children", "class"]) + + return ( + { + if (!local.href) return + event.preventDefault() + platform.openLink(local.href) + }} + {...rest} + > + {local.children} + + ) +} diff --git a/packages/app/src/components/prompt-input-v2.tsx b/packages/app/src/components/prompt-input-v2.tsx deleted file mode 100644 index 96b99aaf4a..0000000000 --- a/packages/app/src/components/prompt-input-v2.tsx +++ /dev/null @@ -1,586 +0,0 @@ -import { ImagePreview } from "@opencode-ai/ui/image-preview" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { ProviderIcon } from "@opencode-ai/ui/provider-icon" -import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" -import { Icon } from "@opencode-ai/ui/v2/icon" -import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" -import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" -import type { ReferenceInfo } from "@opencode-ai/sdk/v2/client" -import { createEffect, createMemo, on, Show } from "solid-js" -import { ModelSelectorPopoverV2 } from "@/components/dialog-select-model" -import { DialogSelectModelUnpaidV2 } from "@/components/dialog-select-model-unpaid-v2" -import type { PromptInputProps } from "@/components/prompt-input/contracts" -import { normalizePromptHistoryEntry, promptLength, type PromptHistoryComment } from "@/components/prompt-input/history" -import { createPersistedPromptInputHistory } from "@/components/prompt-input/history-store" -import { promptDesignPlaceholder, promptPlaceholder } from "@/components/prompt-input/placeholder" -import { createPromptSubmit } from "@/components/prompt-input/submit" -import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file" -import { useComments } from "@/context/comments" -import { useCommand } from "@/context/command" -import { useLanguage } from "@/context/language" -import { useLayout } from "@/context/layout" -import { usePermission } from "@/context/permission" -import { type ImageAttachmentPart, usePrompt } from "@/context/prompt" -import { usePlatform } from "@/context/platform" -import { useSDK } from "@/context/sdk" -import { useSync } from "@/context/sync" -import { createSessionTabs } from "@/pages/session/helpers" -import { showToast } from "@/utils/toast" -import { PromptInputV2, type PromptInputV2Suggestion } from "@opencode-ai/session-ui/v2/prompt-input" -import { - createPromptInputV2Controller, - createPromptInputV2State, - type PromptInputV2Interaction, -} from "@opencode-ai/session-ui/v2/prompt-input/interaction" - -export type PromptInputV2ComposerProps = { - class?: string - controller: PromptInputV2ComposerController - borderUnderlay?: boolean -} - -export type PromptInputV2ControllerProps = Omit -export type PromptInputV2ComposerController = PromptInputV2Interaction & { - readonly model: PromptInputProps["controls"]["model"] -} - -export function PromptInputV2Composer(props: PromptInputV2ComposerProps) { - const dialog = useDialog() - const command = useCommand() - const language = useLanguage() - - return ( -
- - dialog.show(() => ) - } - /> - } - /> -
- ) -} - -export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): PromptInputV2ComposerController { - const sdk = useSDK() - const sync = useSync() - const files = useFile() - const layout = useLayout() - const comments = useComments() - const dialog = useDialog() - const command = useCommand() - const permission = usePermission() - const language = useLanguage() - const platform = usePlatform() - const prompt = props.state ?? usePrompt() - let editor: HTMLDivElement | undefined - - const interaction = createPromptInputV2State() - const mode = () => interaction[0].mode - const history = props.history ?? createPersistedPromptInputHistory() - const tabs = () => props.controls.session.tabs - const activeFileTab = createSessionTabs({ - tabs, - pathFromTab: files.pathFromTab, - normalizeTab: (tab) => (tab.startsWith("file://") ? files.tab(tab) : tab), - }).activeFileTab - const recent = createMemo(() => { - const all = tabs().all() - const active = activeFileTab() - const order = active ? [active, ...all.filter((tab) => tab !== active)] : all - return order.reduce((result, tab) => { - const path = files.pathFromTab(tab) - if (!path || result.includes(path)) return result - return [...result, path] - }, []) - }) - const info = createMemo(() => (props.controls.session.id ? sync().session.get(props.controls.session.id) : undefined)) - const working = createMemo(() => sync().data.session_working(props.controls.session.id ?? "")) - const attachments = createMemo(() => - prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"), - ) - const commentCount = createMemo(() => { - if (mode() === "shell") return 0 - return prompt.context.items().filter((item) => !!item.comment?.trim()).length - }) - const blank = createMemo(() => { - const text = prompt - .current() - .map((part) => ("content" in part ? part.content : "")) - .join("") - return text.trim().length === 0 && attachments().length === 0 && commentCount() === 0 - }) - const stopping = createMemo(() => working() && blank()) - const placeholder = createMemo(() => - promptPlaceholder({ - mode: mode(), - commentCount: commentCount(), - example: mode() === "shell" ? "git status" : "", - suggest: false, - t: (key, params) => language.t(key as Parameters[0], params as never), - }), - ) - const designPlaceholder = () => promptDesignPlaceholder(mode(), placeholder()) - - const historyComments = () => { - const byID = new Map(comments.all().map((item) => [`${item.file}\n${item.id}`, item] as const)) - return prompt.context.items().flatMap((item) => { - const comment = item.comment?.trim() - if (!comment) return [] - const selection = item.commentID ? byID.get(`${item.path}\n${item.commentID}`)?.selection : undefined - const nextSelection = - selection ?? - (item.selection - ? ({ start: item.selection.startLine, end: item.selection.endLine } satisfies SelectedLineRange) - : undefined) - if (!nextSelection) return [] - return [ - { - id: item.commentID ?? item.key, - path: item.path, - selection: { ...nextSelection }, - comment, - time: item.commentID ? (byID.get(`${item.path}\n${item.commentID}`)?.time ?? Date.now()) : Date.now(), - origin: item.commentOrigin, - preview: item.preview, - } satisfies PromptHistoryComment, - ] - }) - } - const restoreHistoryComments = (items: PromptHistoryComment[]) => { - comments.replace( - items.map((item) => ({ - id: item.id, - file: item.path, - selection: { ...item.selection }, - comment: item.comment, - time: item.time, - })), - ) - prompt.context.replaceComments( - items.map((item) => ({ - type: "file", - path: item.path, - selection: selectionFromLines(item.selection), - comment: item.comment, - commentID: item.id, - commentOrigin: item.origin, - preview: item.preview, - })), - ) - } - - const accepting = createMemo(() => { - const id = props.controls.session.id - if (!id) return permission.isAutoAcceptingDirectory(sdk().directory) - return permission.isAutoAccepting(id, sdk().directory) - }) - const submission = createPromptSubmit({ - prompt, - info, - imageAttachments: attachments, - commentCount, - autoAccept: accepting, - mode, - working, - editor: () => editor, - queueScroll: () => requestAnimationFrame(() => editor?.scrollIntoView({ block: "nearest" })), - promptLength, - addToHistory: (value, mode) => controller.addHistory(value, mode), - resetHistoryNavigation: () => controller.resetHistory(), - setMode: (next) => controller.dispatch({ type: next === "shell" ? "mode.shell" : "mode.normal" }), - setPopover: (popover) => { - if (!popover) controller.dispatch({ type: "popover.close" }) - }, - newSessionWorktree: () => props.newSessionWorktree, - onNewSessionWorktreeReset: props.onNewSessionWorktreeReset, - shouldQueue: props.shouldQueue, - onQueue: props.onQueue, - onAbort: props.onAbort, - onSubmit: props.onSubmit, - model: props.controls.model.selection, - }) - - const referenceDescription = (reference: ReferenceInfo) => - reference.source.type === "git" ? reference.source.repository : reference.source.path - const references = createMemo(() => - sync() - .data.reference.filter((reference) => !reference.hidden) - .map((reference) => ({ - id: `reference:${reference.name}`, - kind: "reference" as const, - label: `@${reference.name}`, - path: reference.path, - description: reference.description ?? referenceDescription(reference), - mention: { - type: "file" as const, - path: reference.path, - content: `@${reference.name}`, - start: 0, - end: 0, - mime: "application/x-directory", - filename: reference.name, - }, - })), - ) - const resources = createMemo(() => - Object.values(sync().data.mcp_resource).map((resource) => ({ - id: `resource:${resource.server}:${resource.uri}`, - kind: "resource" as const, - label: `@${resource.name}`, - path: resource.uri, - description: resource.description, - mention: { - type: "file" as const, - path: resource.uri, - content: `@${resource.name}`, - start: 0, - end: 0, - mime: resource.mimeType ?? "text/plain", - filename: resource.name, - url: resource.uri, - source: { - type: "resource" as const, - text: { value: `@${resource.name}`, start: 0, end: resource.name.length + 1 }, - clientName: resource.server, - uri: resource.uri, - }, - }, - resource, - })), - ) - const context = createMemo(() => [ - ...references(), - ...props.controls.agents.available - .filter((agent) => !agent.hidden && agent.mode !== "primary") - .map((agent) => ({ - id: `agent:${agent.name}`, - kind: "agent" as const, - label: `@${agent.name}`, - mention: { type: "agent" as const, name: agent.name, content: `@${agent.name}`, start: 0, end: 0 }, - })), - ...resources(), - ...recent().map((path) => ({ - id: `file:${path}`, - kind: "file" as const, - label: path, - path, - recent: true, - mention: { type: "file" as const, path, content: `@${path}`, start: 0, end: 0 }, - })), - ]) - const slashCommands = createMemo(() => [ - ...sync().data.command.map((item) => ({ - id: `custom.${item.name}`, - trigger: item.name, - title: item.name, - description: item.description, - type: "custom" as const, - })), - ...command.options - .filter((item) => !item.disabled && !item.id.startsWith("suggested.") && item.slash) - .map((item) => ({ - id: item.id, - trigger: item.slash!, - title: item.title, - description: item.description, - type: "builtin" as const, - })), - ]) - const commands = createMemo(() => - slashCommands().map((item) => ({ - id: item.id, - kind: "command", - label: `/${item.trigger}`, - trigger: item.trigger, - title: item.title, - description: item.description, - keybind: command.keybindParts(item.id), - })), - ) - const variants = createMemo(() => ["default", ...props.controls.model.selection.variant.list()]) - const controller = createPromptInputV2Controller({ - store: () => prompt.capture().store, - state: interaction, - identity: () => prompt.capture(), - history: { - entries: (mode) => - history.entries(mode).map((value) => { - const entry = normalizePromptHistoryEntry(value) - return { prompt: entry.prompt, metadata: entry.comments } - }), - add: (value, mode) => history.add(value, mode, mode === "shell" ? [] : historyComments()), - capture: historyComments, - restore: (metadata) => restoreHistoryComments(metadata as PromptHistoryComment[]), - }, - commands, - context, - searchContextFiles: async (query) => - (await files.searchFilesAndDirectories(query)).map((path) => ({ - id: `file:${path}`, - kind: "file", - label: path, - path, - mention: { type: "file", path, content: `@${path}`, start: 0, end: 0 }, - })), - onContextRemove(item) { - if (item?.commentID) comments.remove(item.path, item.commentID) - }, - openAttachment: (attachment) => - dialog.show(() => ), - openContext(key) { - const item = controller.contextItem(key) - if (item) openComment(item, props, sync, layout, files, comments) - }, - onEditor(element) { - editor = element as HTMLDivElement - props.ref?.(editor) - }, - onSuggestionSelect(item) { - if (item.kind !== "command") return - const selected = slashCommands().find((entry) => entry.id === item.id) - if (!selected || selected.type === "custom") return - return () => command.trigger(selected.id, "slash") - }, - attachments: { - picker: platform.openAttachmentPickerDialog, - directory: () => sdk().directory, - isDialogActive: () => !!dialog.active, - warn: () => - showToast({ - title: language.t("prompt.toast.pasteUnsupported.title"), - description: language.t("prompt.toast.pasteUnsupported.description"), - }), - duplicate: () => showToast({ title: language.t("prompt.toast.attachmentDuplicate.title") }), - onError: (error) => - showToast({ - variant: "error", - title: language.t("common.requestFailed"), - description: error instanceof Error ? error.message : String(error), - }), - readClipboardImage: platform.readClipboardImage, - getPathForFile: platform.getPathForFile, - store: platform.draftStore?.putBlob, - }, - view: { - placeholder: designPlaceholder, - get agent() { - return props.controls.agents.visible && props.controls.agents.options.length > 0 - ? { - options: () => props.controls.agents.options.map((name) => ({ id: name, label: name })), - current: () => props.controls.agents.current, - onSelect: (value: string) => props.controls.agents.select(value), - keybind: () => command.keybindParts("agent.cycle"), - } - : undefined - }, - variant: { - options: () => variants().map((value) => ({ id: value, label: value })), - current: () => props.controls.model.selection.variant.current() ?? "default", - onSelect: (value) => props.controls.model.selection.variant.set(value === "default" ? undefined : value), - keybind: () => command.keybindParts("model.variant.cycle"), - }, - submit: { - stopping, - working, - onSubmit: () => void submission.handleSubmit(new Event("submit")), - onStop: () => void submission.abort(), - }, - }, - }) - Object.defineProperty(controller, "model", { get: () => props.controls.model }) - - command.register("prompt-input", () => [ - { - id: "file.attach", - title: language.t("prompt.action.attachFile"), - category: language.t("command.category.file"), - keybind: "mod+u", - disabled: controller.state.mode !== "normal", - onSelect: () => controller.attach(), - }, - { - id: "prompt.mode.shell", - title: language.t("command.prompt.mode.shell"), - category: language.t("command.category.session"), - keybind: "mod+shift+x", - disabled: controller.state.mode === "shell", - onSelect: () => controller.dispatch({ type: "mode.shell" }), - }, - { - id: "prompt.mode.normal", - title: language.t("command.prompt.mode.normal"), - category: language.t("command.category.session"), - keybind: "mod+shift+e", - disabled: controller.state.mode === "normal", - onSelect: () => controller.dispatch({ type: "mode.normal" }), - }, - ]) - - createEffect( - on( - () => props.edit?.id, - (id) => { - const edit = props.edit - if (!id || !edit) return - prompt.context.items().forEach((item) => prompt.context.remove(item.key)) - edit.context.forEach((item) => - prompt.context.add({ - type: item.type, - path: item.path, - selection: item.selection, - comment: item.comment, - commentID: item.commentID, - commentOrigin: item.commentOrigin, - preview: item.preview, - }), - ) - controller.dispatch({ type: "mode.normal" }) - controller.resetHistory() - prompt.set(edit.prompt, promptLength(edit.prompt)) - controller.restoreFocus() - props.onEditLoaded?.() - }, - { defer: true }, - ), - ) - - return controller as PromptInputV2ComposerController -} - -function PromptInputV2ModelControl(props: { - loading: boolean - paid: boolean - title: string - keybind: string[] - model: PromptInputV2ComposerController["model"]["selection"] - providerID?: string - modelName: string - onClose: () => void - onUnpaidClick: () => void -}) { - const shouldAnimate = createMemo((previous) => previous ?? props.loading) - const content = () => ( - <> - - {(providerID) => ( - - )} - - {props.modelName} - - - - - ) - return ( - - - {props.title} - - - } - > - - {content()} - - } - > - ( - - {content()} - - )} - onClose={props.onClose} - /> - - - - ) -} - -function openComment( - item: { path: string; commentID?: string; commentOrigin?: "review" | "file" }, - props: PromptInputV2ControllerProps, - sync: ReturnType, - layout: ReturnType, - files: ReturnType, - comments: ReturnType, -) { - if (!item.commentID) return - const focus = { file: item.path, id: item.commentID } - comments.setActive(focus) - const queueFocus = (attempts = 6) => { - requestAnimationFrame(() => { - comments.setFocus({ ...focus }) - if (attempts <= 0) return - requestAnimationFrame(() => { - const current = comments.focus() - if (current?.file === focus.file && current.id === focus.id) queueFocus(attempts - 1) - }) - }) - } - const diffs = props.controls.session.id ? sync().data.session_diff[props.controls.session.id] : undefined - const review = - item.commentOrigin === "review" || (item.commentOrigin !== "file" && diffs?.some((diff) => diff.file === item.path)) - if (!props.controls.session.reviewPanel.opened()) props.controls.session.reviewPanel.open() - if (review) { - layout.fileTree.setTab("changes") - props.controls.session.tabs.setActive("review") - queueFocus() - return - } - layout.fileTree.setTab("all") - const tab = files.tab(item.path) - void props.controls.session.tabs.open(tab) - props.controls.session.tabs.setActive(tab) - void Promise.resolve(files.load(item.path)).finally(() => queueFocus()) -} diff --git a/packages/app/src/components/prompt-input.stories.tsx b/packages/app/src/components/prompt-input.stories.tsx index 0b9c26ce41..272079e106 100644 --- a/packages/app/src/components/prompt-input.stories.tsx +++ b/packages/app/src/components/prompt-input.stories.tsx @@ -1,8 +1,6 @@ // @ts-nocheck import { createStore } from "solid-js/store" -import type { Todo } from "@opencode-ai/sdk/v2" import { createPromptState } from "@/context/prompt" -import { SessionComposerRegion, createSessionComposerRegionController } from "@/pages/session/composer" import { createPromptInputHistory, PromptInput } from "./prompt-input" function createPromptInputStoryRuntime() { @@ -30,16 +28,8 @@ function PromptInputExample() { activeTab: undefined as string | undefined, reviewOpen: false, }) - const storyModel = { - id: "claude-3-7-sonnet", - name: "Claude 3.7 Sonnet", - provider: { id: "anthropic", name: "Anthropic" }, - } const model = { - current: () => storyModel, - list: () => [storyModel], - visible: () => true, - set: () => {}, + current: () => ({ id: "claude-3-7-sonnet", name: "Claude 3.7 Sonnet", provider: { id: "anthropic" } }), variant: { list: () => ["fast", "thinking"], current: () => controls.variant, @@ -75,6 +65,7 @@ function PromptInputExample() { open: () => setControls("reviewOpen", true), }, }, + newLayoutDesigns: true, } const addReviewComment = () => { const comment = controls.comments + 1 @@ -111,93 +102,6 @@ function PromptInputExample() { ) } -const todos: Todo[] = [ - { id: "todo-1", content: "Inspect the session composer animation", status: "completed" }, - { id: "todo-2", content: "Keep the dock settled on initial render", status: "in_progress" }, - { id: "todo-3", content: "Verify session navigation behavior", status: "pending" }, -] - -function PromptInputWithOpenDock() { - const input = createPromptInputStoryRuntime() - const [controls, setControls] = createStore({ - agent: "build", - activeTab: undefined as string | undefined, - todoCollapsed: false, - }) - const inputControls = { - agents: { - available: [], - options: ["build"], - get current() { - return controls.agent - }, - loading: false, - visible: true, - select: (agent?: string) => setControls("agent", agent ?? "build"), - }, - model: { - selection: { - current: () => ({ id: "claude-3-7-sonnet", name: "Claude 3.7 Sonnet", provider: { id: "anthropic" } }), - variant: { list: () => [], current: () => undefined, set: () => {} }, - }, - paid: true, - loading: false, - }, - session: { - id: "story-session", - tabs: { - active: () => controls.activeTab, - all: () => [], - open: () => {}, - setActive: (tab: string) => setControls("activeTab", tab), - }, - reviewPanel: { opened: () => false, open: () => {} }, - }, - } - const state = { - blocked: () => false, - questionRequest: () => undefined, - permissionRequest: () => undefined, - permissionResponding: () => false, - decide: () => {}, - todos: () => todos, - dock: () => true, - closing: () => false, - opening: () => false, - } - return ( - "story-session", - sessionID: () => "story-session", - prompt: input.state, - ready: () => true, - centered: () => false, - todo: { - collapsed: () => controls.todoCollapsed, - onToggle: () => setControls("todoCollapsed", (collapsed) => !collapsed), - }, - followup: () => undefined, - revert: () => undefined, - onResponseSubmit: () => {}, - openParent: () => {}, - setPromptRef: () => {}, - setDockRef: () => {}, - })} - promptInput={ - {}} - newSessionWorktree="" - onNewSessionWorktreeReset={() => {}} - /> - } - /> - ) -} - export default { title: "App/PromptInput", id: "app-prompt-input", @@ -212,12 +116,3 @@ export const Basic = {
), } - -export const DockAlreadyOpen = { - render: () => ( -
-

Prompt Input with open Todo dock

- -
- ), -} diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 08c1ee3489..a820067a4e 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -13,6 +13,8 @@ import { Match, type JSX, } from "solid-js" +import { createStore, type SetStoreFunction, type Store } from "solid-js/store" +import type { useLocal } from "@/context/local" import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file" import { ContentPart, @@ -47,6 +49,7 @@ import { ModelSelectorPopover, ModelSelectorPopoverV2 } from "@/components/dialo import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid" import { DialogSelectModelUnpaidV2 } from "@/components/dialog-select-model-unpaid-v2" import { useCommand } from "@/context/command" +import { Persist, persisted } from "@/utils/persist" import { usePermission } from "@/context/permission" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" @@ -57,34 +60,121 @@ import { ACCEPTED_FILE_TYPES, pickAttachmentFiles } from "./prompt-input/files" import { canNavigateHistoryAtCursor, navigatePromptHistory, + prependHistoryEntry, type PromptHistoryComment, type PromptHistoryEntry, + type PromptHistoryStoredEntry, promptLength, } from "./prompt-input/history" -import { - createPersistedPromptInputHistory, - createPromptInputHistory, - type PromptInputHistory, -} from "./prompt-input/history-store" -import { - type PromptInputControls, - type PromptInputProps, - type PromptInputState, - type PromptInputSubmission, -} from "./prompt-input/contracts" -import { createPromptSubmit } from "./prompt-input/submit" +import { createPromptSubmit, type FollowupDraft } from "./prompt-input/submit" import { PromptPopover, type AtOption, type SlashCommand } from "./prompt-input/slash-popover" import { PromptContextItems } from "./prompt-input/context-items" import { PromptImageAttachments } from "./prompt-input/image-attachments" import { PromptDragOverlay } from "./prompt-input/drag-overlay" -import { promptDesignPlaceholder, promptPlaceholder } from "./prompt-input/placeholder" +import { promptPlaceholder } from "./prompt-input/placeholder" import { createPromptInputTransientState } from "./prompt-input/transient-state" import { showToast } from "@/utils/toast" import { ImagePreview } from "@opencode-ai/ui/image-preview" import type { ReferenceInfo } from "@opencode-ai/sdk/v2/client" -export { createPromptInputHistory } -export type { PromptInputControls, PromptInputHistory, PromptInputProps, PromptInputState, PromptInputSubmission } +export type PromptInputState = ReturnType + +export type PromptInputHistory = { + entries: (mode: "normal" | "shell") => PromptHistoryStoredEntry[] + add: (prompt: Prompt, mode: "normal" | "shell", comments: PromptHistoryComment[]) => void +} + +export type PromptInputSubmission = { + abort: () => Promise | void + handleSubmit: (event: Event) => Promise | void +} + +export type PromptInputControls = { + agents: { + available: { name: string; hidden?: boolean; mode: string }[] + options: string[] + current: string + loading: boolean + visible: boolean + select: (name: string | undefined) => void + } + model: { + selection: ReturnType["model"] + paid: boolean + loading: boolean + } + session: { + id?: string + tabs: { + active: () => string | undefined + all: () => string[] + open: (tab: string) => void | Promise + setActive: (tab: string) => void + } + reviewPanel: { + opened: () => boolean + open: () => void + } + } + newLayoutDesigns: boolean +} + +export function createPromptInputHistory(): PromptInputHistory { + const [normal, setNormal] = createStore({ entries: [] }) + const [shell, setShell] = createStore({ entries: [] }) + return createPromptInputHistoryStore(normal, setNormal, shell, setShell) +} + +type PromptHistoryState = { entries: PromptHistoryStoredEntry[] } + +function createPromptInputHistoryStore( + normal: Store, + setNormal: SetStoreFunction, + shell: Store, + setShell: SetStoreFunction, +): PromptInputHistory { + return { + entries: (mode) => (mode === "shell" ? shell.entries : normal.entries), + add(prompt, mode, comments) { + const current = mode === "shell" ? shell : normal + const setCurrent = mode === "shell" ? setShell : setNormal + const next = prependHistoryEntry(current.entries, prompt, comments) + if (next === current.entries) return + setCurrent("entries", next) + }, + } +} + +function createPersistedPromptInputHistory() { + const [normal, setNormal] = persisted( + Persist.global("prompt-history", ["prompt-history.v1"]), + createStore({ entries: [] }), + ) + const [shell, setShell] = persisted( + Persist.global("prompt-history-shell", ["prompt-history-shell.v1"]), + createStore({ entries: [] }), + ) + return createPromptInputHistoryStore(normal, setNormal, shell, setShell) +} + +export interface PromptInputProps { + class?: string + variant?: "dock" | "new-session" + state?: PromptInputState + history?: PromptInputHistory + submission?: PromptInputSubmission + controls: PromptInputControls + ref?: (el: HTMLDivElement) => void + newSessionWorktree?: string + onNewSessionWorktreeReset?: () => void + edit?: { id: string; prompt: Prompt; context: FollowupDraft["context"] } + onEditLoaded?: () => void + shouldQueue?: () => boolean + onQueue?: (draft: FollowupDraft) => void + onAbort?: () => void + onSubmit?: () => void + toolbar?: JSX.Element +} const EXAMPLES = [ "prompt.example.1", @@ -591,7 +681,7 @@ export const PromptInput: Component = (props) => { type: "resource", name: resource.name, uri: resource.uri, - client: resource.server, + client: resource.client, display: resource.name, description: resource.description, mime: resource.mimeType, @@ -709,7 +799,7 @@ export const PromptInput: Component = (props) => { title: cmd.name, description: cmd.description, type: "custom" as const, - // source: cmd.source, + source: cmd.source, })) return [...custom, ...builtin] @@ -1101,6 +1191,28 @@ export const PromptInput: Component = (props) => { return true } + const openCommands = () => { + const populated = prompt.dirty() || commentCount() > 0 + requestAnimationFrame(() => { + if (!populated) { + if (!addPart({ type: "text", content: "/", start: 0, end: 0 })) return + slashOnInput("") + setStore({ popover: "slash", slashMenu: false, slashMenuQuery: "" }) + return + } + slashOnInput("") + setStore({ popover: "slash", slashMenu: true, slashMenuQuery: "" }) + }) + } + + const openContext = () => { + requestAnimationFrame(() => { + if (!addPart({ type: "text", content: "@", start: 0, end: 0 })) return + atOnInput("") + setStore({ popover: "at", slashMenu: false, slashMenuQuery: "" }) + }) + } + const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => { history.add(prompt, mode, mode === "shell" ? [] : historyComments()) } @@ -1427,11 +1539,50 @@ export const PromptInput: Component = (props) => { (p) => p, ) + const designPlaceholder = () => { + if (store.mode === "shell") return placeholder() + return "Ask anything, / for commands, @ for context..." + } + + const modelControlState = createMemo(() => ({ + loading: providersLoading(), + shouldAnimate: providersShouldFadeIn(), + paid: props.controls.model.paid, + title: language.t("command.model.choose"), + keybind: command.keybindParts("model.choose"), + model: props.controls.model.selection, + providerID: props.controls.model.selection.current()?.provider?.id, + modelName: props.controls.model.selection.current()?.name ?? language.t("dialog.model.select.title"), + newLayoutDesigns: props.controls.newLayoutDesigns, + style: control(), + onClose: restoreFocus, + onUnpaidClick: () => { + if (props.controls.newLayoutDesigns) { + dialog.show(() => ) + return + } + dialog.show(() => ) + }, + })) + + const newSession = () => props.variant === "new-session" const bindEditorRef = (el: HTMLDivElement) => { editorRef = el restoreEndOnFocus = true props.ref?.(el) } + const showAgentControl = createMemo(() => props.controls.agents.visible && props.controls.agents.options.length > 0) + const agentControlState = createMemo(() => ({ + title: language.t("command.agent.cycle"), + keybind: command.keybindParts("agent.cycle"), + options: props.controls.agents.options, + current: props.controls.agents.current, + style: control(), + onSelect: (value) => { + props.controls.agents.select(value) + restoreFocus() + }, + })) return (
{(promptReady(), null)} @@ -1456,281 +1607,517 @@ export const PromptInput: Component = (props) => { onSlashMenuKeyDown={handleSlashMenuKeyDown} commandKeybind={command.keybind} commandKeybindParts={command.keybindParts} - newLayoutDesigns={false} + newLayoutDesigns={props.controls.newLayoutDesigns} t={(key) => language.t(key as Parameters[0])} /> - - - { - const active = comments.active() - return !!item.commentID && item.commentID === active?.id && item.path === active?.file - }} - openComment={openComment} - remove={(item) => { - if (item.commentID) comments.remove(item.path, item.commentID) - prompt.context.remove(item.key) - }} - newLayoutDesigns={false} - t={(key) => language.t(key as Parameters[0])} - /> - - dialog.show(() => ) - } - onRemove={removeAttachment} - removeLabel={language.t("prompt.attachment.remove")} - newLayoutDesigns={false} - /> -
{ - const target = e.target - if (!(target instanceof HTMLElement)) return - if (target.closest('[data-action="prompt-attach"], [data-action="prompt-submit"]')) { - return - } - editorRef?.focus() - }} - > -
(scrollRef = el)} - style={{ "scroll-padding-bottom": space }} - > -
+ +
+ -
- {placeholder()} -
-
- - - - - -
-
+ + !isCommentItem(item))} + active={(item) => { + const active = comments.active() + return !!item.commentID && item.commentID === active?.id && item.path === active?.file + }} + openComment={openComment} + remove={(item) => { + if (item.commentID) comments.remove(item.path, item.commentID) + prompt.context.remove(item.key) + }} + newLayoutDesigns={props.controls.newLayoutDesigns} + t={(key) => language.t(key as Parameters[0])} + /> + + dialog.show(() => ) + } + onRemove={removeAttachment} + removeLabel={language.t("prompt.attachment.remove")} + newLayoutDesigns={props.controls.newLayoutDesigns} + comments={contextItems().filter(isCommentItem)} + commentActive={(item) => { + const active = comments.active() + return !!item.commentID && item.commentID === active?.id && item.path === active?.file + }} + onOpenComment={openComment} + onRemoveComment={(item) => { + if (item.commentID) comments.remove(item.path, item.commentID) + prompt.context.remove(item.key) + }} + />
{ + const target = e.target + if (!(target instanceof HTMLElement)) return + if (target.closest('[data-action^="prompt-"]')) return + editorRef?.focus() }} > - - {language.t("prompt.mode.shell")} -
-
+ + + + + { + const active = comments.active() + return !!item.commentID && item.commentID === active?.id && item.path === active?.file + }} + openComment={openComment} + remove={(item) => { + if (item.commentID) comments.remove(item.path, item.commentID) + prompt.context.remove(item.key) + }} + newLayoutDesigns={props.controls.newLayoutDesigns} + t={(key) => language.t(key as Parameters[0])} + /> + + dialog.show(() => ) + } + onRemove={removeAttachment} + removeLabel={language.t("prompt.attachment.remove")} + newLayoutDesigns={props.controls.newLayoutDesigns} + /> +
{ + const target = e.target + if (!(target instanceof HTMLElement)) return + if (target.closest('[data-action="prompt-attach"], [data-action="prompt-submit"]')) { + return + } + editorRef?.focus() + }} + > +
(scrollRef = el)} + style={{ "scroll-padding-bottom": space }} + > +
+
+ {placeholder()} +
+
+ +
- {import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1" && state.debugTools && } + {import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1" && }
@@ -2447,7 +2429,7 @@ function UpdateAvailableToast(props: { onCleanup(() => { if (toastId === undefined) return - dismissToast(toastId) + toaster.dismiss(toastId) }) return null diff --git a/packages/app/src/pages/layout/project-avatar-state.ts b/packages/app/src/pages/layout/project-avatar-state.ts index 236f6bd405..8e5dc38d67 100644 --- a/packages/app/src/pages/layout/project-avatar-state.ts +++ b/packages/app/src/pages/layout/project-avatar-state.ts @@ -13,6 +13,7 @@ export function useSessionTabAvatarState( const global = useGlobal() const notification = useNotification() const permission = usePermission() + const permissionState = createMemo(() => permission.ensureServerState(server())) const connection = createMemo(() => global.servers.list().find((item) => ServerConnection.key(item) === server())) const sync = createMemo(() => { const conn = connection() @@ -21,10 +22,9 @@ export function useSessionTabAvatarState( const hasPermissions = createMemo(() => { const serverSync = sync() if (!serverSync) return false - const permissionState = permission.ensureServerState(server()) const [store] = serverSync.child(directory(), { bootstrap: false }) return !!sessionPermissionRequest(store.session, serverSync.session.data.permission, sessionId(), (item) => { - return !permissionState.autoResponds(item, directory()) + return !permissionState().autoResponds(item, directory()) }) }) const hasQuestions = createMemo(() => { @@ -34,11 +34,9 @@ export function useSessionTabAvatarState( return !!sessionQuestionRequest(store.session, serverSync.session.data.question, sessionId()) }) const needsAttention = createMemo(() => hasPermissions() || hasQuestions()) - const notificationState = createMemo(() => { - if (!connection()) return - return notification.ensureServerState(server()) - }) - const unread = createMemo(() => needsAttention() || (notificationState()?.session.unseenCount(sessionId()) ?? 0) > 0) + const unread = createMemo( + () => needsAttention() || notification.ensureServerState(server()).session.unseenCount(sessionId()) > 0, + ) const loading = createMemo(() => { const serverSync = sync() if (!serverSync) return false diff --git a/packages/app/src/pages/layout/session-tab-avatar.tsx b/packages/app/src/pages/layout/session-tab-avatar.tsx index 0902173643..3c776c8671 100644 --- a/packages/app/src/pages/layout/session-tab-avatar.tsx +++ b/packages/app/src/pages/layout/session-tab-avatar.tsx @@ -19,34 +19,16 @@ export function SessionTabAvatar(props: { () => props.directory, () => props.sessionId, ) - return ( - - ) -} - -export function SessionTabAvatarView(props: { - project?: LocalProject - directory: string - revealProjectOnHover?: boolean - unread: boolean - loading: boolean -}) { const projectAvatar = () => ( ) return ( - + sdk().directory) + const openProviderSettings = useSettingsDialog("providers") + useSettingsCommand() + const route = useSessionKey() + const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>() + const local = useLocal() + const model = createPromptModelSelection({ agent: local.agent.current }) + + useComposerCommands({ model }) + + let inputRef: HTMLDivElement | undefined + + const inputController = createPromptInputController({ + sessionKey: route.sessionKey, + sessionID: () => route.params.id, + queryOptions: serverSync().queryOptions, + model, }) - const project = createPromptProjectController({ - controls: draft.project.controls, - onDone: draft.input.restoreFocus, + const projectControls = createPromptProjectControls() + const projectController = createPromptProjectController({ + controls: projectControls, + onDone: () => inputRef?.focus(), }) - useNewSessionCommands({ - restoreFocus: draft.input.restoreFocus, - project: { - empty: project.empty, - open: () => project.setOpen(true), + + command.register("new-session", () => [ + { + id: "command.palette", + title: language.t("command.palette"), + hidden: true, + onSelect: async () => { + const { DialogSelectFile } = await import("@/components/dialog-select-file") + void dialog.show(() => ) + }, }, + { + id: "input.focus", + title: language.t("command.input.focus"), + category: language.t("command.category.view"), + keybind: "ctrl+l", + onSelect: () => inputRef?.focus(), + }, + ]) + + const [store, setStore] = createStore<{ worktree?: string }>({}) + const rightMount = useTitlebarRightMount() + + const showWorkspaceBar = createMemo(() => workspaceBarEnabled && sync().project?.vcs === "git") + const newSessionWorktree = createMemo(() => { + if (!showWorkspaceBar()) return "main" + if (store.worktree) return store.worktree + const project = sync().project + if (project && sdk().directory !== project.worktree) return sdk().directory + return "main" }) + const projectRoot = createMemo(() => sync().project?.worktree ?? sdk().directory) + const localBranch = createMemo(() => serverSync().child(projectRoot())[0].vcs?.branch) + const selectedBranch = createMemo(() => { + const worktree = newSessionWorktree() + if (worktree === "main" || worktree === "create") return localBranch() + return serverSync().child(worktree)[0].vcs?.branch ?? localBranch() + }) + createEffect(() => { - if (!draft.prompt.ready()) return - draft.input.restoreFocus() + if (!prompt.ready()) return + untrack(() => { + const text = searchParams.prompt + if (!text) return + prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) + setSearchParams({ ...searchParams, prompt: undefined }) + }) + }) + + createEffect(() => { + if (!prompt.ready()) return + requestAnimationFrame(() => inputRef?.focus()) }) const ready = Promise.resolve() - const [suspendUntilPromptReady] = createResource( - () => draft.prompt.readyPromise() ?? ready, + const [promptReady] = createResource( + () => prompt.ready.promise ?? ready, (promise) => promise.then(() => true), ) return (
- {suspendUntilPromptReady()} - + + {(mount) => ( + + + + + + + + )} +
- +
+
+ +
+ + {language.t("prompt.loading")} +
+ } + > +
+ { + inputRef = el + }} + newSessionWorktree={newSessionWorktree()} + onNewSessionWorktreeReset={() => setStore("worktree", undefined)} + onSubmit={() => comments.clear()} + toolbar={ + + + + } + /> + +
+ + + + setStore( + "worktree", + value === "main" && sync().project?.worktree !== sdk().directory + ? sync().project?.worktree + : value, + ) + } + onDone={() => inputRef?.focus()} + /> + +
+
+
+ +
+ + serverSync().child(sdk().directory)[0].provider_ready} + connected={() => providers.paid().length > 0} + openProviders={openProviderSettings} + /> +
+
) } + +function ProviderTip(props: { ready: () => boolean; connected: () => boolean; openProviders: () => void }) { + const language = useLanguage() + const [persistedState, setPersistedState, , persistedReady] = persisted( + Persist.global("new-session.provider-tip"), + createStore({ dismissedAt: 0 }), + ) + const visible = createMemo( + () => + props.ready() && + persistedReady() && + !props.connected() && + Date.now() - persistedState.dismissedAt >= providerTipDismissalDuration, + ) + + function dismiss() { + setPersistedState("dismissedAt", Date.now()) + } + + const [ref, setRef] = createSignal() + const presence = createPresence({ + show: () => visible(), + element: () => ref() ?? null, + }) + + return ( + +
+
+ + + + +
+
+
+ ) +} diff --git a/packages/app/src/pages/new-session/new-session-draft-controller.ts b/packages/app/src/pages/new-session/new-session-draft-controller.ts deleted file mode 100644 index bf22834e48..0000000000 --- a/packages/app/src/pages/new-session/new-session-draft-controller.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useSearchParams } from "@solidjs/router" -import { createEffect, untrack } from "solid-js" -import { usePromptInputV2Controller } from "@/components/prompt-input-v2" -import { useComments } from "@/context/comments" -import { useLocal } from "@/context/local" -import { usePrompt } from "@/context/prompt" -import { useServerSync } from "@/context/server-sync" -import { createPromptInputController, createPromptProjectControls } from "@/pages/session/composer" -import { createPromptModelSelection } from "@/pages/session/composer/prompt-model-selection" -import { useSessionKey } from "@/pages/session/session-layout" -import { useComposerCommands } from "@/pages/session/use-composer-commands" - -export function createNewSessionDraftController(workspace: { worktree: () => string; resetWorktree: () => void }) { - const prompt = usePrompt() - const serverSync = useServerSync() - const comments = useComments() - const local = useLocal() - const route = useSessionKey() - const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>() - const model = createPromptModelSelection({ agent: () => local.agent.current() }) - - useComposerCommands({ model }) - - const controls = createPromptInputController({ - sessionKey: route.sessionKey, - sessionID: () => route.params.id, - queryOptions: serverSync().queryOptions, - model, - }) - const projectControls = createPromptProjectControls() - const input = usePromptInputV2Controller({ - get controls() { - return controls() - }, - get newSessionWorktree() { - return workspace.worktree() - }, - onNewSessionWorktreeReset: workspace.resetWorktree, - onSubmit: comments.clear, - }) - - createEffect(() => { - if (!prompt.ready()) return - untrack(() => { - const text = searchParams.prompt - if (!text) return - prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) - setSearchParams({ ...searchParams, prompt: undefined }) - }) - }) - - return { - input, - prompt: { - ready: prompt.ready, - readyPromise: () => prompt.ready.promise, - }, - project: { - controls: projectControls, - }, - } -} - -export type NewSessionDraftController = ReturnType diff --git a/packages/app/src/pages/new-session/new-session-view.tsx b/packages/app/src/pages/new-session/new-session-view.tsx deleted file mode 100644 index 5960e64335..0000000000 --- a/packages/app/src/pages/new-session/new-session-view.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { Tooltip } from "@opencode-ai/ui/tooltip" -import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" -import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" -import { WordmarkV2 } from "@opencode-ai/ui/v2/wordmark-v2" -import { Show, createMemo, createSignal, type Accessor } from "solid-js" -import { createStore } from "solid-js/store" -import { Portal } from "solid-js/web" -import createPresence from "solid-presence" -import { PromptInputV2Composer } from "@/components/prompt-input-v2" -import { PromptGitStatus, PromptWorkspaceSelector } from "@/components/prompt-workspace-selector" -import { - PromptProjectAddButton, - PromptProjectSelector, - type PromptProjectController, -} from "@/components/prompt-project-selector" -import { StatusPopoverV2 } from "@/components/status-popover" -import { useLanguage } from "@/context/language" -import { useSDK } from "@/context/sdk" -import { useServerSync } from "@/context/server-sync" -import { useProviders } from "@/hooks/use-providers" -import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout" -import { Persist, persisted } from "@/utils/persist" -import type { NewSessionDraftController } from "./new-session-draft-controller" -import type { NewSessionWorkspaceController } from "./new-session-workspace-controller" - -const providerTipDismissalDuration = 30 * 24 * 60 * 60 * 1000 - -export function NewSessionView(props: { - input: NewSessionDraftController["input"] - project: PromptProjectController - workspace: NewSessionWorkspaceController -}) { - return ( -
-
-
-
- -
- - - - - -
- - - } - > - - -
-
-
-
-
- -
-
- ) -} - -export function NewSessionStatus(props: { mount: Accessor; visible: Accessor }) { - const language = useLanguage() - - return ( - - {(mount) => ( - - - - - - - - )} - - ) -} - -function ProviderTip() { - const language = useLanguage() - const dialog = useDialog() - const sdk = useSDK() - const serverSync = useServerSync() - const providers = useProviders(() => sdk().directory) - const [persistedState, setPersistedState, , persistedReady] = persisted( - Persist.global("new-session.provider-tip"), - createStore({ dismissedAt: 0 }), - ) - const visible = createMemo( - () => - serverSync().child(sdk().directory)[0].provider_ready && - persistedReady() && - providers.paid().length === 0 && - Date.now() - persistedState.dismissedAt >= providerTipDismissalDuration, - ) - const [ref, setRef] = createSignal() - const presence = createPresence({ - show: visible, - element: () => ref() ?? null, - }) - const openProviders = () => { - void import("@/components/dialog-connect-provider").then(({ DialogConnectProvider }) => { - void dialog.show(() => sdk().directory} />) - }) - } - - return ( - -
-
- - - - -
-
-
- ) -} diff --git a/packages/app/src/pages/new-session/new-session-workspace-controller.test.ts b/packages/app/src/pages/new-session/new-session-workspace-controller.test.ts deleted file mode 100644 index 2a79ae77fa..0000000000 --- a/packages/app/src/pages/new-session/new-session-workspace-controller.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { - normalizeNewSessionWorktree, - resolveNewSessionBranch, - resolveNewSessionWorktree, -} from "./new-session-workspace-controller" - -describe("new session workspace selection", () => { - test("uses main when the workspace bar is unavailable", () => { - expect( - resolveNewSessionWorktree({ - enabled: false, - selected: "/project/feature", - directory: "/project/feature", - projectWorktree: "/project", - }), - ).toBe("main") - }) - - test("derives an existing worktree from the current directory", () => { - expect( - resolveNewSessionWorktree({ enabled: true, directory: "/project/feature", projectWorktree: "/project" }), - ).toBe("/project/feature") - expect(resolveNewSessionWorktree({ enabled: true, directory: "/project", projectWorktree: "/project" })).toBe( - "main", - ) - }) - - test("normalizes main to the project root outside the main worktree", () => { - expect(normalizeNewSessionWorktree("main", "/project/feature", "/project")).toBe("/project") - expect(normalizeNewSessionWorktree("main", "/project", "/project")).toBe("main") - }) - - test("falls back to the local branch for main, create, and unknown worktrees", () => { - const branch = (worktree: string) => (worktree === "/project/feature" ? "feature" : undefined) - expect(resolveNewSessionBranch({ worktree: "main", local: "dev", worktreeBranch: branch })).toBe("dev") - expect(resolveNewSessionBranch({ worktree: "create", local: "dev", worktreeBranch: branch })).toBe("dev") - expect(resolveNewSessionBranch({ worktree: "/project/feature", local: "dev", worktreeBranch: branch })).toBe( - "feature", - ) - expect(resolveNewSessionBranch({ worktree: "/missing", local: "dev", worktreeBranch: branch })).toBe("dev") - }) -}) diff --git a/packages/app/src/pages/new-session/new-session-workspace-controller.ts b/packages/app/src/pages/new-session/new-session-workspace-controller.ts deleted file mode 100644 index f3fc9b2708..0000000000 --- a/packages/app/src/pages/new-session/new-session-workspace-controller.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { createMemo, createSignal } from "solid-js" -import { useSDK } from "@/context/sdk" -import { useServerSync } from "@/context/server-sync" -import { useSync } from "@/context/sync" - -const workspaceBarEnabled = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" - -export function resolveNewSessionWorktree(input: { - enabled: boolean - selected?: string - directory: string - projectWorktree?: string -}) { - if (!input.enabled) return "main" - if (input.selected) return input.selected - if (input.projectWorktree && input.directory !== input.projectWorktree) return input.directory - return "main" -} - -export function normalizeNewSessionWorktree(value: string, directory: string, projectWorktree?: string) { - if (value === "main" && projectWorktree !== directory) return projectWorktree - return value -} - -export function resolveNewSessionBranch(input: { - worktree: string - local?: string - worktreeBranch: (worktree: string) => string | undefined -}) { - if (input.worktree === "main" || input.worktree === "create") return input.local - return input.worktreeBranch(input.worktree) ?? input.local -} - -export function createNewSessionWorkspaceController() { - const sdk = useSDK() - const sync = useSync() - const serverSync = useServerSync() - const [worktree, setWorktree] = createSignal() - const visible = createMemo(() => workspaceBarEnabled && sync().project?.vcs === "git") - const value = createMemo(() => - resolveNewSessionWorktree({ - enabled: visible(), - selected: worktree(), - directory: sdk().directory, - projectWorktree: sync().project?.worktree, - }), - ) - const projectRoot = createMemo(() => sync().project?.worktree ?? sdk().directory) - const localBranch = createMemo(() => serverSync().child(projectRoot())[0].vcs?.branch) - const branch = createMemo(() => - resolveNewSessionBranch({ - worktree: value(), - local: localBranch(), - worktreeBranch: (worktree) => serverSync().child(worktree)[0].vcs?.branch, - }), - ) - - return { - selection: { - value, - reset: () => setWorktree(), - set: (worktree: string) => - setWorktree(normalizeNewSessionWorktree(worktree, sdk().directory, sync().project?.worktree)), - }, - project: { - root: projectRoot, - workspaces: () => sync().project?.sandboxes ?? [], - git: () => sync().project?.vcs === "git", - }, - bar: { - visible, - branch, - }, - } -} - -export type NewSessionWorkspaceController = ReturnType diff --git a/packages/app/src/pages/new-session/use-new-session-commands.tsx b/packages/app/src/pages/new-session/use-new-session-commands.tsx deleted file mode 100644 index a0d835f97f..0000000000 --- a/packages/app/src/pages/new-session/use-new-session-commands.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { useSettingsCommand } from "@/components/settings-dialog" -import { useCommand } from "@/context/command" -import { useLanguage } from "@/context/language" - -export function useNewSessionCommands(input: { - restoreFocus: () => void - project: { - empty: () => boolean - open: () => void - } -}) { - const command = useCommand() - const dialog = useDialog() - const language = useLanguage() - - useSettingsCommand() - command.register("new-session", () => [ - { - id: "command.palette", - title: language.t("command.palette"), - hidden: true, - onSelect: async () => { - const { DialogSelectFile } = await import("@/components/dialog-select-file") - void dialog.show(() => ) - }, - }, - { - id: "input.focus", - title: language.t("command.input.focus"), - category: language.t("command.category.view"), - keybind: "ctrl+l", - onSelect: input.restoreFocus, - }, - { - id: "project.select", - title: language.t("session.new.project.search"), - category: language.t("command.category.project"), - keybind: "mod+shift+o", - disabled: input.project.empty(), - onSelect: input.project.open, - }, - ]) -} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index c6e3a5fcbb..2a0bbfde91 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -6,7 +6,6 @@ import { batch, ErrorBoundary, onCleanup, - Suspense, Show, Match, Switch, @@ -59,7 +58,6 @@ import { useSync } from "@/context/sync" import { useTabs } from "@/context/tabs" import { TerminalProvider, useTerminal } from "@/context/terminal" import { PromptInput } from "@/components/prompt-input" -import { PromptInputV2Composer, usePromptInputV2Controller } from "@/components/prompt-input-v2" import { useSettingsCommand } from "@/components/settings-dialog" import { setCursorPosition } from "@/components/prompt-input/editor-dom" import { promptLength } from "@/components/prompt-input/history" @@ -533,6 +531,7 @@ export default function Page() { const info = createMemo(() => (params.id ? sync().session.get(params.id) : undefined)) const isChildSession = createMemo(() => !!info()?.parentID) + const diffs = createMemo(() => (params.id ? list(sync().data.session_diff[params.id]) : [])) const canReview = createMemo(() => !!sync().project) const reviewTab = createMemo(() => isDesktop()) const tabState = createSessionTabs({ @@ -631,8 +630,6 @@ export default function Page() { }) let reviewFrame: number | undefined - let todoFrame: number | undefined - let todoTimer: number | undefined let diffFrame: number | undefined let diffTimer: number | undefined @@ -668,8 +665,7 @@ export default function Page() { const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes") const wantsReview = createMemo(() => isDesktop() - ? desktopFileTreeOpen() || - (desktopReviewOpen() && (activeTab() === "review" || (newSessionDesign() && !!activeFileTab()))) + ? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review") : store.mobileTab === "changes", ) const vcsMode = createMemo(() => { @@ -690,8 +686,8 @@ export default function Page() { queryFn: mode ? () => sdk() - .api.vcs.diff({ location: { directory: sdk().directory }, mode: mode === "git" ? "working" : mode }) - .then((result) => result.data) + .client.vcs.diff({ mode }) + .then((result) => list(result.data)) .catch((error) => { console.debug("[session-review] failed to load vcs diff", { mode, error }) return [] @@ -738,12 +734,8 @@ export default function Page() { retry: 2, queryFn: () => sdk() - .api.vcs.diff({ - location: { directory: scope }, - mode: mode === "git" ? "working" : mode, - context, - }) - .then((result) => result.data), + .client.vcs.diff({ mode, directory: scope, context }) + .then((result) => result.data ?? []), }) .then((diffs) => diffs.find((diff) => diff.file === file)) @@ -891,41 +883,6 @@ export default function Page() { const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs - createEffect( - on( - () => { - const id = params.id - return [ - sdk().directory, - id, - id ? (sync().data.session_status[id]?.type ?? "idle") : "idle", - id ? composer.blocked() : false, - ] as const - }, - ([dir, id, status, blocked]) => { - if (todoFrame !== undefined) cancelAnimationFrame(todoFrame) - if (todoTimer !== undefined) window.clearTimeout(todoTimer) - todoFrame = undefined - todoTimer = undefined - if (!id) return - if (status === "idle" && !blocked) return - const cached = untrack(() => sync().data.todo[id] !== undefined) - - todoFrame = requestAnimationFrame(() => { - todoFrame = undefined - todoTimer = window.setTimeout(() => { - todoTimer = undefined - if (sdk().directory !== dir || params.id !== id) return - untrack(() => { - void sync().session.todo(id, cached ? { force: true } : undefined) - }) - }, 0) - }) - }, - { defer: true }, - ), - ) - createEffect( on( () => visibleUserMessages().at(-1)?.id, @@ -950,11 +907,10 @@ export default function Page() { ) const stopVcs = sdk().event.listen((evt) => { - const details = evt.details as { type: string; properties?: unknown } - if (details.type !== "file.watcher.updated" && details.type !== "filesystem.changed") return + if (evt.details.type !== "filesystem.changed") return const props = - typeof details.properties === "object" && details.properties - ? (details.properties as Record) + typeof evt.details.properties === "object" && evt.details.properties + ? (evt.details.properties as Record) : undefined const file = typeof props?.file === "string" ? props.file : undefined if (!file || file.startsWith(".git/")) return @@ -1469,6 +1425,44 @@ export default function Page() { requestAnimationFrame(() => attempt(0)) }) + createEffect(() => { + const id = params.id + if (!id) return + + if (!wantsReview()) return + if (sync().data.session_diff[id] !== undefined) return + if (sync().status === "loading") return + + void sync().session.diff(id) + }) + + createEffect( + on( + () => [sessionKey(), wantsReview()] as const, + ([key, wants]) => { + if (diffFrame !== undefined) cancelAnimationFrame(diffFrame) + if (diffTimer !== undefined) window.clearTimeout(diffTimer) + diffFrame = undefined + diffTimer = undefined + if (!wants) return + + const id = params.id + if (!id) return + if (!untrack(() => sync().data.session_diff[id] !== undefined)) return + + diffFrame = requestAnimationFrame(() => { + diffFrame = undefined + diffTimer = window.setTimeout(() => { + diffTimer = undefined + if (sessionKey() !== key) return + void sync().session.diff(id, { force: true }) + }, 0) + }) + }, + { defer: true }, + ), + ) + let treeDir: string | undefined createEffect(() => { const dir = sdk().directory @@ -1724,7 +1718,7 @@ export default function Page() { setFollowup("failed", input.sessionID, undefined) const ok = await sendFollowupDraft({ - api: sdk().api.session, + client: sdk().client, sync: sync(), serverSync: serverSync(), draft: item, @@ -1820,13 +1814,13 @@ export default function Page() { const halt = (sessionID: string) => busy(sessionID) ? sdk() - .api.session.interrupt({ sessionID }) + .client.session.abort({ sessionID }) .catch(() => {}) : Promise.resolve() const revertMutation = useMutation(() => ({ mutationFn: async (input: { sessionID: string; messageID: string }) => { - const session = sdk().api.session + const client = sdk().client const target = sync() const last = target.session.get(input.sessionID)?.revert const value = draft(input.messageID) @@ -1836,8 +1830,10 @@ export default function Page() { roll(input.sessionID, { messageID: input.messageID }, target) prompt.set(value) }, - request: () => halt(input.sessionID).then(() => session.revert.stage(input)), - complete: () => undefined, + request: () => halt(input.sessionID).then(() => client.session.revert(input)), + complete: (result) => { + if (result.data) merge(result.data, target) + }, rollback: () => roll(input.sessionID, last, target), fail, }) @@ -1849,7 +1845,7 @@ export default function Page() { const sessionID = params.id if (!sessionID) return - const session = sdk().api.session + const client = sdk().client const target = sync() const next = userMessages().find((item) => item.id > id) const last = target.session.get(sessionID)?.revert @@ -1866,9 +1862,11 @@ export default function Page() { }, request: () => !next - ? halt(sessionID).then(() => session.revert.clear({ sessionID })) - : halt(sessionID).then(() => session.revert.stage({ sessionID, messageID: next.id }).then(() => undefined)), - complete: () => undefined, + ? halt(sessionID).then(() => client.session.unrevert({ sessionID })) + : halt(sessionID).then(() => client.session.revert({ sessionID, messageID: next.id })), + complete: (result) => { + if (result.data) merge(result.data, target) + }, rollback: () => roll(sessionID, last, target), fail, }) @@ -2000,8 +1998,6 @@ export default function Page() { onCleanup(() => { if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame) - if (todoFrame !== undefined) cancelAnimationFrame(todoFrame) - if (todoTimer !== undefined) window.clearTimeout(todoTimer) if (diffFrame !== undefined) cancelAnimationFrame(diffFrame) if (diffTimer !== undefined) window.clearTimeout(diffTimer) if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame) @@ -2010,6 +2006,78 @@ export default function Page() { useUsageExceededDialogs() + const composerRegion = () => { + const controller = createSessionComposerRegionController({ + state: composer, + sessionKey, + sessionID: () => params.id, + prompt, + centered, + followup: () => + params.id && !isChildSession() + ? { + items: followupDock(), + sending: sendingFollowup(), + onSend: (id) => void sendFollowup(params.id!, id, { manual: true }), + onEdit: editFollowup, + } + : undefined, + revert: () => + rolled().length > 0 + ? { + items: rolled(), + restoring: restoring(), + disabled: reverting(), + onRestore: restore, + } + : undefined, + onResponseSubmit: resumeScroll, + openParent: () => { + const id = info()?.parentID + if (!id) return + navigate( + params.serverKey + ? sessionHref(requireServerKey(params.serverKey), id) + : legacySessionHref(sdk().directory, id), + ) + }, + setPromptRef: (el) => { + inputRef = el + }, + setDockRef: (el) => { + promptDock = el + }, + }) + return ( + { + inputRef = el + }} + newSessionWorktree={newSessionWorktree()} + onNewSessionWorktreeReset={() => setStore("newSessionWorktree", "main")} + onSubmit={() => { + comments.clear() + resumeScroll() + }} + edit={editingFollowup()} + onEditLoaded={clearFollowupEdit} + shouldQueue={queueEnabled} + onQueue={queueFollowup} + onAbort={() => { + const id = params.id + if (!id) return + setFollowup("paused", id, true) + }} + /> + } + /> + ) + } + const mobileTabs = (compact = false, bottom = false) => (
- - {(_) => { - const controller = createSessionComposerRegionController({ - state: composer, - sessionKey, - sessionID: () => params.id, - prompt, - ready: () => !store.deferRender && messagesReady(), - centered, - todo: { - collapsed: () => view().todoCollapsed.get(), - onToggle: () => view().todoCollapsed.set(!view().todoCollapsed.get()), - }, - followup: () => - params.id && !isChildSession() - ? { - items: followupDock(), - sending: sendingFollowup(), - onSend: (id) => void sendFollowup(params.id!, id, { manual: true }), - onEdit: editFollowup, - } - : undefined, - revert: () => - rolled().length > 0 - ? { - items: rolled(), - restoring: restoring(), - disabled: reverting(), - onRestore: restore, - } - : undefined, - onResponseSubmit: resumeScroll, - openParent: () => { - const id = info()?.parentID - if (!id) return - navigate( - params.serverKey - ? sessionHref(requireServerKey(params.serverKey), id) - : legacySessionHref(sdk().directory, id), - ) - }, - setPromptRef: (el) => { - inputRef = el - }, - setDockRef: (el) => { - promptDock = el - }, - }) - return ( - { - inputRef = el - }} - newSessionWorktree={newSessionWorktree()} - onNewSessionWorktreeReset={() => setStore("newSessionWorktree", "main")} - onSubmit={() => { - comments.clear() - resumeScroll() - }} - edit={editingFollowup()} - onEditLoaded={clearFollowupEdit} - shouldQueue={queueEnabled} - onQueue={queueFollowup} - onAbort={() => { - const id = params.id - if (!id) return - setFollowup("paused", id, true) - }} - /> - } - > - {(_) => { - const controller = usePromptInputV2Controller({ - get controls() { - return inputController() - }, - ref: (el) => { - inputRef = el - }, - get newSessionWorktree() { - return newSessionWorktree() - }, - onNewSessionWorktreeReset: () => setStore("newSessionWorktree", "main"), - onSubmit: () => { - comments.clear() - resumeScroll() - }, - get edit() { - return editingFollowup() - }, - onEditLoaded: clearFollowupEdit, - shouldQueue: queueEnabled, - onQueue: queueFollowup, - onAbort: () => { - const id = params.id - if (!id) return - setFollowup("paused", id, true) - }, - }) - return - }} - - } - /> - ) - }} - + {(_) => composerRegion()} {mobileTabs(true, true)} ) @@ -2298,53 +2253,49 @@ export default function Page() {
- - - +
- - hasReview() || reviewV2State.sidebarOpened()} - reviewCount={reviewCount} - reviewPanel={reviewPanelV2} - reviewSidebarToggle={(disabled) => ( - - )} - fileBrowserState={reviewV2State} - activeDiff={activeReviewFile()} - focusReviewDiff={focusReviewDiff} - reviewSnap={ui.reviewSnap} - size={size} - stacked={desktopV2PanelLayout().stacked} - /> - + hasReview() || reviewV2State.sidebarOpened()} + reviewCount={reviewCount} + reviewPanel={reviewPanelV2} + reviewSidebarToggle={(disabled) => ( + + )} + fileBrowserState={reviewV2State} + activeDiff={activeReviewFile()} + focusReviewDiff={focusReviewDiff} + reviewSnap={ui.reviewSnap} + size={size} + stacked={desktopV2PanelLayout().stacked} + />
diff --git a/packages/app/src/pages/session/composer/session-composer-controls.ts b/packages/app/src/pages/session/composer/session-composer-controls.ts index a9b0070bc0..3ead9599bb 100644 --- a/packages/app/src/pages/session/composer/session-composer-controls.ts +++ b/packages/app/src/pages/session/composer/session-composer-controls.ts @@ -2,7 +2,7 @@ import { base64Encode } from "@opencode-ai/core/util/encode" import { createQuery } from "@tanstack/solid-query" import { useNavigate, useSearchParams } from "@solidjs/router" import { type Accessor, createMemo } from "solid-js" -import type { PromptInputControls } from "@/components/prompt-input/contracts" +import type { PromptInputControls } from "@/components/prompt-input" import type { PromptProjectControls } from "@/components/prompt-project-selector" import { useDirectoryPicker } from "@/components/directory-picker" import { useGlobal } from "@/context/global" @@ -12,6 +12,7 @@ import type { QueryOptionsApi } from "@/context/server-sync" import { useServerSDK } from "@/context/server-sdk" import { serverName, ServerConnection, useServer } from "@/context/server" import { useSDK } from "@/context/sdk" +import { useSettings } from "@/context/settings" import { useSync } from "@/context/sync" import { useTabs } from "@/context/tabs" import { useProviders } from "@/hooks/use-providers" @@ -25,39 +26,36 @@ export function createPromptInputController(input: { }) { const layout = useLayout() const local = useLocal() - const sdk = useSDK() + const providers = useProviders() + const settings = useSettings() const sync = useSync() - const providers = useProviders(() => sdk().directory) + const sdk = useSDK() const view = layout.view(input.sessionKey) const agentsQuery = createQuery(() => input.queryOptions.agents(pathKey(sdk().directory))) const globalProvidersQuery = createQuery(() => input.queryOptions.providers(null)) const providersQuery = createQuery(() => input.queryOptions.providers(pathKey(sdk().directory))) - return createMemo(() => { - return { - agents: { - available: sync().data.agent, - options: local.agent.list().map((agent) => agent.name), - current: local.agent.current()?.name ?? "", - loading: agentsQuery.isLoading, - visible: local.agent.visible(), - select: local.agent.set, - }, - model: { - selection: input.model ?? local.model, - paid: providers.paid().length > 0, - loading: - (local.agent.visible() && agentsQuery.isLoading) || - providersQuery.isLoading || - globalProvidersQuery.isLoading, - }, - session: { - id: input.sessionID(), - tabs: layout.tabs(input.sessionKey), - reviewPanel: view.reviewPanel, - }, - } - }) + return createMemo(() => ({ + agents: { + available: sync().data.agent, + options: local.agent.list().map((agent) => agent.name), + current: local.agent.current()?.name ?? "", + loading: agentsQuery.isLoading, + visible: local.agent.visible(), + select: local.agent.set, + }, + model: { + selection: input.model ?? local.model, + paid: providers.paid().length > 0, + loading: agentsQuery.isLoading || providersQuery.isLoading || globalProvidersQuery.isLoading, + }, + session: { + id: input.sessionID(), + tabs: layout.tabs(input.sessionKey), + reviewPanel: view.reviewPanel, + }, + newLayoutDesigns: settings.general.newLayoutDesigns(), + })) } export function createPromptProjectControls() { diff --git a/packages/app/src/pages/session/composer/session-composer-region-controller.ts b/packages/app/src/pages/session/composer/session-composer-region-controller.ts index 4073f1c191..ff425b4c20 100644 --- a/packages/app/src/pages/session/composer/session-composer-region-controller.ts +++ b/packages/app/src/pages/session/composer/session-composer-region-controller.ts @@ -1,7 +1,4 @@ -import { createResizeObserver } from "@solid-primitives/resize-observer" -import { useSpring } from "@opencode-ai/ui/motion-spring" -import { type Accessor, createEffect, createMemo, createResource, onCleanup } from "solid-js" -import { createStore } from "solid-js/store" +import { type Accessor, createEffect, createMemo, createResource } from "solid-js" import type { PromptInputState } from "@/components/prompt-input" import { useSync } from "@/context/sync" import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff" @@ -26,12 +23,7 @@ export function createSessionComposerRegionController(input: { sessionKey: Accessor sessionID: Accessor prompt: PromptInputState - ready: Accessor centered: Accessor - todo: { - collapsed: Accessor - onToggle: () => void - } followup: Accessor revert: Accessor onResponseSubmit: () => void @@ -40,41 +32,6 @@ export function createSessionComposerRegionController(input: { setDockRef: (el: HTMLDivElement) => void }) { const sync = useSync() - const [store, setStore] = createStore({ - ready: input.ready() || input.state.dock(), - height: 320, - body: undefined as HTMLDivElement | undefined, - }) - let timer: number | undefined - let frame: number | undefined - - const clear = () => { - if (timer !== undefined) window.clearTimeout(timer) - if (frame !== undefined) cancelAnimationFrame(frame) - timer = undefined - frame = undefined - } - - createEffect(() => { - input.sessionKey() - const ready = input.ready() - const dock = input.state.dock() - - clear() - if (store.ready || (!ready && !dock)) return - if (dock) { - setStore("ready", true) - return - } - - frame = requestAnimationFrame(() => { - frame = undefined - timer = window.setTimeout(() => { - setStore("ready", true) - timer = undefined - }, 140) - }) - }) createEffect(() => { if (!input.prompt.ready()) return @@ -92,27 +49,10 @@ export function createSessionComposerRegionController(input: { }) }) - createEffect(() => { - const el = store.body - if (!el) return - const update = () => setStore("height", el.getBoundingClientRect().height) - createResizeObserver(el, update) - update() - }) - - onCleanup(clear) - const parentID = createMemo(() => { const id = input.sessionID() return id ? sync().session.get(id)?.parentID : undefined }) - const open = createMemo(() => store.ready && input.state.dock() && !input.state.closing()) - const progress = useSpring( - () => (open() ? 1 : 0), - { visualDuration: 0.3, bounce: 0 }, - () => `${input.sessionKey()}\0${store.ready}`, - ) - const value = createMemo(() => Math.max(0, Math.min(1, progress()))) const ready = Promise.resolve() const [promptReady] = createResource( () => input.prompt.ready.promise ?? ready, @@ -122,7 +62,6 @@ export function createSessionComposerRegionController(input: { return { state: input.state, centered: input.centered, - todo: input.todo, followup: input.followup, revert: input.revert, onResponseSubmit: input.onResponseSubmit, @@ -134,11 +73,7 @@ export function createSessionComposerRegionController(input: { showComposer: () => !input.state.blocked() || !!parentID(), handoffPrompt: () => getSessionHandoff(input.sessionKey())?.prompt, promptReady: () => input.prompt.ready() || promptReady(), - dock: () => (store.ready && input.state.dock()) || value() > 0.001, - dockProgress: value, - dockHeight: () => Math.max(78, store.height), - lift: () => (input.revert()?.items.length ? 18 : 36 * value()), - setDockBodyRef: (el: HTMLDivElement) => setStore("body", el), + lift: () => (input.revert()?.items.length ? 18 : 0), } } diff --git a/packages/app/src/pages/session/composer/session-composer-region.tsx b/packages/app/src/pages/session/composer/session-composer-region.tsx index 600ff41e3d..feb2b0aee4 100644 --- a/packages/app/src/pages/session/composer/session-composer-region.tsx +++ b/packages/app/src/pages/session/composer/session-composer-region.tsx @@ -5,7 +5,6 @@ import { SessionPermissionDock } from "@/pages/session/composer/session-permissi import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock" import { SessionFollowupDock } from "@/pages/session/composer/session-followup-dock" import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock" -import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock" import type { SessionComposerRegionController } from "./session-composer-region-controller" export function SessionComposerRegion(props: { @@ -60,28 +59,6 @@ export function SessionComposerRegion(props: { - -
-
- -
-
-
)} -
+
{controller.handoffPrompt() || language.t("prompt.loading")}
@@ -109,11 +83,7 @@ export function SessionComposerRegion(props: { > {(revert) => ( -
+
@@ -104,35 +103,3 @@ describe("sessionQuestionRequest", () => { expect(sessionQuestionRequest(sessions, questions, "root")?.id).toBe("q-grand") }) }) - -describe("todoState", () => { - test("hides when there are no todos", () => { - expect(todoState({ count: 0, done: false, live: true })).toBe("hide") - }) - - test("opens while the session is still working", () => { - expect(todoState({ count: 2, done: false, live: true })).toBe("open") - }) - - test("closes completed todos after a running turn", () => { - expect(todoState({ count: 2, done: true, live: true })).toBe("close") - }) - - test("clears stale todos when the turn ends", () => { - expect(todoState({ count: 2, done: false, live: false })).toBe("clear") - }) - - test("clears completed todos when the session is no longer live", () => { - expect(todoState({ count: 2, done: true, live: false })).toBe("clear") - }) -}) - -describe("todoDockAtBoundary", () => { - test("shows active todos when entering a session", () => { - expect(todoDockAtBoundary("open")).toBe(true) - }) - - test("hides completed todos when entering a session", () => { - expect(todoDockAtBoundary("close")).toBe(false) - }) -}) diff --git a/packages/app/src/pages/session/composer/session-composer-state.ts b/packages/app/src/pages/session/composer/session-composer-state.ts index f54e0c9e4f..eb282c15fc 100644 --- a/packages/app/src/pages/session/composer/session-composer-state.ts +++ b/packages/app/src/pages/session/composer/session-composer-state.ts @@ -1,35 +1,20 @@ -import { createEffect, createMemo, on, onCleanup } from "solid-js" +import { createMemo } from "solid-js" import { createStore } from "solid-js/store" -import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2" +import type { PermissionRequest, QuestionRequest } from "@opencode-ai/sdk/v2" import { useParams } from "@solidjs/router" import { showToast } from "@/utils/toast" -import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { usePermission } from "@/context/permission" import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree" -export const todoState = (input: { - count: number - done: boolean - live: boolean -}): "hide" | "clear" | "open" | "close" => { - if (input.count === 0) return "hide" - if (!input.live) return "clear" - if (!input.done) return "open" - return "close" -} - -export const todoDockAtBoundary = (state: ReturnType) => state === "open" - const idle = { type: "idle" as const } -export function createSessionComposerController(options?: { closeMs?: number | (() => number) }) { +export function createSessionComposerController() { const params = useParams() const sdk = useSDK() const sync = useSync() - const serverSync = useServerSync() const language = useLanguage() const permission = usePermission() @@ -49,24 +34,8 @@ export function createSessionComposerController(options?: { closeMs?: number | ( return !!permissionRequest() || !!questionRequest() }) - const todos = createMemo((): Todo[] => { - const id = params.id - if (!id) return [] - return serverSync().session.data.todo[id] ?? [] - }) - - const done = createMemo( - () => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"), - ) - - const live = createMemo(() => sync().data.session_working(params.id ?? "") || blocked()) - const [store, setStore] = createStore({ - sessionID: params.id, responding: undefined as string | undefined, - dock: todos().length > 0 && !done() && live(), - closing: false, - opening: false, }) const permissionResponding = createMemo(() => { @@ -82,7 +51,7 @@ export function createSessionComposerController(options?: { closeMs?: number | ( setStore("responding", perm.id) sdk() - .api.permission.reply({ sessionID: perm.sessionID, requestID: perm.id, reply: response }) + .client.permission.respond({ sessionID: perm.sessionID, permissionID: perm.id, response }) .catch((err: unknown) => { const description = err instanceof Error ? err.message : String(err) showToast({ title: language.t("common.requestFailed"), description }) @@ -92,112 +61,12 @@ export function createSessionComposerController(options?: { closeMs?: number | ( }) } - let timer: number | undefined - let raf: number | undefined - - const closeMs = () => { - const value = options?.closeMs - if (typeof value === "function") return Math.max(0, value()) - if (typeof value === "number") return Math.max(0, value) - return 400 - } - - const scheduleClose = () => { - if (timer) window.clearTimeout(timer) - timer = window.setTimeout(() => { - setStore({ dock: false, closing: false }) - timer = undefined - }, closeMs()) - } - - // Keep stale turn todos from reopening if the model never clears them. - const clear = () => { - const id = params.id - if (!id) return - sync().set("todo", id, []) - } - - createEffect( - on( - () => [params.id, todos().length, done(), live()] as const, - ([id, count, complete, active], previous) => { - if (raf) cancelAnimationFrame(raf) - raf = undefined - - const next = todoState({ - count, - done: complete, - live: active, - }) - - if (!previous || previous[0] !== id) { - if (timer) window.clearTimeout(timer) - timer = undefined - setStore({ sessionID: id, dock: todoDockAtBoundary(next), closing: false, opening: false }) - if (next === "clear") clear() - return - } - - if (next === "hide") { - if (timer) window.clearTimeout(timer) - timer = undefined - setStore({ dock: false, closing: false, opening: false }) - return - } - - if (next === "clear") { - if (timer) window.clearTimeout(timer) - timer = undefined - clear() - return - } - - if (next === "open") { - if (timer) window.clearTimeout(timer) - timer = undefined - const hidden = !store.dock || store.closing - setStore({ dock: true, closing: false }) - if (hidden) { - setStore("opening", true) - raf = requestAnimationFrame(() => { - setStore("opening", false) - raf = undefined - }) - return - } - setStore("opening", false) - return - } - - setStore({ dock: true, opening: false, closing: true }) - if (!timer) scheduleClose() - }, - ), - ) - - onCleanup(() => { - if (!timer) return - window.clearTimeout(timer) - }) - - onCleanup(() => { - if (!raf) return - cancelAnimationFrame(raf) - }) - return { blocked, questionRequest, permissionRequest, permissionResponding, decide, - todos, - dock: () => - store.sessionID === params.id - ? store.dock - : todoDockAtBoundary(todoState({ count: todos().length, done: done(), live: live() })), - closing: () => store.sessionID === params.id && store.closing, - opening: () => store.sessionID === params.id && store.opening, } } diff --git a/packages/app/src/pages/session/composer/session-question-dock.tsx b/packages/app/src/pages/session/composer/session-question-dock.tsx index 941424e247..445a9f47a0 100644 --- a/packages/app/src/pages/session/composer/session-question-dock.tsx +++ b/packages/app/src/pages/session/composer/session-question-dock.tsx @@ -223,8 +223,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit } const replyMutation = useMutation(() => ({ - mutationFn: (answers: QuestionAnswer[]) => - sdk().api.question.reply({ sessionID: props.request.sessionID, requestID: props.request.id, answers }), + mutationFn: (answers: QuestionAnswer[]) => sdk().client.question.reply({ requestID: props.request.id, answers }), onMutate: () => { props.onSubmit() }, @@ -236,7 +235,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit })) const rejectMutation = useMutation(() => ({ - mutationFn: () => sdk().api.question.reject({ sessionID: props.request.sessionID, requestID: props.request.id }), + mutationFn: () => sdk().client.question.reject({ requestID: props.request.id }), onMutate: () => { props.onSubmit() }, diff --git a/packages/app/src/pages/session/composer/session-revert-dock.stories.tsx b/packages/app/src/pages/session/composer/session-revert-dock.stories.tsx index ce3fd26b02..dd030870a1 100644 --- a/packages/app/src/pages/session/composer/session-revert-dock.stories.tsx +++ b/packages/app/src/pages/session/composer/session-revert-dock.stories.tsx @@ -1,6 +1,5 @@ import { For } from "solid-js" import { createStore } from "solid-js/store" -import { DockShell } from "@opencode-ai/ui/dock-surface" import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock" import { SettingsProvider, useSettings } from "@/context/settings" @@ -79,17 +78,12 @@ function Stage(props: { count: number }) { {/* Reproduce the real composer stack: dock + card overlapping the dock's bottom by lift() = 18px */}
- Ask anything... - +
diff --git a/packages/app/src/pages/session/composer/session-todo-dock.tsx b/packages/app/src/pages/session/composer/session-todo-dock.tsx deleted file mode 100644 index d44b490262..0000000000 --- a/packages/app/src/pages/session/composer/session-todo-dock.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import type { Todo } from "@opencode-ai/sdk/v2" -import { AnimatedNumber } from "@opencode-ai/ui/animated-number" -import { Checkbox } from "@opencode-ai/ui/checkbox" -import { DockTray } from "@opencode-ai/ui/dock-surface" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { useSpring } from "@opencode-ai/ui/motion-spring" -import { TextReveal } from "@opencode-ai/ui/text-reveal" -import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough" -import { createResizeObserver } from "@solid-primitives/resize-observer" -import { Index, createEffect, createMemo } from "solid-js" -import { Dynamic } from "solid-js/web" -import { createStore } from "solid-js/store" -import { useLanguage } from "@/context/language" -import { useSettings } from "@/context/settings" - -const doneToken = "\u0000done\u0000" -const totalToken = "\u0000total\u0000" - -function dot(status: Todo["status"]) { - if (status !== "in_progress") return undefined - return ( - - - - ) -} - -export function SessionTodoDock(props: { - todos: Todo[] - collapsed: boolean - onToggle: () => void - collapseLabel: string - expandLabel: string - dockProgress: number -}) { - const language = useLanguage() - const settings = useSettings() - const [store, setStore] = createStore({ - height: 78, - }) - - const total = createMemo(() => props.todos.length) - const done = createMemo(() => props.todos.filter((todo) => todo.status === "completed").length) - const label = createMemo(() => language.t("session.todo.progress", { done: done(), total: total() })) - const progress = createMemo(() => - language - .t("session.todo.progress", { done: doneToken, total: totalToken }) - .split(/(\u0000done\u0000|\u0000total\u0000)/), - ) - - const active = createMemo( - () => - props.todos.find((todo) => todo.status === "in_progress") ?? - props.todos.find((todo) => todo.status === "pending") ?? - props.todos.filter((todo) => todo.status === "completed").at(-1) ?? - props.todos[0], - ) - - const preview = createMemo(() => active()?.content ?? "") - const collapse = useSpring(() => (props.collapsed ? 1 : 0), { visualDuration: 0.3, bounce: 0 }) - const dock = createMemo(() => Math.max(0, Math.min(1, props.dockProgress))) - const shut = createMemo(() => 1 - dock()) - const value = createMemo(() => Math.max(0, Math.min(1, collapse()))) - const hide = createMemo(() => Math.max(value(), shut())) - const off = createMemo(() => hide() > 0.98) - const turn = createMemo(() => Math.max(0, Math.min(1, value()))) - const full = createMemo(() => Math.max(78, store.height)) - let contentRef: HTMLDivElement | undefined - - createEffect(() => { - const el = contentRef - if (!el) return - const update = () => { - setStore("height", (height) => Math.max(height, el.scrollHeight)) - } - update() - createResizeObserver(el, update) - }) - - return ( - -
-
{ - if (event.key !== "Enter" && event.key !== " ") return - event.preventDefault() - props.onToggle() - }} - > - - - {(item) => - item() === doneToken ? ( - - ) : item() === totalToken ? ( - - ) : ( - {item()} - ) - } - - -
- -
-
- { - event.preventDefault() - event.stopPropagation() - }} - onClick={(event) => { - event.stopPropagation() - props.onToggle() - }} - aria-label={props.collapsed ? props.expandLabel : props.collapseLabel} - /> -
-
- -
0.1, - }} - style={{ - visibility: off() ? "hidden" : "visible", - opacity: `${Math.max(0, Math.min(1, 1 - hide()))}`, - }} - > - -
-
-
- ) -} - -function TodoList(props: { todos: Todo[] }) { - const [store, setStore] = createStore({ - stuck: false, - }) - - return ( -
-
{ - setStore("stuck", e.currentTarget.scrollTop > 0) - }} - > - - {(todo) => ( - - - - )} - -
-
-
- ) -} diff --git a/packages/app/src/pages/session/composer/todo-panel-motion.stories.tsx b/packages/app/src/pages/session/composer/todo-panel-motion.stories.tsx deleted file mode 100644 index 41d1f984bd..0000000000 --- a/packages/app/src/pages/session/composer/todo-panel-motion.stories.tsx +++ /dev/null @@ -1,622 +0,0 @@ -// @ts-nocheck -import { createEffect, createMemo, onCleanup } from "solid-js" -import { createStore } from "solid-js/store" -import type { Todo } from "@opencode-ai/sdk/v2" -import { useServerSync } from "@/context/global-sync" -import { PromptInput } from "@/components/prompt-input" -import { usePrompt } from "@/context/prompt" -import { - SessionComposerRegion, - createSessionComposerController, - createSessionComposerRegionController, -} from "@/pages/session/composer" - -export default { - title: "UI/Todo Panel Motion", - id: "components-todo-panel-motion", - tags: ["autodocs"], - parameters: { - docs: { - description: { - component: `### Overview -This playground renders the real session composer region from app code. - -### Source path -- \`packages/app/src/pages/session/composer/session-composer-region.tsx\` - -### Includes -- \`SessionTodoDock\` (real) -- \`PromptInput\` (real) - -No visual reimplementation layer is used for the dock/input stack.`, - }, - }, - }, -} - -const pool = [ - "Refactor ToolStatusTitle DOM measurement to offscreen global measurer (unconstrained by timeline layout)", - "Remove inline measure nodes/CSS hooks and keep width morph behavior intact", - "Run typechecks/tests and report what changed", - "Verify reduced-motion behavior in timeline", - "Review diff for animation edge cases", - "Document rollout notes in PR description", - "Check keyboard and screen reader semantics", - "Add storybook controls for iteration speed", -] - -const btn = (accent?: boolean) => - ({ - padding: "6px 14px", - "border-radius": "6px", - border: "1px solid var(--color-divider, #333)", - background: accent ? "var(--color-accent, #58f)" : "var(--color-fill-element, #222)", - color: "var(--color-text, #eee)", - cursor: "pointer", - "font-size": "13px", - }) as const - -const controls = { - agents: { available: [], options: ["build"], current: "build", loading: false, visible: true, select: () => {} }, - model: { - selection: { - current: () => ({ id: "claude-3-7-sonnet", name: "Claude 3.7 Sonnet", provider: { id: "anthropic" } }), - variant: { list: () => [], current: () => undefined, set: () => {} }, - }, - paid: true, - loading: false, - }, - session: { - id: "story-session", - tabs: { active: () => undefined, all: () => [], open: () => {}, setActive: () => {} }, - reviewPanel: { opened: () => false, open: () => {} }, - }, -} - -const css = ` -[data-component="todo-stage"] { - display: grid; - gap: 20px; - padding: 20px; -} - -[data-component="todo-preview"] { - height: 560px; - min-height: 0; -} - -[data-component="todo-session-root"] { - position: relative; - width: 100%; - height: 100%; - overflow: hidden; - display: flex; - flex-direction: column; - background: var(--background-base); - border: 1px solid var(--border-weak-base); - border-radius: 12px; -} - -[data-component="todo-session-frame"] { - flex: 1 1 auto; - min-height: 0; - display: flex; - flex-direction: column; -} - -[data-component="todo-session-panel"] { - position: relative; - flex: 1 1 auto; - min-height: 0; - height: 100%; - display: flex; - flex-direction: column; - background: var(--background-stronger); -} - -[data-slot="todo-preview-content"] { - flex: 1 1 auto; - min-height: 0; - overflow: hidden; -} - -[data-slot="todo-preview-scroll"] { - height: 100%; - overflow: auto; - min-height: 0; - padding: 14px 16px; - display: flex; - flex-direction: column; - gap: 10px; -} - -[data-slot="todo-preview-spacer"] { - flex: 1 1 auto; - min-height: 0; -} - -[data-slot="todo-preview-msg"] { - border-radius: 8px; - border: 1px solid var(--border-weak-base); - background: var(--surface-base); - color: var(--text-weak); - padding: 8px 10px; - font-size: 13px; - line-height: 1.35; -} - -[data-slot="todo-preview-msg"][data-strong="true"] { - color: var(--text-strong); -} -` - -export const Playground = { - render: () => { - const global = useServerSync() - const prompt = usePrompt() - const [cfg, setCfg] = createStore({ - open: true, - collapsed: false, - step: 1, - dockOpenDuration: 0.3, - dockOpenBounce: 0, - dockCloseDuration: 0.3, - dockCloseBounce: 0, - drawerExpandDuration: 0.3, - drawerExpandBounce: 0, - drawerCollapseDuration: 0.3, - drawerCollapseBounce: 0, - subtitleDuration: 600, - subtitleAuto: true, - subtitleTravel: 25, - subtitleEdge: 17, - countDuration: 600, - countMask: 18, - countMaskHeight: 0, - countWidthDuration: 560, - }) - const open = () => cfg.open - const step = () => cfg.step - const dockOpenDuration = () => cfg.dockOpenDuration - const dockOpenBounce = () => cfg.dockOpenBounce - const dockCloseDuration = () => cfg.dockCloseDuration - const dockCloseBounce = () => cfg.dockCloseBounce - const drawerExpandDuration = () => cfg.drawerExpandDuration - const drawerExpandBounce = () => cfg.drawerExpandBounce - const drawerCollapseDuration = () => cfg.drawerCollapseDuration - const drawerCollapseBounce = () => cfg.drawerCollapseBounce - const subtitleDuration = () => cfg.subtitleDuration - const subtitleAuto = () => cfg.subtitleAuto - const subtitleTravel = () => cfg.subtitleTravel - const subtitleEdge = () => cfg.subtitleEdge - const countDuration = () => cfg.countDuration - const countMask = () => cfg.countMask - const countMaskHeight = () => cfg.countMaskHeight - const countWidthDuration = () => cfg.countWidthDuration - const state = createSessionComposerController({ closeMs: () => Math.round(dockCloseDuration() * 1000) }) - let frame - let scrollRef - - const todos = createMemo(() => { - const done = Math.max(0, Math.min(3, step())) - return pool.slice(0, 3).map((content, i) => ({ - id: `todo-${i + 1}`, - content, - status: i < done ? "completed" : i === done && done < 3 ? "in_progress" : "pending", - })) - }) - - createEffect(() => { - global.todo.set("story-session", todos()) - }) - - const clear = () => { - if (frame) cancelAnimationFrame(frame) - frame = undefined - } - - const pin = () => { - if (!scrollRef) return - scrollRef.scrollTop = scrollRef.scrollHeight - } - - const collapsed = () => cfg.collapsed - const setCollapsed = (value: boolean) => setCfg("collapsed", value) - const openDock = () => { - clear() - setCfg("open", true) - frame = requestAnimationFrame(() => { - pin() - frame = undefined - }) - } - - const closeDock = () => { - clear() - setCfg("open", false) - } - - const dockOpen = () => open() - - const toggleDock = () => { - if (dockOpen()) { - closeDock() - return - } - openDock() - } - - const toggleDrawer = () => { - if (!dockOpen()) { - openDock() - frame = requestAnimationFrame(() => { - pin() - setCollapsed(true) - frame = undefined - }) - return - } - setCollapsed(!collapsed()) - } - - const cycle = () => { - setCfg("step", (value) => (value + 1) % 4) - } - - onCleanup(clear) - - return ( -
- - -
-
-
-
-
-
-
-
- Thinking Checking type safety -
-
Shell Prints five topic blocks between timed commands
-
-
- -
- "story-session", - sessionID: () => "story-session", - prompt, - ready: () => true, - centered: () => false, - todo: { collapsed, onToggle: () => setCollapsed(!collapsed()) }, - followup: () => undefined, - revert: () => undefined, - onResponseSubmit: pin, - openParent: () => {}, - setPromptRef: () => {}, - setDockRef: () => {}, - })} - promptInput={ - {}, handleSubmit: (event) => event.preventDefault() }} - ref={() => {}} - newSessionWorktree="" - onNewSessionWorktreeReset={() => {}} - /> - } - /> -
-
-
-
-
- -
- - - - {[0, 1, 2, 3].map((value) => ( - - ))} -
- -
-
Dock open
- - - -
- Dock close -
- - - -
- Drawer expand -
- - - -
- Drawer collapse -
- - - -
- Subtitle odometer -
- - - - - -
- Count odometer -
- - - - -
-
- ) - }, -} diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx index b67b810a4b..044ebda7ae 100644 --- a/packages/app/src/pages/session/file-tabs.tsx +++ b/packages/app/src/pages/session/file-tabs.tsx @@ -24,12 +24,6 @@ import { getSessionHandoff } from "@/pages/session/handoff" import { useSessionLayout } from "@/pages/session/session-layout" import { createSessionTabs } from "@/pages/session/helpers" -type SessionFileViewProps = { - tab: string -} - -const selectionSide = (range: SelectedLineRange) => range.endSide ?? range.side ?? "additions" - function FileCommentMenu(props: { moreLabel: string editLabel: string @@ -213,9 +207,8 @@ export function FileTabContent(props: { tab: string }) { ) } -export function SessionFileView(props: SessionFileViewProps) { +export function SessionFileView(props: { tab: string }) { const settings = useSettings() - return ( }> @@ -555,10 +548,10 @@ function SessionFileViewV2(props: { tab: string }) { }) } - const buildPreview = (filePath: string, lines: SelectedLineRange) => { + const buildPreview = (filePath: string, selection: FileSelection) => { const source = filePath === path() ? contents() : file.get(filePath)?.content?.content if (!source) return undefined - return selectionPreview(source, selectionFromLines(lines)) + return selectionPreview(source, selection) } const addCommentToContext = (input: { @@ -569,7 +562,7 @@ function SessionFileViewV2(props: { tab: string }) { origin?: "review" | "file" }) => { const selection = selectionFromLines(input.selection) - const preview = input.preview ?? buildPreview(input.file, input.selection) + const preview = input.preview ?? buildPreview(input.file, selection) const saved = comments.add({ file: input.file, @@ -594,7 +587,7 @@ function SessionFileViewV2(props: { tab: string }) { comment: string }) => { comments.update(input.file, input.id, input.comment) - const preview = input.file === path() ? buildPreview(input.file, input.selection) : undefined + const preview = input.file === path() ? buildPreview(input.file, selectionFromLines(input.selection)) : undefined prompt.context.updateComment(input.file, input.id, { comment: input.comment, ...(preview ? { preview } : {}), @@ -635,7 +628,7 @@ function SessionFileViewV2(props: { tab: string }) { mention: { items: file.searchFilesAndDirectories, }, - getSide: selectionSide, + getSide: (range) => range.endSide ?? range.side ?? "additions", state: { opened: () => note.openedComment, setOpened: (id) => setNote("openedComment", id), @@ -764,7 +757,7 @@ function SessionFileViewV2(props: { tab: string }) { commentsUi.onLineNumberSelectionEnd(range) }} search={search} - class="select-text" + class="select-text [--opencode-diffs-bg:var(--v2-background-bg-base)]" media={{ mode: "auto", path: path(), diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx index 1b65af7121..586942399d 100644 --- a/packages/app/src/pages/session/review-tab.tsx +++ b/packages/app/src/pages/session/review-tab.tsx @@ -1,7 +1,6 @@ import { createEffect, onCleanup, type JSX } from "solid-js" import { makeEventListener } from "@solid-primitives/event-listener" -import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" -import type { FileDiffInfo } from "@opencode-ai/client/promise" +import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2" import { SessionReview } from "@opencode-ai/session-ui/session-review" import type { SessionReviewCommentActions, @@ -15,7 +14,7 @@ import type { LineComment } from "@/context/comments" export type DiffStyle = "unified" | "split" -type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff +type ReviewDiff = FileDiffInfo | VcsFileDiff export interface SessionReviewTabProps { title?: JSX.Element diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 6f074fa203..7c84ca7078 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -23,8 +23,7 @@ import { Mark } from "@opencode-ai/ui/logo" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" -import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" -import type { FileDiffInfo } from "@opencode-ai/client/promise" +import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2" import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" import { useDialog } from "@opencode-ai/ui/context/dialog" @@ -57,17 +56,9 @@ import { setSessionHandoff } from "@/pages/session/handoff" import { useSessionLayout } from "@/pages/session/session-layout" import { SessionFileBrowserTab, type SessionFileBrowserState } from "@/pages/session/v2/session-file-browser-tab" -type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff -type RenderDiff = FileDiffInfo | (SnapshotFileDiff & { file: string }) | VcsFileDiff -const FILE_TREE_WIDTH_MIN = 240 - -function renderDiff(value: ReviewDiff): value is RenderDiff { - return typeof value.file === "string" -} - export function SessionSidePanel(props: { canReview: () => boolean - diffs: () => ReviewDiff[] + diffs: () => (FileDiffInfo | VcsFileDiff)[] diffsReady: () => boolean empty: () => string hasReview: () => boolean @@ -105,16 +96,15 @@ export function SessionSidePanel(props: { }), ) const open = createMemo(() => reviewOpen() || fileOpen()) - const fileTreeWidth = createMemo(() => Math.max(FILE_TREE_WIDTH_MIN, layout.fileTree.width())) const reviewTab = createMemo(() => isDesktop()) const panelWidth = createMemo(() => { if (!open()) return "0px" if (reviewOpen()) return "auto" - return `${fileTreeWidth()}px` + return `${layout.fileTree.width()}px` }) - const treeWidth = createMemo(() => (fileOpen() ? `${fileTreeWidth()}px` : "0px")) + const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px")) - const diffs = createMemo(() => props.diffs().filter(renderDiff)) + const diffs = createMemo(() => props.diffs()) const diffFiles = createMemo(() => diffs().map((d) => d.file)) const kinds = createMemo(() => { const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => { @@ -241,7 +231,6 @@ export function SessionSidePanel(props: { return active !== "review" && active !== "context" && active !== "empty" }) const openFileKeybind = createMemo(() => command.keybindParts("file.open")) - const closeTabKeybind = createMemo(() => command.keybindParts("tab.close")) const [store, setStore] = createStore({ activeDraggable: undefined as string | undefined, }) @@ -555,7 +544,7 @@ export function SessionSidePanel(props: { > {(toggle) => ( -
+
{toggle()(activeTab() === SESSION_OPEN_FILE_TAB)}
)} @@ -575,15 +564,9 @@ export function SessionSidePanel(props: { - {language.t("common.closeTab")} - 0}> - - - - } + @@ -594,7 +577,7 @@ export function SessionSidePanel(props: { onClick={() => tabs().close("context")} aria-label={language.t("common.closeTab")} /> - + } hideCloseButton onMiddleClick={() => tabs().close("context")} @@ -622,15 +605,9 @@ export function SessionSidePanel(props: { - {language.t("common.closeTab")} - 0}> - - - - } + @@ -641,7 +618,7 @@ export function SessionSidePanel(props: { onClick={() => tabs().close(SESSION_OPEN_FILE_TAB)} aria-label={language.t("common.closeTab")} /> - + } hideCloseButton onMiddleClick={() => tabs().close(SESSION_OPEN_FILE_TAB)} @@ -847,8 +824,8 @@ export function SessionSidePanel(props: { { props.size.touch() diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx index eca1541cea..e7f9feba44 100644 --- a/packages/app/src/pages/session/timeline/message-timeline.tsx +++ b/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -283,14 +283,6 @@ export function MessageTimeline(props: { return sync().data.session_status[id] ?? idle }) const sessionMessages = createMemo(() => (sessionID() ? (sync().data.message[sessionID()!] ?? []) : [])) - const projectedMessages = createMemo(() => { - const id = sessionID() - if (!id) return [] - const visible = new Set(props.userMessages.map((message) => message.id)) - const boundary = sessionMessages().find((message) => message.role === "user" && !visible.has(message.id))?.id - const messages = sync().data.session_message[id] ?? [] - return boundary ? messages.filter((message) => message.id < boundary) : messages - }) const info = createMemo(() => { const id = sessionID() if (!id) return @@ -333,7 +325,6 @@ export function MessageTimeline(props: { const projection = createTimelineProjection({ messages: sessionMessages, userMessages: () => props.userMessages, - sessionMessages: projectedMessages, parts: getMsgParts, status: sessionStatus, showReasoningSummaries: settings.general.showReasoningSummaries, @@ -646,7 +637,7 @@ export function MessageTimeline(props: { const viewShare = () => { const url = shareUrl() if (!url) return - platform.openExternal(url) + platform.openLink(url) } const errorMessage = (err: unknown) => { @@ -674,7 +665,7 @@ export function MessageTimeline(props: { const titleMutation = useMutation(() => ({ mutationFn: (input: { id: string; title: string }) => - sdk().api.session.rename({ sessionID: input.id, title: input.title }), + sdk().client.session.update({ sessionID: input.id, title: input.title }), onSuccess: (_, input) => { sync().set( produce((draft) => { @@ -812,14 +803,13 @@ export function MessageTimeline(props: { const archiveSession = async (sessionID: string) => { const session = sync().session.get(sessionID) if (!session) return - if ((await sdk().protocol) !== "v1") return const sessions = sync().data.session ?? [] const index = sessions.findIndex((s) => s.id === sessionID) const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) await sdk() - .client.session.update({ sessionID, directory: sdk().directory, time: { archived: Date.now() } }) + .client.session.update({ sessionID, time: { archived: Date.now() } }) .then(() => { sync().set( produce((draft) => { @@ -848,8 +838,8 @@ export function MessageTimeline(props: { const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) const result = await sdk() - .api.session.remove({ sessionID }) - .then(() => true) + .client.session.delete({ sessionID }) + .then((x) => x.data) .catch((err) => { showToast({ title: language.t("session.delete.failed.title"), @@ -1267,7 +1257,7 @@ export function MessageTimeline(props: { const part = getMsgPart(value.group.ref.messageID, value.group.ref.partID) if (part?.type === "tool") return part } - const asyncFile = () => ["edit", "write", "apply_patch"].includes(tool()?.tool ?? "") + const asyncFile = () => ["edit", "write", "patch", "apply_patch"].includes(tool()?.tool ?? "") const [ready, setReady] = createSignal(initialItem.size <= timelineFallbackItemSize || !asyncFile()) let contentMeasureFrame: number | undefined diff --git a/packages/app/src/pages/session/timeline/projection.ts b/packages/app/src/pages/session/timeline/projection.ts index e30c936d73..ea8ea4f132 100644 --- a/packages/app/src/pages/session/timeline/projection.ts +++ b/packages/app/src/pages/session/timeline/projection.ts @@ -1,15 +1,16 @@ -import type { SessionMessageInfo } from "@opencode-ai/client/promise" +import { Binary } from "@opencode-ai/core/util/binary" import type { AssistantMessage, Message, Part, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2" -import { createMemo, type Accessor } from "solid-js" +import { createMemo, mapArray, type Accessor } from "solid-js" import { reuseTimelineRows } from "./row-reconciliation" import { Timeline, TimelineRow } from "./rows" export { reuseTimelineRows } from "./row-reconciliation" +const emptyAssistantMessages: AssistantMessage[] = [] + export function createTimelineProjection(input: { messages: Accessor userMessages: Accessor - sessionMessages: Accessor parts: (messageID: string) => Part[] status: Accessor showReasoningSummaries: Accessor @@ -29,20 +30,47 @@ export function createTimelineProjection(input: { }) return result }) - const projection = createMemo(() => - Timeline.constructSessionMessageRows( - input.sessionMessages(), - (messageID) => messageByID().get(messageID) as UserMessage | AssistantMessage | undefined, - input.parts, - input.showReasoningSummaries(), - input.status().type, - input.inlineComments(), - input.userMessages(), + const activeMessageID = createMemo(() => { + const parentID = input + .messages() + .findLast( + (message): message is AssistantMessage => + message.role === "assistant" && typeof message.time.completed !== "number", + )?.parentID + if (parentID) { + const messages = input.messages() + const result = Binary.search(messages, parentID, (message) => message.id) + const message = result.found ? messages[result.index] : messages.find((item) => item.id === parentID) + if (message?.role === "user") return message.id + } + + if (input.status().type === "idle") return + return input.messages().findLast((message) => message.role === "user")?.id + }) + const messageRowMemos = createMemo( + mapArray(input.userMessages, (userMessage, indexAccessor) => + createMemo((previous: TimelineRow.TimelineRow[] | undefined) => + reuseTimelineRows( + previous, + Timeline.constructMessageRows( + userMessage, + input.parts, + assistantMessagesByParent().get(userMessage.id) ?? emptyAssistantMessages, + indexAccessor(), + input.showReasoningSummaries(), + input.status().type, + activeMessageID() === userMessage.id, + input.inlineComments(), + ), + ), + ), ), ) - const activeMessageID = createMemo(() => projection().activeMessageID) const rows = createMemo((previous: TimelineRow.TimelineRow[] | undefined) => - reuseTimelineRows(previous, projection().rows), + reuseTimelineRows( + previous, + messageRowMemos().flatMap((memo) => memo()), + ), ) const rowByKey = createMemo(() => new Map(rows().map((row) => [TimelineRow.key(row), row] as const))) const messageRowIndex = createMemo(() => { diff --git a/packages/app/src/pages/session/timeline/rows-current.test.ts b/packages/app/src/pages/session/timeline/rows-current.test.ts deleted file mode 100644 index a5952a5e66..0000000000 --- a/packages/app/src/pages/session/timeline/rows-current.test.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { describe, expect, mock, test } from "bun:test" -import type { SessionMessageInfo } from "@opencode-ai/client/promise" -import { normalizeSessionMessages } from "@/utils/session-message" - -mock.module("@opencode-ai/session-ui/message-part", () => ({ - renderable: () => true, - groupParts: (refs: Array<{ messageID: string; part: { id: string } }>) => - refs.map((ref) => ({ - type: "part" as const, - key: ref.part.id, - ref: { messageID: ref.messageID, partID: ref.part.id }, - })), -})) - -const { Timeline, TimelineRow } = await import("./rows") - -describe("current session timeline rows", () => { - test("derives turns and tagged rows from chronological current messages", () => { - const source = [ - { id: "msg_1", type: "user", text: "first", time: { created: 1 } }, - { - id: "msg_2", - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [{ type: "text", text: "answer" }], - time: { created: 2, completed: 3 }, - }, - { id: "msg_3", type: "user", text: "second", time: { created: 4 } }, - { - id: "msg_4", - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [{ type: "reasoning", text: "working" }], - time: { created: 5 }, - }, - ] satisfies SessionMessageInfo[] - const normalized = normalizeSessionMessages("ses_1", source) - const messages = new Map(normalized.messages.map((message) => [message.id, message])) - - const result = Timeline.constructSessionMessageRows( - source, - (messageID) => messages.get(messageID), - (messageID) => normalized.parts.get(messageID) ?? [], - true, - "busy", - true, - normalized.messages.filter((message) => message.role === "user"), - ) - - expect(result.activeMessageID).toBe("msg_3") - expect(result.rows.map(TimelineRow.key)).toEqual([ - "user-message:msg_1", - "assistant-part:msg_1:msg_2:text:0", - "turn-gap:msg_3", - "user-message:msg_3", - "assistant-part:msg_3:msg_4:reasoning:0", - ]) - }) - - test("renders a current shell message as a standalone turn", () => { - const source = [ - { - id: "msg_shell", - type: "shell", - shellID: "shell_1", - command: "pwd", - status: "exited", - exit: 0, - output: { output: "/repo", cursor: 5, size: 5, truncated: false }, - time: { created: 1, completed: 2 }, - }, - ] satisfies SessionMessageInfo[] - const normalized = normalizeSessionMessages("ses_1", source) - const messages = new Map(normalized.messages.map((message) => [message.id, message])) - - const result = Timeline.constructSessionMessageRows( - source, - (messageID) => messages.get(messageID), - (messageID) => normalized.parts.get(messageID) ?? [], - true, - "idle", - true, - normalized.messages.filter((message) => message.role === "user"), - ) - - expect(result.activeMessageID).toBe("msg_shell") - expect(result.rows.map(TimelineRow.key)).toEqual([ - "user-message:msg_shell", - "assistant-part:msg_shell:msg_shell:tool", - ]) - }) - - test("keeps a projected parent missing from the source page before newer turns", () => { - const source = [ - { id: "msg_user_1", type: "user", text: "first question", time: { created: 1 } }, - { - id: "msg_assistant_1", - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [{ type: "text", text: "first answer" }], - time: { created: 2, completed: 3 }, - }, - { id: "msg_user_2", type: "user", text: "second question", time: { created: 4 } }, - { - id: "msg_assistant_2", - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [{ type: "text", text: "second answer" }], - time: { created: 5, completed: 6 }, - }, - ] satisfies SessionMessageInfo[] - const normalized = normalizeSessionMessages("ses_1", source) - const messages = new Map(normalized.messages.map((message) => [message.id, message])) - - const result = Timeline.constructSessionMessageRows( - source.slice(1), - (messageID) => messages.get(messageID), - (messageID) => normalized.parts.get(messageID) ?? [], - true, - "idle", - true, - normalized.messages.filter((message) => message.role === "user"), - ) - - expect(result.rows.map(TimelineRow.key)).toEqual([ - "user-message:msg_user_1", - "assistant-part:msg_user_1:msg_assistant_1:text:0", - "turn-gap:msg_user_2", - "user-message:msg_user_2", - "assistant-part:msg_user_2:msg_assistant_2:text:0", - ]) - }) - - test("renders an optimistic user turn and thinking before the protocol message arrives", () => { - const source = [ - { id: "msg_1", type: "user", text: "existing", time: { created: 1 } }, - ] satisfies SessionMessageInfo[] - const normalized = normalizeSessionMessages("ses_1", source) - const optimistic = { - id: "msg_2", - sessionID: "ses_1", - role: "user" as const, - time: { created: 2 }, - agent: "build", - model: { modelID: "model", providerID: "provider" }, - } - const result = Timeline.constructSessionMessageRows( - source, - (messageID) => - messageID === optimistic.id ? optimistic : normalized.messages.find((message) => message.id === messageID), - () => [], - true, - "busy", - true, - [...normalized.messages.filter((message) => message.role === "user"), optimistic], - ) - - expect(result.activeMessageID).toBe(optimistic.id) - expect(result.rows.map(TimelineRow.key)).toEqual([ - "user-message:msg_1", - "turn-gap:msg_2", - "user-message:msg_2", - "thinking:msg_2", - ]) - }) - - test("removes a failed assistant error when the turn continues streaming", () => { - const source = [ - { id: "msg_user", type: "user", text: "recover", time: { created: 1 } }, - { - id: "msg_failed", - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [], - error: { type: "ProviderError", message: "temporary failure" }, - time: { created: 2, completed: 3 }, - }, - { - id: "msg_recovery", - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [{ type: "text", text: "streaming again" }], - time: { created: 4 }, - }, - ] satisfies SessionMessageInfo[] - const normalized = normalizeSessionMessages("ses_1", source) - const messages = new Map(normalized.messages.map((message) => [message.id, message])) - - const result = Timeline.constructSessionMessageRows( - source, - (messageID) => messages.get(messageID), - (messageID) => normalized.parts.get(messageID) ?? [], - true, - "busy", - true, - normalized.messages.filter((message) => message.role === "user"), - ) - - expect(result.rows.map((row) => row._tag)).toEqual(["UserMessage", "AssistantPart"]) - }) -}) diff --git a/packages/app/src/pages/session/timeline/rows.ts b/packages/app/src/pages/session/timeline/rows.ts index 879646e86a..72cd9a180c 100644 --- a/packages/app/src/pages/session/timeline/rows.ts +++ b/packages/app/src/pages/session/timeline/rows.ts @@ -1,9 +1,7 @@ import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note" -import type { SessionMessageInfo } from "@opencode-ai/client/promise" -import { AssistantMessage, Part, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2" +import { AssistantMessage, Part, SessionStatus, SnapshotFileDiff, UserMessage } from "@opencode-ai/sdk/v2" import { groupParts, renderable, type PartGroup } from "@opencode-ai/session-ui/message-part" import { TimelineRow, type SummaryDiff } from "./timeline-row" -import { uniqueSummaryDiffs } from "./summary-diffs" export { TimelineRow, type SummaryDiff } from "./timeline-row" @@ -32,71 +30,6 @@ export type TimelineRowMap = { } export namespace Timeline { - export function constructSessionMessageRows( - messages: SessionMessageInfo[], - getMessage: (messageID: string) => UserMessage | AssistantMessage | undefined, - getMessageParts: (messageID: string) => Part[], - showReasoning: boolean, - status: SessionStatus["type"], - inlineComments: boolean, - projectedUserMessages: UserMessage[], - ) { - const turns: { user: UserMessage; assistants: AssistantMessage[] }[] = [] - const turnByUserID = new Map() - messages.forEach((message) => { - const projected = getMessage(message.id) - if (message.type === "shell" && projected?.role === "user") { - const assistant = getMessage(`${message.id}:assistant`) - const turn = { user: projected, assistants: assistant?.role === "assistant" ? [assistant] : [] } - turns.push(turn) - turnByUserID.set(projected.id, turn) - return - } - if (projected?.role === "user") { - if (turnByUserID.has(projected.id)) return - const turn = { user: projected, assistants: [] } - turns.push(turn) - turnByUserID.set(projected.id, turn) - return - } - if (projected?.role !== "assistant") return - const existing = turnByUserID.get(projected.parentID) - if (existing) { - existing.assistants.push(projected) - return - } - const user = getMessage(projected.parentID) - if (user?.role !== "user") return - const turn = { user, assistants: [projected] } - turns.push(turn) - turnByUserID.set(user.id, turn) - }) - const latestUserMessageID = turns.at(-1)?.user.id - projectedUserMessages.forEach((user) => { - if (turnByUserID.has(user.id)) return - if (latestUserMessageID && user.id < latestUserMessageID) return - const turn = { user, assistants: [] } - turns.push(turn) - turnByUserID.set(user.id, turn) - }) - const activeMessageID = turns.at(-1)?.user.id - return { - activeMessageID, - rows: turns.flatMap((turn, index) => - constructMessageRows( - turn.user, - getMessageParts, - turn.assistants, - index, - showReasoning, - status, - turn.user.id === activeMessageID, - inlineComments, - ), - ), - } - } - export function constructMessageRows( userMessage: UserMessage, getMessageParts: (messageID: string) => Part[], @@ -116,8 +49,7 @@ export namespace Timeline { const compaction = userParts.some((p) => p.type === "compaction") const interruptedMessageIndex = assistantMessages.findIndex((m) => m.error?.name === "MessageAbortedError") const interrupted = interruptedMessageIndex !== -1 - const latestError = assistantMessages.at(-1)?.error - const error = latestError?.name === "MessageAbortedError" ? undefined : latestError + const error = assistantMessages.find((m) => m.error && m.error.name !== "MessageAbortedError")?.error const assistantPartRefs = assistantMessages.flatMap((message, messageIndex) => getMessageParts(message.id) @@ -205,7 +137,14 @@ export namespace Timeline { if (isActive && status === "retry") rows.push(new TimelineRow.Retry({ userMessageID: userMessage.id })) - const diffs = uniqueSummaryDiffs(userMessage.summary?.diffs) + const diffs = (userMessage.summary?.diffs ?? []) + .reduceRight((result, diff) => { + if (!isSummaryDiff(diff)) return result + if (result.some((item) => item.file === diff.file)) return result + result.push(diff) + return result + }, []) + .reverse() if (diffs.length > 0 && (status === "idle" || !isActive)) { rows.push( new TimelineRow.DiffSummary({ @@ -230,6 +169,10 @@ export namespace Timeline { return rows } + function isSummaryDiff(value: SnapshotFileDiff): value is SummaryDiff { + return typeof value.file === "string" + } + function reasoningHeading(text: string) { const markdown = text.replace(/\r\n?/g, "\n") const html = markdown.match(/]*>([\s\S]*?)<\/h[1-6]>/i) diff --git a/packages/app/src/pages/session/timeline/summary-diffs.test.ts b/packages/app/src/pages/session/timeline/summary-diffs.test.ts deleted file mode 100644 index 9b66bf6771..0000000000 --- a/packages/app/src/pages/session/timeline/summary-diffs.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2" -import { uniqueSummaryDiffs } from "./summary-diffs" - -const diff = (file: string, additions: number) => - ({ - file, - additions, - deletions: 0, - }) satisfies SnapshotFileDiff - -describe("uniqueSummaryDiffs", () => { - test("drops entries without files and preserves unique input", () => { - const alpha = diff("alpha.ts", 1) - const beta = diff("beta.ts", 1) - const invalid = { additions: 1, deletions: 0 } satisfies SnapshotFileDiff - - expect(uniqueSummaryDiffs(undefined)).toEqual([]) - expect(uniqueSummaryDiffs([])).toEqual([]) - expect(uniqueSummaryDiffs([invalid])).toEqual([]) - - const result = uniqueSummaryDiffs([alpha, invalid, beta]) - expect(result).toEqual([alpha, beta]) - expect(result[0]).toBe(alpha) - expect(result[1]).toBe(beta) - }) - - test("keeps the last diff per file in the legacy display order", () => { - const oldAlpha = diff("alpha.ts", 1) - const oldBeta = diff("beta.ts", 1) - const newAlpha = diff("alpha.ts", 2) - const charlie = diff("charlie.ts", 1) - const newBeta = diff("beta.ts", 2) - - const result = uniqueSummaryDiffs([oldAlpha, oldBeta, newAlpha, charlie, newBeta]) - - expect(result).toEqual([newAlpha, charlie, newBeta]) - expect(result[0]).toBe(newAlpha) - expect(result[1]).toBe(charlie) - expect(result[2]).toBe(newBeta) - }) -}) diff --git a/packages/app/src/pages/session/timeline/summary-diffs.ts b/packages/app/src/pages/session/timeline/summary-diffs.ts deleted file mode 100644 index 2df37af5a6..0000000000 --- a/packages/app/src/pages/session/timeline/summary-diffs.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2" -import type { SummaryDiff } from "./timeline-row" - -export function uniqueSummaryDiffs(diffs: SnapshotFileDiff[] | undefined) { - const files = new Set() - return (diffs ?? []) - .reduceRight((result, diff) => { - if (!isSummaryDiff(diff)) return result - const file = diff.file - if (files.has(file)) return result - files.add(file) - result.push(diff) - return result - }, []) - .reverse() -} - -function isSummaryDiff(diff: SnapshotFileDiff): diff is SummaryDiff { - return typeof diff.file === "string" -} diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index 12dd96a5e6..275e6ec4bc 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -5,6 +5,7 @@ import { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-b import { useFile, selectionFromLines, type FileSelection, type SelectedLineRange } from "@/context/file" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" +import { useLocal } from "@/context/local" import { usePermission } from "@/context/permission" import { usePrompt } from "@/context/prompt" import { useSDK } from "@/context/sdk" @@ -18,7 +19,6 @@ import { extractPromptFromParts } from "@/utils/prompt" import { UserMessage } from "@opencode-ai/sdk/v2" import { useSessionLayout } from "@/pages/session/session-layout" import { createSessionOwnership } from "./session-ownership" -import { useLocal } from "@/context/local" export type SessionCommandContext = { navigateMessageByOffset: (offset: number) => void @@ -40,6 +40,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const dialog = useDialog() const file = useFile() const language = useLanguage() + const local = useLocal() const permission = usePermission() const prompt = usePrompt() const sdk = useSDK() @@ -47,7 +48,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const sync = useSync() const terminal = useTerminal() const layout = useLayout() - const local = useLocal() const navigate = useNavigate() const { params, sessionKey, tabs, view } = useSessionLayout() const sessionOwnership = createSessionOwnership(sessionKey) @@ -306,7 +306,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const sessionID = params.id if (!sessionID) return const owner = sessionOwnership.capture() - const session = sdk().api.session + const client = sdk().client const directory = sdk().directory const promptSession = prompt.capture() const revert = info()?.revert?.messageID @@ -316,13 +316,13 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const parts = sync().data.part[message.id] if (sync().data.session_working(sessionID)) { - await session.interrupt({ sessionID }).catch(() => {}) + await client.session.abort({ sessionID }).catch(() => {}) } await runCommand({ owner, prompt: promptSession, - request: () => session.revert.stage({ sessionID, messageID: message.id }), + request: () => client.session.revert({ sessionID, messageID: message.id }), updatePrompt: (promptSession) => { if (parts) promptSession.set(extractPromptFromParts(parts, { directory })) }, @@ -334,7 +334,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const sessionID = params.id if (!sessionID) return const owner = sessionOwnership.capture() - const session = sdk().api.session + const client = sdk().client const messages = userMessages() const promptSession = prompt.capture() @@ -346,7 +346,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { await runCommand({ owner, prompt: promptSession, - request: () => session.revert.clear({ sessionID }), + request: () => client.session.unrevert({ sessionID }), updatePrompt: (promptSession) => promptSession.reset(), updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id >= revertMessageID)), }) @@ -356,7 +356,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { await runCommand({ owner, prompt: promptSession, - request: () => session.revert.stage({ sessionID, messageID: next.id }), + request: () => client.session.revert({ sessionID, messageID: next.id }), updatePrompt: () => undefined, updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id < next.id)), }) @@ -375,9 +375,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => { return } - await sdk().api.session.compact({ + await sdk().client.session.summarize({ sessionID, - model: { providerID: model.provider.id, modelID: model.id }, + modelID: model.id, + providerID: model.provider.id, }) } diff --git a/packages/app/src/pages/session/v2/review-diff-kinds.ts b/packages/app/src/pages/session/v2/review-diff-kinds.ts index d3adb1f2ff..8b252b0258 100644 --- a/packages/app/src/pages/session/v2/review-diff-kinds.ts +++ b/packages/app/src/pages/session/v2/review-diff-kinds.ts @@ -1,18 +1,13 @@ -import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" -import type { FileDiffInfo } from "@opencode-ai/client/promise" +import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2" import type { Kind } from "@/components/file-tree-v2" import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model" -export type RenderDiff = FileDiffInfo | (SnapshotFileDiff & { file: string }) | VcsFileDiff +export type RenderDiff = FileDiffInfo | VcsFileDiff export function normalizePath(p: string) { return normalizeFileTreeV2Path(p) } -export function filterRenderableDiff(value: FileDiffInfo | SnapshotFileDiff | VcsFileDiff): value is RenderDiff { - return typeof value.file === "string" -} - export function reviewDiffNeedsLoad(diff: RenderDiff) { if (diff.additions === 0 && diff.deletions === 0) return false return !diff.patch || !/^@@ /m.test(diff.patch) diff --git a/packages/app/src/pages/session/v2/review-panel-v2.tsx b/packages/app/src/pages/session/v2/review-panel-v2.tsx index fcd6bbb79f..bcf53c1d70 100644 --- a/packages/app/src/pages/session/v2/review-panel-v2.tsx +++ b/packages/app/src/pages/session/v2/review-panel-v2.tsx @@ -1,6 +1,5 @@ import { createMemo, createResource, createSignal, Show, type JSX } from "solid-js" -import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" -import type { FileDiffInfo } from "@opencode-ai/client/promise" +import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2" import { SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX, SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN, @@ -22,7 +21,6 @@ import FileTreeV2 from "@/components/file-tree-v2" import { useLanguage } from "@/context/language" import { useSDK } from "@/context/sdk" import { - filterRenderableDiff, filterReviewFiles, reviewDiffKinds, reviewDiffNeedsLoad, @@ -31,7 +29,7 @@ import { import type { ReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state" import { applyFileListKeyDown, SessionFileListV2 } from "@/pages/session/v2/session-file-list-v2" -type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff +type ReviewDiff = FileDiffInfo | VcsFileDiff export type ReviewPanelV2Props = { title?: JSX.Element @@ -57,7 +55,7 @@ export type ReviewPanelV2Props = { export function ReviewPanelV2(props: ReviewPanelV2Props) { const sdk = useSDK() - const diffs = createMemo(() => props.diffs().filter(filterRenderableDiff)) + const diffs = createMemo(() => props.diffs()) const filteredFiles = createMemo(() => filterReviewFiles( diffs().map((diff) => diff.file), diff --git a/packages/app/src/utils/diffs.test.ts b/packages/app/src/utils/diffs.test.ts index a3d25f4279..f6d768e1de 100644 --- a/packages/app/src/utils/diffs.test.ts +++ b/packages/app/src/utils/diffs.test.ts @@ -1,6 +1,5 @@ import { describe, expect, test } from "bun:test" -import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2" -import type { FileDiffInfo } from "@opencode-ai/client/promise" +import type { FileDiffInfo } from "@opencode-ai/sdk/v2" import type { Message } from "@opencode-ai/sdk/v2/client" import { diffs, message } from "./diffs" @@ -10,7 +9,7 @@ const item = { additions: 1, deletions: 1, status: "modified", -} satisfies FileDiffInfo & SnapshotFileDiff +} satisfies FileDiffInfo describe("diffs", () => { test("keeps valid arrays", () => { diff --git a/packages/app/src/utils/diffs.ts b/packages/app/src/utils/diffs.ts index a8eec75a9a..60df039410 100644 --- a/packages/app/src/utils/diffs.ts +++ b/packages/app/src/utils/diffs.ts @@ -1,8 +1,7 @@ -import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" -import type { FileDiffInfo } from "@opencode-ai/client/promise" +import type { FileDiffInfo } from "@opencode-ai/sdk/v2" import type { Message } from "@opencode-ai/sdk/v2/client" -type Diff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff +type Diff = FileDiffInfo function diff(value: unknown): value is Diff { if (!value || typeof value !== "object" || Array.isArray(value)) return false diff --git a/packages/app/src/utils/draft-store.ts b/packages/app/src/utils/draft-store.ts deleted file mode 100644 index cd0895f52c..0000000000 --- a/packages/app/src/utils/draft-store.ts +++ /dev/null @@ -1,171 +0,0 @@ -import type { AsyncStorage } from "@solid-primitives/storage" - -export type BlobReference = { id: string; url: string } - -type Driver = { - get(key: string): Promise - set(key: string, value: string): Promise - remove(key: string): Promise - putBlob(blob: Blob): Promise - getBlob(id: string): Promise -} - -export type DraftStore = AsyncStorage & { putBlob(blob: Blob): Promise } -const urls = new Map() - -function blobUrl(id: string, blob: Blob) { - const existing = urls.get(id) - if (existing) return existing - const url = URL.createObjectURL(blob) - urls.set(id, url) - return url -} - -async function blobID(blob: Blob) { - const id = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", await blob.arrayBuffer()))) - .map((byte) => byte.toString(16).padStart(2, "0")) - .join("") - return id -} - -export async function createBlobReference(blob: Blob): Promise { - const id = await blobID(blob) - return { id, url: blobUrl(id, blob) } -} - -export function createDraftStore(driver: Driver): DraftStore { - const versions = new Map() - const putBlob = async (blob: Blob) => { - const id = await driver.putBlob(blob) - return { id, url: blobUrl(id, blob) } - } - const encode = async (value: unknown): Promise => { - if (Array.isArray(value)) return Promise.all(value.map(encode)) - if (!value || typeof value !== "object") return value - const item = value as Record - if (item.type === "image" && typeof item.dataUrl === "string") { - const blob = await fetch(item.dataUrl).then((response) => response.blob()) - const { dataUrl: _, ...rest } = item - return { ...rest, blob: { id: await driver.putBlob(blob) } } - } - if ("blob" in item && item.blob && typeof item.blob === "object") { - const blob = item.blob as Record - if (typeof blob.id === "string" && blob.id.startsWith("data:")) { - const data = await fetch(blob.id).then((response) => response.blob()) - return { ...item, blob: { id: await driver.putBlob(data) } } - } - return { ...item, blob: { id: blob.id } } - } - return Object.fromEntries( - await Promise.all(Object.entries(item).map(async ([key, entry]) => [key, await encode(entry)])), - ) - } - const decode = async (value: unknown): Promise => { - if (Array.isArray(value)) return Promise.all(value.map(decode)) - if (!value || typeof value !== "object") return value - const item = value as Record - if (item.blob && typeof item.blob === "object") { - const ref = item.blob as Record - if (typeof ref.id === "string") { - const blob = await driver.getBlob(ref.id) - if (blob) return { ...item, blob: { id: ref.id, url: blobUrl(ref.id, blob) } } - } - } - return Object.fromEntries( - await Promise.all(Object.entries(item).map(async ([key, entry]) => [key, await decode(entry)])), - ) - } - return { - getItem: async (key) => { - const value = await driver.get(key) - return value === null ? null : JSON.stringify(await decode(JSON.parse(value))) - }, - setItem: async (key, value) => { - const version = (versions.get(key) ?? 0) + 1 - versions.set(key, version) - const encoded = JSON.stringify(await encode(JSON.parse(value))) - if (versions.get(key) === version) await driver.set(key, encoded) - }, - removeItem: async (key) => { - versions.set(key, (versions.get(key) ?? 0) + 1) - await driver.remove(key) - }, - putBlob, - } -} - -export function createBrowserDraftStore(): DraftStore { - const request = indexedDB.open("opencode-drafts", 1) - request.addEventListener("upgradeneeded", () => { - request.result.createObjectStore("documents") - request.result.createObjectStore("blobs") - }) - const db = new Promise((resolve, reject) => { - request.addEventListener("success", () => { - const database = request.result - const transaction = database.transaction(["documents", "blobs"], "readwrite") - const documents = transaction.objectStore("documents").getAll() - documents.addEventListener("success", () => { - const used = new Set() - JSON.parse(`[${documents.result.join(",")}]`, (_key, item) => { - if (item?.blob && typeof item.blob.id === "string") used.add(item.blob.id) - return item - }) - const blobs = transaction.objectStore("blobs").openKeyCursor() - blobs.addEventListener("success", () => { - const cursor = blobs.result - if (!cursor) return - if (!used.has(String(cursor.key))) cursor.delete() - cursor.continue() - }) - }) - transaction.addEventListener("complete", () => resolve(database)) - transaction.addEventListener("abort", () => resolve(database)) - }) - request.addEventListener("error", () => reject(request.error)) - }) - const get = async (store: string, key: string) => { - const result = (await db).transaction(store).objectStore(store).get(key) - return new Promise((resolve, reject) => { - result.addEventListener("success", () => resolve(result.result)) - result.addEventListener("error", () => reject(result.error)) - }) - } - const write = async (store: string, key: string, value?: unknown) => { - const transaction = (await db).transaction(store, "readwrite") - if (value === undefined) transaction.objectStore(store).delete(key) - else transaction.objectStore(store).put(value, key) - return new Promise((resolve, reject) => { - transaction.addEventListener("complete", () => resolve()) - transaction.addEventListener("error", () => reject(transaction.error)) - }) - } - return createDraftStore({ - get: async (key) => ((await get("documents", key)) as string | undefined) ?? null, - set: (key, value) => write("documents", key, value), - remove: (key) => write("documents", key), - putBlob: async (blob) => { - const id = await blobID(blob) - await write("blobs", id, blob) - return id - }, - getBlob: async (id) => ((await get("blobs", id)) as Blob | undefined) ?? null, - }) -} - -export async function blobDataUrl(blob: BlobReference, mime: string) { - const data = await fetch(blob.url).then((response) => response.blob()) - return new Promise((resolve, reject) => { - const reader = new FileReader() - reader.addEventListener("error", () => reject(reader.error)) - reader.addEventListener("load", () => { - const value = typeof reader.result === "string" ? reader.result : "" - resolve(`data:${mime};base64,${value.slice(value.indexOf(",") + 1)}`) - }) - reader.readAsDataURL(data) - }) -} - -export function createLegacyBlobReference(dataUrl: string): BlobReference { - return { id: dataUrl, url: dataUrl } -} diff --git a/packages/app/src/utils/menu-dismiss-controller.ts b/packages/app/src/utils/menu-dismiss-controller.ts deleted file mode 100644 index 0a3009eb71..0000000000 --- a/packages/app/src/utils/menu-dismiss-controller.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** Coordinates focus restoration and actions that must run after menu content unmounts. */ -export function createMenuDismissController(content: () => HTMLElement | undefined) { - let restoreTrigger = true - - return { - /** Allows the menu primitive to restore focus to its trigger when closing. */ - allowTriggerRestore() { - restoreTrigger = true - }, - /** Keeps focus at its current or next destination instead of returning it to the trigger. */ - preventTriggerRestore() { - restoreTrigger = false - }, - /** Applies the current restoration policy during the menu primitive's close-focus event. */ - onCloseAutoFocus(event: Event) { - if (!restoreTrigger) event.preventDefault() - }, - /** Runs an action after the menu unmounts and its focus-close work has settled. */ - afterClose(callback: () => void) { - const complete = () => { - if (content()?.isConnected) { - requestAnimationFrame(complete) - return - } - requestAnimationFrame(() => requestAnimationFrame(callback)) - } - requestAnimationFrame(complete) - }, - } -} diff --git a/packages/app/src/utils/notification-click.test.ts b/packages/app/src/utils/notification-click.test.ts new file mode 100644 index 0000000000..fa81b0e025 --- /dev/null +++ b/packages/app/src/utils/notification-click.test.ts @@ -0,0 +1,27 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { handleNotificationClick, setNavigate } from "./notification-click" + +describe("notification click", () => { + afterEach(() => { + setNavigate(undefined as any) + }) + + test("navigates via registered navigate function", () => { + const calls: string[] = [] + setNavigate((href) => calls.push(href)) + handleNotificationClick("/abc/session/123") + expect(calls).toEqual(["/abc/session/123"]) + }) + + test("does not navigate when href is missing", () => { + const calls: string[] = [] + setNavigate((href) => calls.push(href)) + handleNotificationClick(undefined) + expect(calls).toEqual([]) + }) + + test("falls back to location.assign without registered navigate", () => { + handleNotificationClick("/abc/session/123") + // falls back to window.location.assign — no error thrown + }) +}) diff --git a/packages/app/src/utils/notification-click.ts b/packages/app/src/utils/notification-click.ts new file mode 100644 index 0000000000..316b278206 --- /dev/null +++ b/packages/app/src/utils/notification-click.ts @@ -0,0 +1,13 @@ +let nav: ((href: string) => void) | undefined + +export const setNavigate = (fn: (href: string) => void) => { + nav = fn +} + +export const handleNotificationClick = (href?: string) => { + window.focus() + if (!href) return + if (nav) return nav(href) + console.warn("notification-click: navigate function not set, falling back to window.location.assign") + window.location.assign(href) +} diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts index a7742c399e..a2daae4866 100644 --- a/packages/app/src/utils/persist.ts +++ b/packages/app/src/utils/persist.ts @@ -15,7 +15,6 @@ type PersistedWithReady = [ ] type PersistTarget = { - draft?: boolean storage?: string scope?: "window" legacyStorageNames?: string[] @@ -296,14 +295,6 @@ async function removeAsync(storage: AsyncStorage, key: string) { } catch {} } -function toAsyncStorage(storage: SyncStorage | AsyncStorage): AsyncStorage { - return { - getItem: async (key) => storage.getItem(key), - setItem: async (key, value) => storage.setItem(key, value), - removeItem: async (key) => storage.removeItem(key), - } -} - async function migrateLegacyAsync(input: { current: AsyncStorage legacyStore?: AsyncStorage @@ -522,9 +513,6 @@ export const Persist = { if (session) return Persist.serverSession(scope, dir, session, key, legacy) return Persist.serverWorkspace(scope, dir, key, legacy) }, - prompt(target: PersistTarget): PersistTarget { - return { ...target, draft: true } - }, } function resolveTarget(target: PersistTarget, platform: Platform): PersistTarget { @@ -538,12 +526,9 @@ function resolveTarget(target: PersistTarget, platform: Platform): PersistTarget } export function removePersisted( - target: { draft?: boolean; storage?: string; legacyStorageNames?: string[]; key: string }, + target: { storage?: string; legacyStorageNames?: string[]; key: string }, platform?: Platform, ) { - if (target.draft && platform?.draftStore) { - void platform.draftStore.removeItem(`${target.storage ?? "default"}:${target.key}`) - } const isDesktop = platform?.platform === "desktop" && !!platform.storage if (isDesktop) { @@ -576,17 +561,8 @@ export function persisted( const legacy = config.legacy ?? [] const isDesktop = platform.platform === "desktop" && !!platform.storage - const draft = config.draft ? platform.draftStore : undefined const currentStorage = (() => { - if (draft) { - const prefix = `${config.storage ?? "default"}:` - return { - getItem: (key: string) => draft.getItem(prefix + key), - setItem: (key: string, value: string) => draft.setItem(prefix + key, value), - removeItem: (key: string) => draft.removeItem(prefix + key), - } satisfies AsyncStorage - } if (isDesktop) return platform.storage?.(config.storage) if (!config.storage) return localStorageDirect() return localStorageWithPrefix(config.storage) @@ -601,7 +577,7 @@ export function persisted( const legacyStorageNames = config.legacyStorageNames ?? [] const storage = (() => { - if (!isDesktop && !draft) { + if (!isDesktop) { const current = currentStorage as SyncStorage const legacyStore = legacyStorage as SyncStorage const legacyStores = legacyStorageNames.map(localStorageWithPrefix) @@ -633,26 +609,15 @@ export function persisted( const current = currentStorage as AsyncStorage const legacyStore = legacyStorage as AsyncStorage | undefined - const oldCurrent = draft - ? isDesktop - ? platform.storage?.(config.storage) - : config.storage - ? localStorageWithPrefix(config.storage) - : localStorageDirect() - : undefined - const legacyStores = [ - oldCurrent, - ...legacyStorageNames.map((name) => (isDesktop ? platform.storage?.(name) : localStorageWithPrefix(name))), - ] + const legacyStores = legacyStorageNames + .map((name) => platform.storage?.(name) as AsyncStorage | undefined) .filter((x) => !!x) - .map(toAsyncStorage) - let draftLatest: string | undefined const api: AsyncStorage = { getItem: async (key) => { const value = await readCurrentAsync({ storage: current, key, defaults, migrate: config.migrate }) if (value !== undefined) return value - const migrated = await migrateLegacyAsync({ + return migrateLegacyAsync({ current, legacyStore, stores: legacyStores, @@ -661,15 +626,8 @@ export function persisted( defaults, migrate: config.migrate, }) - if (draftLatest === undefined) { - if (draft && migrated !== null) return (await current.getItem(key)) ?? migrated - return migrated - } - await current.setItem(key, draftLatest) - return draftLatest }, setItem: async (key, value) => { - if (draft) draftLatest = value await current.setItem(key, value) }, removeItem: async (key) => { diff --git a/packages/app/src/utils/prompt.test.ts b/packages/app/src/utils/prompt.test.ts index 8b86d43b90..1ecaf02c97 100644 --- a/packages/app/src/utils/prompt.test.ts +++ b/packages/app/src/utils/prompt.test.ts @@ -37,18 +37,8 @@ describe("extractPromptFromParts", () => { expect(result).toHaveLength(3) expect(result[0]).toMatchObject({ type: "text", content: "check these" }) expect(result.slice(1)).toMatchObject([ - { - type: "image", - filename: "a.png", - mime: "image/png", - blob: expect.objectContaining({ id: expect.any(String) }), - }, - { - type: "image", - filename: "b.pdf", - mime: "application/pdf", - blob: expect.objectContaining({ id: expect.any(String) }), - }, + { type: "image", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" }, + { type: "image", filename: "b.pdf", mime: "application/pdf", dataUrl: "data:application/pdf;base64,BBB" }, ]) }) }) diff --git a/packages/app/src/utils/prompt.ts b/packages/app/src/utils/prompt.ts index 67d32086bb..35aec0071a 100644 --- a/packages/app/src/utils/prompt.ts +++ b/packages/app/src/utils/prompt.ts @@ -1,6 +1,5 @@ import type { AgentPart as MessageAgentPart, FilePart, Part, TextPart } from "@opencode-ai/sdk/v2" import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt" -import { createLegacyBlobReference } from "@/utils/draft-store" type Inline = | { @@ -108,7 +107,7 @@ export function extractPromptFromParts(parts: Part[], opts?: { directory?: strin id: filePart.id, filename: filePart.filename ?? attachmentName, mime: filePart.mime, - blob: createLegacyBlobReference(filePart.url), + dataUrl: filePart.url, }) } } diff --git a/packages/app/src/utils/server-compat.test.ts b/packages/app/src/utils/server-compat.test.ts deleted file mode 100644 index 52e5ec6e3b..0000000000 --- a/packages/app/src/utils/server-compat.test.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { createApiForServer, createSdkForServer } from "./server" -import { createCompatibleApi } from "./server-compat" - -function setup( - protocol: "v1" | "v2" | Promise<"v1" | "v2">, - responses?: { vcs?: { branch: string; default_branch: string } }, -) { - const requests: Request[] = [] - const fetcher = Object.assign( - async (input: string | URL | Request, init?: RequestInit) => { - const request = new Request(input, init) - requests.push(request) - if (request.method === "PATCH") { - return Response.json({ - id: "ses_1", - slug: "ses_1", - projectID: "project", - directory: "/repo", - title: "Session", - version: "1", - time: { created: 1, updated: 1 }, - }) - } - if (request.method === "POST" && request.url.endsWith("/prompt_async")) - return new Response(undefined, { status: 204 }) - if (request.method === "POST" && request.url.endsWith("/prompt")) { - return Response.json({ - admittedSeq: 1, - id: "msg_1", - sessionID: "ses_1", - timeCreated: 1, - type: "user", - data: { text: "hello" }, - delivery: "steer", - }) - } - if (request.method === "GET" && new URL(request.url).pathname === "/vcs") - return Response.json(responses?.vcs ?? {}) - if (request.method === "GET") return Response.json([]) - return new Response(undefined, { status: 204 }) - }, - { preconnect: globalThis.fetch.preconnect }, - ) - const server = { url: "http://localhost:4096" } - const api = createCompatibleApi({ - protocol: typeof protocol === "string" ? Promise.resolve(protocol) : protocol, - current: createApiForServer({ server, fetch: fetcher }), - legacy: (directory) => createSdkForServer({ server, fetch: fetcher, directory, throwOnError: true }), - directory: "/repo", - }) - return { api, requests } -} - -describe("createCompatibleApi", () => { - /* - test("routes V1 archive through the legacy session update", async () => { - const { api, requests } = setup("v1") - await api.session.archive({ sessionID: "ses_1", directory: "/repo" }) - - const url = new URL(requests[0]!.url) - expect(url.pathname).toBe("/session/ses_1") - expect(requests[0]!.headers.get("x-opencode-directory")).toBe("%2Frepo") - expect(requests[0]!.method).toBe("PATCH") - expect(await requests[0]!.json()).toMatchObject({ time: { archived: expect.any(Number) } }) - }) - */ - - test("converts current prompts to the V1 prompt contract", async () => { - const { api, requests } = setup("v1") - await api.session.prompt({ - sessionID: "ses_1", - id: "msg_1", - text: "hello @src/index.ts", - agent: "build", - model: { providerID: "provider", modelID: "model" }, - files: [ - { uri: "file:///repo/src/index.ts", name: "index.ts", mention: { text: "@src/index.ts", start: 6, end: 19 } }, - { uri: "data:text/plain;base64,aGVsbG8=", name: "notes.txt" }, - ], - }) - - expect(new URL(requests[0]!.url).pathname).toBe("/session/ses_1/prompt_async") - const body = await requests[0]!.json() - expect(body).toMatchObject({ - messageID: "msg_1", - agent: "build", - model: { providerID: "provider", modelID: "model" }, - parts: [ - { type: "text", text: "hello @src/index.ts" }, - { - type: "file", - mime: "text/plain", - url: "file:///repo/src/index.ts", - filename: "index.ts", - source: { - type: "file", - text: { value: "@src/index.ts", start: 6, end: 19 }, - path: "file:///repo/src/index.ts", - }, - }, - { - type: "file", - mime: "text/plain", - url: "data:text/plain;base64,aGVsbG8=", - filename: "notes.txt", - }, - ], - }) - expect(body.parts[2]).not.toHaveProperty("source") - }) - - test("preserves original parts for V1 optimistic reconciliation", async () => { - const { api, requests } = setup("v1") - await api.session.prompt({ - sessionID: "ses_1", - id: "msg_1", - text: "look", - files: [{ uri: "data:image/png;base64,AAAA", name: "image.png" }], - legacyParts: [ - { id: "prt_text", type: "text", text: "look" }, - { id: "prt_image", type: "file", mime: "image/png", url: "data:image/png;base64,AAAA", filename: "image.png" }, - ], - }) - - expect((await requests[0]!.json()).parts).toEqual([ - { id: "prt_text", type: "text", text: "look" }, - { id: "prt_image", type: "file", mime: "image/png", url: "data:image/png;base64,AAAA", filename: "image.png" }, - ]) - }) - - test("resolves protocol detection once across implementation methods", async () => { - let detections = 0 - const resolved = Promise.resolve<"v1" | "v2">("v2") - const protocol = new Proxy(resolved, { - get(target, property) { - if (property !== "then") return Reflect.get(target, property, target) - detections++ - return target.then.bind(target) - }, - }) - const { api } = setup(protocol) - - await api.session.list() - await api.session.list() - - expect(detections).toBe(1) - }) - - /* - test("keeps V2 session actions on the current API", async () => { - const { api, requests } = setup("v2") - await api.session.archive({ sessionID: "ses_1" }) - - expect(new URL(requests[0]!.url).pathname).toBe("/api/session/ses_1/archive") - expect(requests[0]!.method).toBe("POST") - }) - */ - - test("uses the global V1 session search endpoint", async () => { - const { api, requests } = setup("v1") - await api.session.list({ parentID: null, search: "session", limit: 50 }) - - expect(new URL(requests[0]!.url).pathname).toBe("/experimental/session") - }) - - /* - test("projects the V1 default branch", async () => { - const { api } = setup("v1", { vcs: { branch: "feature", default_branch: "dev" } }) - - expect(await api.vcs.get({ location: { directory: "/repo" } })).toMatchObject({ - data: { branch: "feature", defaultBranch: "dev" }, - }) - }) - */ - - test("translates current file searches to the V1 dirs parameter", async () => { - const { api, requests } = setup("v1") - await api.file.find({ location: { directory: "/repo" }, query: "src", type: "file", limit: 20 }) - - const url = new URL(requests[0]!.url) - expect(url.pathname).toBe("/find/file") - expect(url.searchParams.get("dirs")).toBe("false") - expect(url.searchParams.get("limit")).toBe("20") - }) - - test("routes V1 permission replies through the requested directory", async () => { - const { api, requests } = setup("v1") - await api.permission.reply({ - sessionID: "ses_1", - requestID: "permission_1", - reply: "once", - location: { directory: "/other" }, - }) - - expect(new URL(requests[0]!.url).pathname).toBe("/session/ses_1/permissions/permission_1") - expect(new URL(requests[0]!.url).searchParams.get("directory")).toBe("/other") - }) - - test("disposes the V1 instance after connecting a provider", async () => { - const { api, requests } = setup("v1") - - await api.integration.connect.key({ - integrationID: "openrouter", - key: "secret", - location: { directory: "/repo" }, - }) - - expect(requests.map((request) => new URL(request.url).pathname)).toEqual([ - "/auth/openrouter", - "/instance/dispose", - "/instance/dispose", - ]) - expect(requests[1]!.headers.get("x-opencode-directory")).toBe("%2Frepo") - expect(requests[2]!.headers.get("x-opencode-directory")).toBeNull() - }) - - test("disposes the V1 instance after completing provider OAuth", async () => { - const { api, requests } = setup("v1") - - await api.integration.oauth.complete({ - integrationID: "openrouter", - attemptID: "openrouter:0", - code: "code", - location: { directory: "/repo" }, - }) - - expect(requests.map((request) => new URL(request.url).pathname)).toEqual([ - "/provider/openrouter/oauth/callback", - "/instance/dispose", - "/instance/dispose", - ]) - expect(requests[1]!.headers.get("x-opencode-directory")).toBe("%2Frepo") - expect(requests[2]!.headers.get("x-opencode-directory")).toBeNull() - }) -}) diff --git a/packages/app/src/utils/server-compat.ts b/packages/app/src/utils/server-compat.ts deleted file mode 100644 index 1df1338b71..0000000000 --- a/packages/app/src/utils/server-compat.ts +++ /dev/null @@ -1,518 +0,0 @@ -import type { ServerApi } from "./server" -import type { ServerProtocol } from "./server-protocol" -import type { AgentPartInput, FilePartInput, OpencodeClient, Session, TextPartInput } from "@opencode-ai/sdk/v2/client" -import type { - Project, - ProjectCurrent, - SessionApi, - SessionCommandInput, - SessionCommandOutput, - SessionCompactInput, - SessionCompactOutput, - SessionInfo, - SessionPromptInput, - SessionPromptOutput, - SessionShellInput, - SessionShellOutput, -} from "@opencode-ai/client/promise" - -type LegacyClient = OpencodeClient -type LegacyFor = (directory?: string) => LegacyClient -type CompatibleSessionApi = Omit< - SessionApi, - "prompt" | "command" | "shell" | "compact" | "rename" | "archive" | "remove" -> & { - prompt: (input: SessionPromptInput & LegacyPrompt) => Promise - command: (input: SessionCommandInput) => Promise - shell: (input: SessionShellInput & LegacyPrompt) => Promise - compact: (input: SessionCompactInput & { model?: LegacyPrompt["model"] }) => Promise - rename: (input: Parameters[0] & LegacyLocation) => ReturnType - // archive: (input: Parameters[0] & LegacyLocation) => ReturnType - remove: (input: Parameters[0] & LegacyLocation) => ReturnType -} -type CompatiblePermissionApi = Omit & { - reply: ( - input: Parameters[0] & { location?: { directory?: string } }, - ) => ReturnType -} -export type CompatibleApi = Omit & { - readonly session: CompatibleSessionApi - readonly permission: CompatiblePermissionApi -} -type LegacyPrompt = { - agent?: string - model?: { providerID: string; modelID: string } - variant?: string - legacyParts?: (TextPartInput | FilePartInput | AgentPartInput)[] -} -type LegacyLocation = { directory?: string } -type CompatibleInput = { - protocol: Promise - current: ServerApi - legacy: LegacyFor - directory?: string -} - -function mime(uri: string) { - const match = /^data:([^;,]+)/.exec(uri) - return match?.[1] ?? "application/octet-stream" -} - -function sessionInfo(session: Session): SessionInfo { - return { - id: session.id, - parentID: session.parentID, - projectID: session.projectID, - agent: session.agent, - model: session.model && { - id: session.model.id, - providerID: session.model.providerID, - variant: session.model.variant, - }, - cost: session.cost ?? 0, - tokens: session.tokens ?? { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - time: session.time, - title: session.title, - location: { directory: session.directory, workspaceID: session.workspaceID }, - subpath: session.path, - revert: session.revert && { - messageID: session.revert.messageID, - partID: session.revert.partID, - snapshot: session.revert.snapshot, - }, - } -} - -export function createCompatibleApi(input: CompatibleInput): CompatibleApi { - const v1 = createV1Api(input) - return lazyApi( - input.protocol.then((protocol) => (protocol === "v1" ? v1 : input.current)), - input.current, - ) -} - -function lazyApi(implementation: Promise, shape: T): T { - const cache = new Map() - return new Proxy(shape, { - get(target, property, receiver) { - const sample = Reflect.get(target, property, receiver) - if (typeof sample === "function") { - return (...args: unknown[]) => - implementation.then((value) => { - const method = Reflect.get(value, property) - if (typeof method !== "function") throw new Error(`API method unavailable: ${String(property)}`) - return Reflect.apply(method, value, args) - }) - } - if (sample === null || typeof sample !== "object") return sample - if (cache.has(property)) return cache.get(property) - const nested = lazyApi( - implementation.then((value) => { - const result = Reflect.get(value, property) - if (result === null || typeof result !== "object") { - throw new Error(`API namespace unavailable: ${String(property)}`) - } - return result - }), - sample, - ) - cache.set(property, nested) - return nested - }, - }) -} - -function createV1Api(input: CompatibleInput): CompatibleApi { - const directory = (location?: { directory?: string }) => location?.directory ?? input.directory - const legacy = (location?: { directory?: string }) => input.legacy(directory(location)) - const located = (data: T, value?: { directory?: string }) => ({ - location: { - directory: directory(value) ?? "", - project: { id: "", directory: directory(value) ?? "" }, - }, - data, - }) - - return { - ...input.current, - session: { - ...input.current.session, - async list( - value?: Parameters[0], - options?: Parameters[1], - ) { - if (!value?.directory && value?.search !== undefined) { - const result = await legacy().experimental.session.list( - { - roots: value.parentID === null ? true : undefined, - search: value.search, - limit: value.limit, - }, - options, - ) - return { data: (result.data ?? []).map(sessionInfo), cursor: {} } - } - const result = await legacy({ directory: value?.directory }).session.list({ - directory: value?.directory, - roots: value?.parentID === null ? true : undefined, - search: value?.search, - limit: value?.limit, - }) - return { data: (result.data ?? []).map(sessionInfo), cursor: {} } - }, - async create(value?: Parameters[0]) { - const result = await legacy(value?.location ?? undefined).session.create({ - directory: directory(value?.location ?? undefined), - }) - if (!result.data) throw new Error("Failed to create session") - return sessionInfo(result.data) - }, - async get(value: Parameters[0]) { - const result = await legacy().session.get(value) - if (!result.data) throw new Error(`Session not found: ${value.sessionID}`) - return sessionInfo(result.data) - }, - async active() { - const result = await legacy().session.status() - return Object.fromEntries( - Object.entries(result.data ?? {}).flatMap(([sessionID, status]) => - status.type === "idle" ? [] : [[sessionID, { type: "running" as const }]], - ), - ) - }, - async rename(value: Parameters[0] & LegacyLocation) { - await legacy(value).session.update({ sessionID: value.sessionID, title: value.title }) - }, - // async archive(value: Parameters[0] & LegacyLocation) { - // await legacy(value).session.update({ sessionID: value.sessionID, time: { archived: Date.now() } }) - // }, - async remove(value: Parameters[0] & LegacyLocation) { - await legacy(value).session.delete(value) - }, - async fork(value: Parameters[0]) { - const result = await legacy().session.fork(value) - if (!result.data) throw new Error("Failed to fork session") - return sessionInfo(result.data) - }, - async interrupt(value: Parameters[0]) { - await legacy().session.abort(value) - }, - async prompt(value: SessionPromptInput & LegacyPrompt) { - await legacy().session.promptAsync({ - sessionID: value.sessionID, - messageID: value.id ?? undefined, - agent: value.agent, - model: value.model, - variant: value.variant, - parts: value.legacyParts ?? [ - { type: "text", text: value.text }, - ...(value.files ?? []).map((file) => ({ - type: "file" as const, - mime: file.mention ? "text/plain" : mime(file.uri), - url: file.uri, - filename: file.name, - source: file.mention - ? { - type: "file" as const, - text: { value: file.mention.text, start: file.mention.start, end: file.mention.end }, - path: file.uri, - } - : undefined, - })), - ...(value.agents ?? []).map((agent) => ({ - type: "agent" as const, - name: agent.name, - source: agent.mention - ? { value: agent.mention.text, start: agent.mention.start, end: agent.mention.end } - : undefined, - })), - ], - }) - return { - admittedSeq: 0, - id: value.id ?? "", - sessionID: value.sessionID, - timeCreated: Date.now(), - type: "user", - data: { text: value.text }, - delivery: value.delivery ?? "steer", - } - }, - async command(value: SessionCommandInput) { - await legacy().session.command({ - sessionID: value.sessionID, - messageID: value.id ?? undefined, - command: value.command, - arguments: value.arguments ?? "", - agent: value.agent ?? undefined, - model: value.model ? `${value.model.providerID}/${value.model.id}` : undefined, - variant: value.model?.variant, - parts: value.files?.map((file) => ({ - type: "file" as const, - mime: mime(file.uri), - url: file.uri, - filename: file.name, - })), - }) - return { - admittedSeq: 0, - id: value.id ?? "", - sessionID: value.sessionID, - timeCreated: Date.now(), - type: "user", - data: { text: `/${value.command} ${value.arguments ?? ""}`.trim() }, - delivery: value.delivery ?? "steer", - } - }, - async shell(value: SessionShellInput & LegacyPrompt) { - await legacy().session.shell({ - sessionID: value.sessionID, - command: value.command, - agent: value.agent, - model: value.model, - }) - }, - compact: async (value: SessionCompactInput & { model?: LegacyPrompt["model"] }) => { - if (!value.model) throw new Error("A model is required to compact a V1 session") - await legacy().session.summarize({ - sessionID: value.sessionID, - providerID: value.model.providerID, - modelID: value.model.modelID, - }) - return { - admittedSeq: 0, - id: value.id ?? "", - sessionID: value.sessionID, - timeCreated: Date.now(), - type: "compaction", - } - }, - revert: { - stage: async (value: Parameters[0]) => { - await legacy().session.revert(value) - return { messageID: value.messageID } - }, - clear: async (value: Parameters[0]) => { - await legacy().session.unrevert(value) - }, - commit: input.current.session.revert.commit, - }, - }, - project: { - ...input.current.project, - async list() { - return ((await legacy().project.list()).data ?? []) as Project[] - }, - async current(value?: Parameters[0]) { - const result = await legacy(value?.location).project.current() - if (!result.data) throw new Error("Project not found") - return { id: result.data.id, directory: result.data.worktree } satisfies ProjectCurrent - }, - // async update(value: Parameters[0]) { - // const project = (await legacy().project.list()).data?.find((item) => item.id === value.projectID) - // const result = await legacy({ directory: project?.worktree }).project.update({ - // ...value, - // directory: project?.worktree, - // }) - // if (!result.data) throw new Error(`Project not found: ${value.projectID}`) - // return result.data as Project - // }, - async directories(value: Parameters[0]) { - const result = await legacy(value.location).worktree.list() - return (result.data ?? []).map((item) => ({ directory: item })) - }, - }, - // path: { - // ...input.current.path, - // async get(value?: Parameters[0]) { - // const result = await legacy(value?.location).path.get() - // if (!result.data) throw new Error("Path unavailable") - // return result.data - // }, - // }, - vcs: { - ...input.current.vcs, - // async get(value?: Parameters[0]) { - // const result = await legacy(value?.location).vcs.get() - // return located({ branch: result.data?.branch, defaultBranch: result.data?.default_branch }, value?.location) - // }, - async status(value?: Parameters[0]) { - const result = await legacy(value?.location).vcs.status() - return located(result.data ?? [], value?.location) - }, - async diff(value: Parameters[0]) { - const result = await legacy(value.location).vcs.diff({ - mode: value.mode === "working" ? "git" : value.mode, - context: value.context, - }) - return located( - (result.data ?? []).map((file) => ({ - file: file.file, - patch: file.patch ?? "", - additions: file.additions, - deletions: file.deletions, - status: file.status ?? "modified", - })), - value.location, - ) - }, - }, - file: { - ...input.current.file, - async list(value?: Parameters[0]) { - const result = await legacy(value?.location).file.list({ path: value?.path ?? "" }) - return located(result.data ?? [], value?.location) - }, - async find(value: Parameters[0]) { - const result = await legacy(value.location).find.files({ - query: value.query, - dirs: value.type === undefined ? undefined : value.type === "directory" ? "true" : "false", - limit: value.limit, - }) - return located( - (result.data ?? []).map((path) => ({ path, type: value.type ?? "file" })), - value.location, - ) - }, - }, - integration: { - ...input.current.integration, - async get(value: Parameters[0]) { - const methods = ((await legacy(value.location).provider.auth()).data?.[value.integrationID] ?? []).map( - (method, index) => - method.type === "api" - ? { type: "key" as const, label: method.label } - : { type: "oauth" as const, id: String(index), label: method.label, prompts: method.prompts }, - ) - return located( - { - id: value.integrationID, - name: value.integrationID, - methods, - connections: [], - }, - value.location, - ) - }, - connect: { - ...input.current.integration.connect, - key: async (value: Parameters[0]) => { - await legacy(value.location).auth.set({ - providerID: value.integrationID, - auth: { type: "api", key: value.key }, - }) - await legacy(value.location).instance.dispose() - await input.legacy().instance.dispose() - }, - }, - oauth: { - ...input.current.integration.oauth, - connect: async (value: Parameters[0]) => { - const method = Number(value.methodID) - const result = await legacy(value.location).provider.oauth.authorize( - { providerID: value.integrationID, method, inputs: value.inputs }, - { throwOnError: true }, - ) - if (!result.data) throw new Error("Failed to start OAuth authorization") - return located( - { - attemptID: `${value.integrationID}:${method}`, - url: result.data.url, - instructions: result.data.instructions, - mode: result.data.method, - time: { created: Date.now(), expires: Date.now() + 10 * 60 * 1000 }, - }, - value.location, - ) - }, - complete: async (value: Parameters[0]) => { - const method = Number(value.attemptID.split(":").at(-1)) - await legacy(value.location).provider.oauth.callback( - { providerID: value.integrationID, method, code: value.code }, - { throwOnError: true }, - ) - await legacy(value.location).instance.dispose() - await input.legacy().instance.dispose() - }, - status: async (value: Parameters[0]) => { - const method = Number(value.attemptID.split(":").at(-1)) - await legacy(value.location).provider.oauth.callback( - { providerID: value.integrationID, method }, - { throwOnError: true }, - ) - await legacy(value.location).instance.dispose() - await input.legacy().instance.dispose() - return located( - { status: "complete" as const, time: { created: Date.now(), expires: Date.now() } }, - value.location, - ) - }, - }, - }, - pty: { - ...input.current.pty, - // async shells(value?: Parameters[0]) { - // return located((await legacy(value?.location).pty.shells()).data ?? [], value?.location) - // }, - async list(value?: Parameters[0]) { - return located((await legacy(value?.location).pty.list()).data ?? [], value?.location) - }, - async create(value?: Parameters[0]) { - const result = await legacy(value?.location).pty.create({ - command: value?.command, - args: value?.args ? [...value.args] : undefined, - cwd: value?.cwd, - title: value?.title, - env: value?.env, - }) - if (!result.data) throw new Error("Failed to create terminal") - return located(result.data, value?.location) - }, - async get(value: Parameters[0]) { - const result = await legacy(value.location).pty.get({ ptyID: value.ptyID }) - if (!result.data) throw new Error(`Terminal not found: ${value.ptyID}`) - return located(result.data, value.location) - }, - async update(value: Parameters[0]) { - const result = await legacy(value.location).pty.update({ - ptyID: value.ptyID, - title: value.title, - size: value.size, - }) - if (!result.data) throw new Error(`Terminal not found: ${value.ptyID}`) - return located(result.data, value.location) - }, - async remove(value: Parameters[0]) { - await legacy(value.location).pty.remove({ ptyID: value.ptyID }) - }, - // async connectToken(value: Parameters[0]) { - // const result = await legacy(value.location).pty.connectToken({ ptyID: value.ptyID }) - // if (!result.data) throw new Error(`Failed to connect terminal: ${value.ptyID}`) - // return located(result.data, value.location) - // }, - }, - permission: { - ...input.current.permission, - async reply(value: Parameters[0] & { location?: { directory?: string } }) { - await legacy(value.location).permission.respond({ - sessionID: value.sessionID, - permissionID: value.requestID, - response: value.reply, - directory: directory(value.location), - }) - }, - }, - question: { - ...input.current.question, - async reply(value: Parameters[0]) { - await legacy().question.reply({ - requestID: value.requestID, - answers: value.answers.map((answer) => [...answer]), - }) - }, - async reject(value: Parameters[0]) { - await legacy().question.reject({ requestID: value.requestID }) - }, - }, - } -} diff --git a/packages/app/src/utils/server-health.test.ts b/packages/app/src/utils/server-health.test.ts index 69a8c7b3be..b1c8f2c7e2 100644 --- a/packages/app/src/utils/server-health.test.ts +++ b/packages/app/src/utils/server-health.test.ts @@ -14,45 +14,15 @@ function abortFromInput(input: RequestInfo | URL, init?: RequestInit) { describe("checkServerHealth", () => { test("returns healthy response with version", async () => { - let request: URL | undefined - const fetch = (async (input: RequestInfo | URL) => { - request = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input) - return new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), { + const fetch = (async () => + new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), { status: 200, headers: { "content-type": "application/json" }, - }) - }) as unknown as typeof globalThis.fetch + })) as unknown as typeof globalThis.fetch const result = await checkServerHealth(server, fetch) expect(result).toEqual({ healthy: true, version: "1.2.3" }) - expect(request?.pathname).toBe("/api/health") - }) - - test("falls back to the V1 health endpoint", async () => { - const paths: string[] = [] - const fetch = (async (input: RequestInfo | URL) => { - const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input) - paths.push(url.pathname) - if (url.pathname === "/api/health") return new Response(undefined, { status: 404 }) - return Response.json({ healthy: true, version: "1.18.4" }) - }) as unknown as typeof globalThis.fetch - - expect(await checkServerHealth(server, fetch)).toEqual({ healthy: true, version: "1.18.4" }) - expect(paths).toEqual(["/api/health", "/global/health"]) - }) - - test("falls back when the current health response is malformed", async () => { - const paths: string[] = [] - const fetch = (async (input: RequestInfo | URL) => { - const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input) - paths.push(url.pathname) - if (url.pathname === "/api/health") return Response.json({}) - return Response.json({ healthy: true, version: "1.18.4" }) - }) as unknown as typeof globalThis.fetch - - expect(await checkServerHealth(server, fetch)).toEqual({ healthy: true, version: "1.18.4" }) - expect(paths).toEqual(["/api/health", "/global/health"]) }) test("allows slow servers thirty seconds by default", async () => { @@ -172,7 +142,7 @@ describe("checkServerHealth", () => { retryDelayMs: 1, }) - expect(count).toBe(6) + expect(count).toBe(3) expect(result).toEqual({ healthy: false }) }) }) diff --git a/packages/app/src/utils/server-health.ts b/packages/app/src/utils/server-health.ts index 1d7d9e4b2e..1b684d9af7 100644 --- a/packages/app/src/utils/server-health.ts +++ b/packages/app/src/utils/server-health.ts @@ -1,7 +1,6 @@ import { usePlatform } from "@/context/platform" import { ServerConnection } from "@/context/server" -import { authTokenFromCredentials, createSdkForServer } from "./server" -import { ClientError, OpenCode } from "@opencode-ai/client" +import { createSdkForServer } from "./server" import { Accessor, createEffect, onCleanup } from "solid-js" import { createStore, reconcile } from "solid-js/store" @@ -62,7 +61,6 @@ function wait(ms: number, signal?: AbortSignal) { function retryable(error: unknown, signal?: AbortSignal) { if (signal?.aborted) return false - if (error instanceof ClientError) return error.reason === "Transport" if (!(error instanceof Error)) return false if (error.name === "AbortError" || error.name === "TimeoutError") return false if (error instanceof TypeError) return true @@ -84,31 +82,15 @@ export async function checkServerHealth( .then(() => attempt(count + 1)) .catch(() => ({ healthy: false })) } - const attempt = async (count: number): Promise => { - const current = await OpenCode.make({ - baseUrl: server.url, + const attempt = (count: number): Promise => + createSdkForServer({ + server, fetch, - headers: server.password - ? { - Authorization: `Basic ${authTokenFromCredentials({ username: server.username, password: server.password })}`, - } - : undefined, + signal, }) - .health.get({ signal }) - .then((x) => - typeof x.healthy === "boolean" - ? { data: { healthy: x.healthy, version: x.version } } - : { error: new Error("Invalid health response") }, - ) - .catch((error) => ({ error })) - if ("data" in current && current.data) return current.data - if (signal?.aborted) return { healthy: false } - - return createSdkForServer({ server, fetch, signal }) .global.health() .then((x) => (x.error ? next(count, x.error) : { healthy: x.data?.healthy === true, version: x.data?.version })) .catch((error) => next(count, error)) - } return attempt(0).finally(() => timeout?.clear?.()) } diff --git a/packages/app/src/utils/server-protocol.test.ts b/packages/app/src/utils/server-protocol.test.ts deleted file mode 100644 index 2130a968c4..0000000000 --- a/packages/app/src/utils/server-protocol.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { detectServerProtocol } from "./server-protocol" - -const server = { url: "http://localhost:4096" } -const json = (value: unknown, status = 200) => - new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } }) -const mockFetch = (run: (input: string | URL | Request) => Promise) => - Object.assign(run, { preconnect: globalThis.fetch.preconnect }) - -describe("detectServerProtocol", () => { - test("prefers the legacy health endpoint when both API generations exist", async () => { - const fetcher = mockFetch((input) => { - const path = new URL(input instanceof Request ? input.url : input).pathname - if (path === "/global/health") return Promise.resolve(json({ healthy: true, version: "1.18.4" })) - return Promise.resolve(json({ healthy: true, version: "2.0.0", pid: 123 })) - }) - - expect(await detectServerProtocol(server, fetcher)).toBe("v1") - }) - - test("recognizes V2 health by its process identifier", async () => { - const fetcher = mockFetch((input) => { - const path = new URL(input instanceof Request ? input.url : input).pathname - if (path === "/global/health") return Promise.resolve(json({}, 404)) - return Promise.resolve(json({ healthy: true, version: "2.0.0", pid: 123 })) - }) - - expect(await detectServerProtocol(server, fetcher)).toBe("v2") - }) - - test("recognizes the transitional V1 API health response", async () => { - const fetcher = mockFetch((input) => { - const path = new URL(input instanceof Request ? input.url : input).pathname - if (path === "/global/health") return Promise.resolve(json({}, 404)) - return Promise.resolve(json({ healthy: true })) - }) - - expect(await detectServerProtocol(server, fetcher)).toBe("v1") - }) -}) diff --git a/packages/app/src/utils/server-protocol.ts b/packages/app/src/utils/server-protocol.ts deleted file mode 100644 index 27b8dc208e..0000000000 --- a/packages/app/src/utils/server-protocol.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { ServerConnection } from "@/context/server" -import { authTokenFromCredentials } from "./server" - -export type ServerProtocol = "v1" | "v2" - -function headers(server: ServerConnection.HttpBase) { - if (!server.password) return - return { - Authorization: `Basic ${authTokenFromCredentials({ username: server.username, password: server.password })}`, - } -} - -async function probe(server: ServerConnection.HttpBase, fetch: typeof globalThis.fetch, path: string) { - const response = await fetch(new URL(path, server.url), { - headers: headers(server), - signal: AbortSignal.timeout(5_000), - }) - if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) return - const value: unknown = await response.json() - if (!value || typeof value !== "object") return - return value -} - -export async function detectServerProtocol( - server: ServerConnection.HttpBase, - fetch: typeof globalThis.fetch, -): Promise { - const legacy = await probe(server, fetch, "/global/health").catch(() => undefined) - if (legacy && "healthy" in legacy && legacy.healthy === true) return "v1" - - const current = await probe(server, fetch, "/api/health").catch(() => undefined) - if (current && "pid" in current && typeof current.pid === "number") return "v2" - if (current && "healthy" in current && current.healthy === true) return "v1" - return "v2" -} diff --git a/packages/app/src/utils/server.ts b/packages/app/src/utils/server.ts index 1c8292ca9d..603784e4d4 100644 --- a/packages/app/src/utils/server.ts +++ b/packages/app/src/utils/server.ts @@ -1,5 +1,4 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" -import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise" import type { ServerConnection } from "@/context/server" import { decode64 } from "@/utils/base64" @@ -40,23 +39,3 @@ export function createSdkForServer({ baseUrl: server.url, }) } - -export function createApiForServer(input: { - server: ServerConnection.HttpBase - fetch?: typeof globalThis.fetch -}): OpenCodeClient { - return OpenCode.make({ - baseUrl: input.server.url, - fetch: input.fetch, - headers: input.server.password - ? { - Authorization: `Basic ${authTokenFromCredentials({ - username: input.server.username, - password: input.server.password, - })}`, - } - : undefined, - }) -} - -export type ServerApi = OpenCodeClient diff --git a/packages/app/src/utils/session-message.test.ts b/packages/app/src/utils/session-message.test.ts deleted file mode 100644 index a69c414e16..0000000000 --- a/packages/app/src/utils/session-message.test.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { SessionMessageInfo } from "@opencode-ai/client/promise" -import { normalizeSessionMessages } from "./session-message" - -describe("normalizeSessionMessages", () => { - test("projects current turns into stable legacy rendering records", () => { - const source = [ - { id: "msg_1", type: "agent-switched", agent: "build", time: { created: 1 } }, - { - id: "msg_2", - type: "model-switched", - model: { id: "claude", providerID: "anthropic", variant: "high" }, - time: { created: 2 }, - }, - { - id: "msg_3", - type: "user", - text: "inspect @src/client.ts", - files: [ - { - data: "aGVsbG8=", - mime: "text/plain", - name: "note.txt", - source: { type: "inline" }, - }, - { - data: "ZXhwb3J0IHt9", - mime: "text/plain", - name: "client.ts", - source: { type: "inline" }, - mention: { text: "@src/client.ts", start: 8, end: 22 }, - }, - ], - agents: [{ name: "review", mention: { text: "@review", start: 0, end: 7 } }], - time: { created: 3 }, - }, - { - id: "msg_4", - type: "assistant", - agent: "build", - model: { id: "claude", providerID: "anthropic", variant: "high" }, - content: [ - { type: "reasoning", text: "Thinking", time: { created: 4, completed: 5 } }, - { type: "text", text: "Result" }, - { - type: "tool", - id: "call_1", - name: "read", - state: { - status: "completed", - input: { filePath: "note.txt" }, - metadata: { title: "note.txt" }, - content: [{ type: "text", text: "hello" }], - }, - time: { created: 5, ran: 6, completed: 7 }, - }, - ], - cost: 0.1, - tokens: { input: 10, output: 5, reasoning: 2, cache: { read: 1, write: 0 } }, - time: { created: 4, completed: 7 }, - }, - { - id: "msg_5", - type: "compaction", - status: "completed", - reason: "auto", - summary: "summary", - recent: "recent", - time: { created: 8 }, - }, - ] satisfies SessionMessageInfo[] - - const result = normalizeSessionMessages("ses_1", source) - - expect(result.messages).toHaveLength(2) - expect(result.messages[0]).toMatchObject({ - id: "msg_3", - role: "user", - agent: "build", - model: { providerID: "anthropic", modelID: "claude", variant: "high" }, - }) - expect(result.messages[1]).toMatchObject({ id: "msg_4", role: "assistant", parentID: "msg_3", cost: 0.1 }) - expect(result.parts.get("msg_3")?.map((part) => part.id)).toEqual([ - "msg_3:text:0", - "msg_3:file:0", - "msg_3:file:1", - "msg_3:agent:0", - "msg_5:compaction", - ]) - expect(result.parts.get("msg_3")?.[2]).toMatchObject({ - type: "file", - source: { - type: "file", - path: "src/client.ts", - text: { value: "@src/client.ts", start: 8, end: 22 }, - }, - }) - expect(result.parts.get("msg_4")?.map((part) => part.id)).toEqual(["msg_4:reasoning:0", "msg_4:text:0", "call_1"]) - expect(result.parts.get("msg_4")?.[2]).toMatchObject({ - type: "tool", - tool: "read", - state: { status: "completed", output: "hello" }, - }) - }) - - test("does not invent a parent for an assistant-only page", () => { - const source = [ - { - id: "msg_2", - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [{ type: "text", text: "orphan" }], - time: { created: 2 }, - }, - ] satisfies SessionMessageInfo[] - - expect(normalizeSessionMessages("ses_1", source).messages).toEqual([]) - }) - - test("projects a current shell message into a renderable standalone turn", () => { - const source = [ - { - id: "msg_shell", - type: "shell", - shellID: "shell_1", - command: "printf hello", - status: "exited", - exit: 0, - output: { output: "hello", cursor: 5, size: 5, truncated: false }, - time: { created: 1, completed: 2 }, - }, - ] satisfies SessionMessageInfo[] - - const result = normalizeSessionMessages("ses_1", source) - - expect(result.messages).toEqual([ - expect.objectContaining({ id: "msg_shell", role: "user" }), - expect.objectContaining({ id: "msg_shell:assistant", role: "assistant", parentID: "msg_shell" }), - ]) - expect(result.parts.get("msg_shell")).toEqual([expect.objectContaining({ type: "text", text: "printf hello" })]) - expect(result.parts.get("msg_shell:assistant")).toEqual([ - expect.objectContaining({ - type: "tool", - tool: "bash", - state: expect.objectContaining({ - status: "completed", - input: { command: "printf hello" }, - output: "hello", - title: "Shell", - }), - }), - ]) - }) - - test("adapts current edit fields for the legacy edit renderer", () => { - const source = [ - { id: "msg_user", type: "user", text: "edit it", time: { created: 1 } }, - { - id: "msg_assistant", - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [ - { - type: "tool", - id: "call_edit", - name: "edit", - state: { - status: "completed", - input: { path: "/repo/README.md", oldString: "old", newString: "new" }, - content: [{ type: "text", text: "Edited file successfully" }], - metadata: { - files: [ - { - file: "README.md", - patch: "@@ -1 +1 @@\n-old\n+new", - additions: 1, - deletions: 1, - status: "modified", - }, - ], - replacements: 1, - }, - }, - time: { created: 2, ran: 3, completed: 4 }, - }, - ], - time: { created: 2, completed: 4 }, - }, - ] satisfies SessionMessageInfo[] - - const result = normalizeSessionMessages("ses_1", source) - - expect(result.parts.get("msg_assistant")).toEqual([ - expect.objectContaining({ - type: "tool", - tool: "edit", - state: expect.objectContaining({ - status: "completed", - input: expect.objectContaining({ path: "/repo/README.md", filePath: "/repo/README.md" }), - metadata: expect.objectContaining({ - filediff: { - file: "README.md", - patch: "@@ -1 +1 @@\n-old\n+new", - additions: 1, - deletions: 1, - }, - }), - }), - }), - ]) - }) -}) diff --git a/packages/app/src/utils/session-message.ts b/packages/app/src/utils/session-message.ts deleted file mode 100644 index 93d86a66bb..0000000000 --- a/packages/app/src/utils/session-message.ts +++ /dev/null @@ -1,358 +0,0 @@ -import type { - SessionMessageAssistant, - SessionMessageAssistantTool, - SessionMessageInfo, - SessionMessageShell, - SessionMessageUser, -} from "@opencode-ai/client/promise" -import type { AssistantMessage, FilePart, Message, Part, ToolPart, UserMessage } from "@opencode-ai/sdk/v2" -import { Option, Schema } from "effect" - -const emptyTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } -const emptyModel: { id: string; providerID: string; variant?: string } = { id: "", providerID: "" } -const decodeToolInput = Schema.decodeUnknownOption(Schema.UnknownFromJsonString) - -function record(value: unknown): value is Record { - return !!value && typeof value === "object" && !Array.isArray(value) -} - -function normalizeToolInput(name: string, input: Record) { - if (!["edit", "write"].includes(name) || typeof input.path !== "string" || typeof input.filePath === "string") - return input - return { ...input, filePath: input.path } -} - -function normalizeToolMetadata(name: string, metadata: Record) { - if (name !== "edit" || !Array.isArray(metadata.files)) return metadata - const file = metadata.files.find(record) - if (!file || typeof file.file !== "string") return metadata - return { - ...metadata, - filediff: { - file: file.file, - patch: typeof file.patch === "string" ? file.patch : undefined, - additions: typeof file.additions === "number" ? file.additions : 0, - deletions: typeof file.deletions === "number" ? file.deletions : 0, - }, - } -} - -export function normalizeSessionMessages(sessionID: string, source: readonly SessionMessageInfo[]) { - const messages: Message[] = [] - const parts = new Map() - let agent = "" - let model = emptyModel - let parentID: string | undefined - - source.forEach((message) => { - if (message.type === "agent-switched") { - agent = message.agent - return - } - if (message.type === "model-switched") { - model = message.model - return - } - if (message.type === "user") { - parentID = message.id - messages.push(userMessage(sessionID, message, agent, model)) - parts.set(message.id, userParts(sessionID, message)) - return - } - if (message.type === "synthetic" && message.description?.trim()) { - parentID = message.id - messages.push({ - id: message.id, - sessionID, - role: "user", - time: message.time, - agent, - model: { providerID: model.providerID, modelID: model.id, variant: model.variant }, - }) - parts.set(message.id, [textPart(sessionID, message.id, 0, message.description, true)]) - return - } - if (message.type === "shell") { - messages.push(...shellMessages(sessionID, message, agent, model)) - parts.set(message.id, [textPart(sessionID, message.id, 0, message.command)]) - parts.set(`${message.id}:assistant`, [shellPart(sessionID, message)]) - parentID = undefined - return - } - if (message.type === "assistant") { - agent = message.agent - model = message.model - if (!parentID) return - const parent = messages.findLast((item) => item.id === parentID) - if (parent?.role === "user") { - parent.agent = message.agent - parent.model = { - providerID: message.model.providerID, - modelID: message.model.id, - variant: message.model.variant, - } - } - messages.push(assistantMessage(sessionID, parentID, message)) - parts.set(message.id, assistantParts(sessionID, message)) - return - } - if (message.type !== "compaction" || !parentID) return - parts.set(parentID, [ - ...(parts.get(parentID) ?? []), - { - id: `${message.id}:compaction`, - sessionID, - messageID: parentID, - type: "compaction", - auto: message.reason === "auto", - }, - ]) - }) - - return { messages, parts } -} - -function shellMessages( - sessionID: string, - message: SessionMessageShell, - agent: string, - model: { id: string; providerID: string; variant?: string }, -): [UserMessage, AssistantMessage] { - return [ - { - id: message.id, - sessionID, - role: "user", - time: { created: message.time.created }, - agent, - model: { providerID: model.providerID, modelID: model.id, variant: model.variant }, - }, - { - id: `${message.id}:assistant`, - sessionID, - role: "assistant", - time: message.time, - parentID: message.id, - modelID: model.id, - providerID: model.providerID, - variant: model.variant, - mode: agent, - agent, - path: { cwd: "", root: "" }, - cost: 0, - tokens: emptyTokens, - }, - ] -} - -function shellPart(sessionID: string, message: SessionMessageShell): ToolPart { - const input = { command: message.command } - const start = message.time.created - const state: ToolPart["state"] = - message.status === "running" - ? { status: "running", input, time: { start } } - : { - status: "completed", - input, - output: message.output?.output ?? "", - title: "Shell", - metadata: { - status: message.status, - exit: message.exit, - truncated: message.output?.truncated, - }, - time: { start, end: message.time.completed ?? start }, - } - return { - id: `${message.id}:tool`, - sessionID, - messageID: `${message.id}:assistant`, - type: "tool", - callID: message.shellID, - tool: "bash", - state, - } -} - -export function sessionMessagePartID(messageID: string, type: "text" | "reasoning", ordinal: number) { - return `${messageID}:${type}:${ordinal}` -} - -function userMessage( - sessionID: string, - message: SessionMessageUser, - agent: string, - model: { id: string; providerID: string; variant?: string }, -): UserMessage { - return { - id: message.id, - sessionID, - role: "user", - time: message.time, - agent, - model: { providerID: model.providerID, modelID: model.id, variant: model.variant }, - } -} - -function userParts(sessionID: string, message: SessionMessageUser): Part[] { - return [ - textPart(sessionID, message.id, 0, message.text), - ...(message.files ?? []).map( - (file, index): FilePart => ({ - id: `${message.id}:file:${index}`, - sessionID, - messageID: message.id, - type: "file", - mime: file.mime, - filename: file.name, - url: file.source.type === "uri" ? file.source.uri : `data:${file.mime};base64,${file.data}`, - source: file.mention - ? { - type: "file", - text: { value: file.mention.text, start: file.mention.start, end: file.mention.end }, - path: file.mention.text.startsWith("@") ? file.mention.text.slice(1) : (file.name ?? file.mention.text), - } - : undefined, - }), - ), - ...(message.agents ?? []).map( - (item, index): Part => ({ - id: `${message.id}:agent:${index}`, - sessionID, - messageID: message.id, - type: "agent", - name: item.name, - source: item.mention - ? { value: item.mention.text, start: item.mention.start, end: item.mention.end } - : undefined, - }), - ), - ] -} - -function assistantMessage(sessionID: string, parentID: string, message: SessionMessageAssistant): AssistantMessage { - const error = message.error - ? message.error.type.toLowerCase().includes("abort") || message.error.type.toLowerCase().includes("interrupt") - ? { name: "MessageAbortedError" as const, data: { message: message.error.message } } - : { name: "UnknownError" as const, data: { message: message.error.message } } - : undefined - return { - id: message.id, - sessionID, - role: "assistant", - time: message.time, - error, - parentID, - modelID: message.model.id, - providerID: message.model.providerID, - variant: message.model.variant, - mode: message.agent, - agent: message.agent, - path: { cwd: "", root: "" }, - cost: message.cost ?? 0, - tokens: message.tokens ?? emptyTokens, - finish: message.finish, - } -} - -function assistantParts(sessionID: string, message: SessionMessageAssistant): Part[] { - const ordinals = { text: 0, reasoning: 0 } - return message.content.flatMap((content): Part[] => { - if (content.type === "text") { - const part = textPart(sessionID, message.id, ordinals.text++, content.text) - return content.text.trim() ? [part] : [] - } - if (content.type === "reasoning") { - const part: Part = { - id: sessionMessagePartID(message.id, "reasoning", ordinals.reasoning++), - sessionID, - messageID: message.id, - type: "reasoning", - text: content.text, - metadata: content.state, - time: { - start: content.time?.created ?? message.time.created, - end: content.time?.completed, - }, - } - return content.text.trim() ? [part] : [] - } - return [toolPart(sessionID, message.id, content)] - }) -} - -function textPart(sessionID: string, messageID: string, ordinal: number, text: string, synthetic?: boolean): Part { - return { - id: sessionMessagePartID(messageID, "text", ordinal), - sessionID, - messageID, - type: "text", - text, - synthetic, - } -} - -function toolPart(sessionID: string, messageID: string, tool: SessionMessageAssistantTool): ToolPart { - const start = tool.time.ran ?? tool.time.created - const state = (() => { - if (tool.state.status === "streaming") { - const value = Option.getOrUndefined(decodeToolInput(tool.state.input)) - const input = normalizeToolInput(tool.name, record(value) ? value : {}) - return { status: "pending" as const, input, raw: tool.state.input } - } - if (tool.state.status === "running") { - return { - status: "running" as const, - input: normalizeToolInput(tool.name, tool.state.input), - // metadata: normalizeToolMetadata(tool.name, tool.state.structured), - metadata: normalizeToolMetadata(tool.name, tool.state.metadata ?? {}), - time: { start }, - } - } - if (tool.state.status === "error") { - return { - status: "error" as const, - input: normalizeToolInput(tool.name, tool.state.input), - error: tool.state.error.message, - // metadata: normalizeToolMetadata(tool.name, tool.state.structured), - metadata: normalizeToolMetadata(tool.name, tool.state.metadata ?? {}), - time: { start, end: tool.time.completed ?? start }, - } - } - const attachments = tool.state.content.flatMap((item, index): FilePart[] => - item.type === "file" - ? [ - { - id: `${tool.id}:file:${index}`, - sessionID, - messageID, - type: "file", - mime: item.mime, - filename: item.name, - url: item.uri, - }, - ] - : [], - ) - return { - status: "completed" as const, - input: normalizeToolInput(tool.name, tool.state.input), - output: tool.state.content.flatMap((item) => (item.type === "text" ? [item.text] : [])).join("\n"), - title: tool.name, - // metadata: normalizeToolMetadata(tool.name, tool.state.structured), - metadata: normalizeToolMetadata(tool.name, tool.state.metadata ?? {}), - time: { start, end: tool.time.completed ?? start }, - attachments: attachments.length ? attachments : undefined, - } - })() - return { - id: tool.id, - sessionID, - messageID, - type: "tool", - callID: tool.id, - tool: tool.name, - state, - metadata: { providerState: tool.providerState, providerResultState: tool.providerResultState }, - } -} diff --git a/packages/app/src/utils/session.test.ts b/packages/app/src/utils/session.test.ts deleted file mode 100644 index b15c23b660..0000000000 --- a/packages/app/src/utils/session.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { SessionApi, SessionInfo, SessionListInput } from "@opencode-ai/client/promise" -import { listAllSessions, normalizeSessionInfo } from "./session" - -describe("normalizeSessionInfo", () => { - test("adapts a current session to the app session shape", () => { - const result = normalizeSessionInfo({ - id: "session-1", - projectID: "project-1", - agent: "build", - model: { id: "gpt-5", providerID: "openai", variant: "high" }, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: 1, updated: 1 }, - title: "New session", - location: { directory: "/repo/worktree", workspaceID: "workspace-1" }, - subpath: "worktree", - revert: { messageID: "message-1", partID: "part-1", snapshot: "snapshot", files: [] }, - } as SessionInfo) - - expect(result).toEqual({ - id: "session-1", - slug: "session-1", - projectID: "project-1", - workspaceID: "workspace-1", - directory: "/repo/worktree", - path: "worktree", - parentID: undefined, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - title: "New session", - agent: "build", - model: { id: "gpt-5", providerID: "openai", variant: "high" }, - version: "", - time: { created: 1, updated: 1 }, - revert: { messageID: "message-1", partID: "part-1", snapshot: "snapshot" }, - }) - }) -}) - -describe("listAllSessions", () => { - test("loads every page in server order and retains the query", async () => { - const calls: SessionListInput[] = [] - const pages = new Map([ - [undefined, { data: [sessionInfo("session-3"), sessionInfo("session-2")], cursor: { next: "next" } }], - ["next", { data: [sessionInfo("session-1", true)], cursor: {} }], - ]) - const api = { - list: async (query = {}) => { - calls.push(query) - return pages.get(query.cursor) ?? { data: [], cursor: {} } - }, - } satisfies Pick - - const result = await listAllSessions(api, { directory: "/repo", order: "desc" }) - - expect(result.map((session) => session.id)).toEqual(["session-3", "session-2", "session-1"]) - expect(result[2]?.time.archived).toBe(2) - expect(calls).toEqual([ - { directory: "/repo", order: "desc", limit: 100, cursor: undefined }, - { directory: "/repo", order: "desc", limit: 100, cursor: "next" }, - ]) - }) - - test("requests the terminal empty page when the server returns a next cursor", async () => { - const cursors: Array = [] - const api = { - list: async (query = {}) => { - cursors.push(query.cursor) - if (query.cursor) return { data: [], cursor: { next: "unused" } } - return { data: [sessionInfo("session-1")], cursor: { next: "terminal" } } - }, - } satisfies Pick - - const result = await listAllSessions(api, { directory: "/repo", limit: 25 }) - - expect(result.map((session) => session.id)).toEqual(["session-1"]) - expect(cursors).toEqual([undefined, "terminal"]) - }) -}) - -function sessionInfo(id: string, archived = false) { - return { - id, - projectID: "project-1", - agent: "build", - model: { id: "model-1", providerID: "provider-1" }, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: 1, updated: 1, archived: archived ? 2 : undefined }, - title: id, - location: { directory: "/repo" }, - } as SessionInfo -} diff --git a/packages/app/src/utils/session.ts b/packages/app/src/utils/session.ts deleted file mode 100644 index faf847967b..0000000000 --- a/packages/app/src/utils/session.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { SessionApi, SessionInfo, SessionListInput } from "@opencode-ai/client/promise" -import type { Session } from "@opencode-ai/sdk/v2/client" - -export function normalizeSessionInfo(input: SessionInfo | Session): Session { - if (!("location" in input)) return input - return { - id: input.id, - slug: input.id, - projectID: input.projectID, - workspaceID: input.location.workspaceID, - directory: input.location.directory, - path: input.subpath, - parentID: input.parentID, - cost: input.cost, - tokens: input.tokens, - title: input.title, - agent: input.agent, - model: input.model, - version: "", - time: input.time, - revert: input.revert && { - messageID: input.revert.messageID, - partID: input.revert.partID, - snapshot: input.revert.snapshot, - }, - } -} - -export async function listAllSessions(api: Pick, input: Omit) { - const load = async (cursor?: string): Promise => { - const result = await api.list({ ...input, limit: input.limit ?? 100, cursor }) - const sessions = result.data.map(normalizeSessionInfo) - if (result.data.length === 0 || !result.cursor.next) return sessions - return [...sessions, ...(await load(result.cursor.next))] - } - return load() -} diff --git a/packages/app/src/utils/terminal-websocket-url.test.ts b/packages/app/src/utils/terminal-websocket-url.test.ts index aac854ca82..5fa1506b1e 100644 --- a/packages/app/src/utils/terminal-websocket-url.test.ts +++ b/packages/app/src/utils/terminal-websocket-url.test.ts @@ -2,28 +2,8 @@ import { describe, expect, test } from "bun:test" import { terminalWebSocketURL } from "./terminal-websocket-url" describe("terminalWebSocketURL", () => { - test("uses the current ticketed PTY route", () => { + test("uses query auth without embedding credentials in websocket URL", () => { const url = terminalWebSocketURL({ - url: "http://127.0.0.1:49365", - id: "pty_test", - directory: "/tmp/project", - cursor: 0, - ticket: "connect-ticket", - }) - - expect(url.protocol).toBe("ws:") - expect(url.username).toBe("") - expect(url.password).toBe("") - expect(url.pathname).toBe("/api/pty/pty_test/connect") - expect(url.searchParams.get("location[directory]")).toBe("/tmp/project") - expect(url.searchParams.get("cursor")).toBe("0") - expect(url.searchParams.get("ticket")).toBe("connect-ticket") - expect(url.searchParams.has("auth_token")).toBe(false) - }) - - test("uses query auth without embedding credentials in websocket URL for v1", () => { - const url = terminalWebSocketURL({ - protocol: "v1", url: "http://127.0.0.1:49365", id: "pty_test", directory: "/tmp/project", @@ -36,14 +16,11 @@ describe("terminalWebSocketURL", () => { expect(url.protocol).toBe("ws:") expect(url.username).toBe("") expect(url.password).toBe("") - expect(url.pathname).toBe("/pty/pty_test/connect") - expect(url.searchParams.get("directory")).toBe("/tmp/project") expect(url.searchParams.get("auth_token")).toBe(btoa("opencode:secret")) }) - test("omits query auth for same-origin saved credentials for v1", () => { + test("omits query auth for same-origin saved credentials", () => { const url = terminalWebSocketURL({ - protocol: "v1", url: "https://app.example.test", id: "pty_test", directory: "/tmp/project", @@ -54,14 +31,11 @@ describe("terminalWebSocketURL", () => { }) expect(url.protocol).toBe("wss:") - expect(url.pathname).toBe("/pty/pty_test/connect") - expect(url.searchParams.get("directory")).toBe("/tmp/project") expect(url.searchParams.has("auth_token")).toBe(false) }) - test("uses query auth for same-origin credentials from auth_token for v1", () => { + test("uses query auth for same-origin credentials from auth_token", () => { const url = terminalWebSocketURL({ - protocol: "v1", url: "https://app.example.test", id: "pty_test", directory: "/tmp/project", @@ -73,8 +47,6 @@ describe("terminalWebSocketURL", () => { }) expect(url.protocol).toBe("wss:") - expect(url.pathname).toBe("/pty/pty_test/connect") - expect(url.searchParams.get("directory")).toBe("/tmp/project") expect(url.searchParams.get("auth_token")).toBe(btoa("opencode:secret")) }) }) diff --git a/packages/app/src/utils/terminal-websocket-url.ts b/packages/app/src/utils/terminal-websocket-url.ts index a32b239cc9..06facdc7d2 100644 --- a/packages/app/src/utils/terminal-websocket-url.ts +++ b/packages/app/src/utils/terminal-websocket-url.ts @@ -1,7 +1,6 @@ import { authTokenFromCredentials } from "@/utils/server" export function terminalWebSocketURL(input: { - protocol?: "v1" | "v2" url: string id: string directory: string @@ -12,24 +11,18 @@ export function terminalWebSocketURL(input: { password?: string authToken?: boolean }) { - const isV1 = input.protocol === "v1" - const next = new URL(`${input.url}${isV1 ? `/pty/${input.id}/connect` : `/api/pty/${input.id}/connect`}`) - if (isV1) { - next.searchParams.set("directory", input.directory) - } else { - next.searchParams.set("location[directory]", input.directory) - } + const next = new URL(`${input.url}/pty/${input.id}/connect`) + next.searchParams.set("directory", input.directory) next.searchParams.set("cursor", String(input.cursor)) next.protocol = next.protocol === "https:" ? "wss:" : "ws:" if (input.ticket) { next.searchParams.set("ticket", input.ticket) return next } - if (isV1 && input.password && (!input.sameOrigin || input.authToken)) { + if (input.password && (!input.sameOrigin || input.authToken)) next.searchParams.set( "auth_token", authTokenFromCredentials({ username: input.username, password: input.password }), ) - } return next } diff --git a/packages/app/src/utils/toast.tsx b/packages/app/src/utils/toast.tsx index 6f23b63d1e..e444548508 100644 --- a/packages/app/src/utils/toast.tsx +++ b/packages/app/src/utils/toast.tsx @@ -1,12 +1,6 @@ import { Icon, type IconProps } from "@opencode-ai/ui/icon" -import { - Toast, - showToast as showLegacyToast, - toaster as legacyToaster, - type ToastOptions, - type ToastVariant, -} from "@opencode-ai/ui/toast" -import { ToastV2, showToastV2, toasterV2 } from "@opencode-ai/ui/v2/toast-v2" +import { Toast, showToast as showLegacyToast, type ToastOptions, type ToastVariant } from "@opencode-ai/ui/toast" +import { ToastV2, showToastV2 } from "@opencode-ai/ui/v2/toast-v2" let v2 = false @@ -33,13 +27,6 @@ export function showToast(options: ToastOptions | string) { }) } -// v1 and v2 ids come from separate registries, so dismissal has to use the same -// implementation that issued the id. -export function dismissToast(toastId: number) { - if (!v2) return legacyToaster.dismiss(toastId) - return toasterV2.dismiss(toastId) -} - function resolveIcon(icon: IconProps["name"] | undefined, variant: ToastVariant | undefined) { const name = icon ?? (variant === "success" ? "check" : undefined) if (!name) return diff --git a/packages/app/test-browser/command-palette.test.ts b/packages/app/test-browser/command-palette.test.ts index 6a74834fd0..421a2e71fd 100644 --- a/packages/app/test-browser/command-palette.test.ts +++ b/packages/app/test-browser/command-palette.test.ts @@ -1,6 +1,5 @@ import { describe, expect, test } from "bun:test" -import type { Project } from "@opencode-ai/sdk/v2/client" -import type { SessionInfo } from "@opencode-ai/client/promise" +import type { GlobalSession, Project } from "@opencode-ai/sdk/v2/client" import { createRoot } from "solid-js" import { createServerSessionEntries } from "@/components/command-palette" import type { LocalProject } from "@/context/layout" @@ -15,16 +14,15 @@ const stored: Project = { time: { created: 1, updated: 1 }, } -const session: SessionInfo = { +const session: GlobalSession = { id: "session-1", + slug: "session-1", projectID: stored.id, - agent: "build", - model: { id: "model-1", providerID: "provider-1" }, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - location: { directory: stored.worktree }, + directory: stored.worktree, title: "Palette session", + version: "1", time: { created: 1, updated: 2 }, + project: { id: stored.id, name: stored.name, worktree: stored.worktree }, } describe("command palette sessions", () => { diff --git a/packages/app/test-browser/prompt-attachments.test.ts b/packages/app/test-browser/prompt-attachments.test.ts index 49dad279cd..ca7686598e 100644 --- a/packages/app/test-browser/prompt-attachments.test.ts +++ b/packages/app/test-browser/prompt-attachments.test.ts @@ -1,10 +1,7 @@ import { describe, expect, test } from "bun:test" import { createRoot } from "solid-js" -import { createStore } from "solid-js/store" import { createPromptAttachmentsCore } from "@/components/prompt-input/attachments" import { createPromptState } from "@/context/prompt" -import { createPromptInputV2Attachments } from "../../session-ui/src/v2/components/prompt-input/attachments" -import type { PromptInputV2Prompt } from "../../session-ui/src/v2/components/prompt-input/types" describe("prompt attachment session ownership", () => { test("adds an asynchronously read image to the session where the read started", async () => { @@ -88,88 +85,6 @@ describe("prompt attachment session ownership", () => { }) }) -test("rejects a duplicate native clipboard attachment in the V2 prompt store", async () => { - await createRoot(async (dispose) => { - const [state, setState] = createStore({ prompt: [] as PromptInputV2Prompt }) - const duplicate = Promise.withResolvers() - const files = [ - new File(["hello"], "clipboard-1.txt", { type: "text/plain" }), - new File(["hello"], "clipboard-2.txt", { type: "text/plain" }), - ] - const attachments = createPromptInputV2Attachments({ - capture: () => ({ - current: () => state.prompt, - cursor: () => 0, - set: (prompt) => setState("prompt", prompt), - }), - editor: () => document.createElement("div"), - focusEditor: () => undefined, - addPart: () => false, - setDraggingType: () => undefined, - directory: () => "/", - isDialogActive: () => false, - warn: () => undefined, - duplicate: duplicate.resolve, - onError: () => undefined, - readClipboardImage: async () => files.shift() ?? null, - }) - const event = { - clipboardData: { items: [], getData: () => "" }, - preventDefault: () => undefined, - stopPropagation: () => undefined, - } as unknown as ClipboardEvent - - await attachments.handlePaste(event) - await attachments.handlePaste(event) - await duplicate.promise - - expect(state.prompt).toHaveLength(1) - dispose() - }) -}) - -test("rejects desktop duplicates and keeps changed files in the V2 prompt store", async () => { - await createRoot(async (dispose) => { - const [state, setState] = createStore({ prompt: [] as PromptInputV2Prompt }) - const duplicates: string[] = [] - const attachments = createPromptInputV2Attachments({ - capture: () => ({ - current: () => state.prompt, - cursor: () => 0, - set: (prompt) => setState("prompt", prompt), - }), - editor: () => document.createElement("div"), - focusEditor: () => undefined, - addPart: () => false, - setDraggingType: () => undefined, - directory: () => "/", - isDialogActive: () => false, - warn: () => undefined, - duplicate: () => duplicates.push("duplicate"), - onError: () => undefined, - getPathForFile: (file) => (file.name === "browser.txt" ? "" : `/tmp/${file.name}`), - }) - const first = new File(["first"], "a.txt", { type: "text/plain" }) - const second = new File(["second"], "b.txt", { type: "text/plain" }) - - await attachments.addAttachments([first, second]) - await attachments.addAttachments([first, second]) - expect(state.prompt).toHaveLength(2) - expect(duplicates).toEqual(["duplicate", "duplicate"]) - - await attachments.addAttachments([new File(["edited"], "a.txt", { type: "text/plain" })]) - expect(state.prompt).toHaveLength(3) - - await attachments.addAttachments([ - new File(["same"], "browser.txt", { type: "text/plain" }), - new File(["same"], "browser.txt", { type: "text/plain" }), - ]) - expect(state.prompt).toHaveLength(4) - expect(duplicates).toHaveLength(3) - dispose() - }) -}) - function images(prompt: ReturnType) { return prompt.current().filter((part) => part.type === "image") } diff --git a/packages/app/test-browser/prompt-persistence.test.ts b/packages/app/test-browser/prompt-persistence.test.ts index f3d8f8ae4f..a7b08078d7 100644 --- a/packages/app/test-browser/prompt-persistence.test.ts +++ b/packages/app/test-browser/prompt-persistence.test.ts @@ -2,7 +2,6 @@ import { beforeAll, describe, expect, mock, test } from "bun:test" import type { AsyncStorage } from "@solid-primitives/storage" import { createEffect, createRoot } from "solid-js" import { ServerScope } from "@/utils/server-scope" -import { createDraftStore } from "@/utils/draft-store" let Prompt: typeof import("@/context/prompt") let read: ((value: string | null) => void) | undefined @@ -31,7 +30,7 @@ beforeAll(async () => { }), })) mock.module("@/context/platform", () => ({ - usePlatform: () => ({ platform: "desktop", storage: () => storage, draftStore: storage }), + usePlatform: () => ({ platform: "desktop", storage: () => storage }), })) Prompt = await import("@/context/prompt") @@ -70,50 +69,3 @@ describe("prompt persistence", () => { }) }) }) - -test("moves legacy image data URLs into blobs and hydrates object URLs", async () => { - const documents = new Map() - const blobs = new Map() - const store = createDraftStore({ - get: async (key) => documents.get(key) ?? null, - set: async (key, value) => void documents.set(key, value), - remove: async (key) => void documents.delete(key), - putBlob: async (blob) => { - const id = String(blob.size) - blobs.set(id, blob) - return id - }, - getBlob: async (id) => blobs.get(id) ?? null, - }) - - await store.setItem("prompt", JSON.stringify({ prompt: [{ type: "image", dataUrl: "data:image/png;base64,YQ==" }] })) - expect(documents.get("prompt")).not.toContain("dataUrl") - const value = JSON.parse((await store.getItem("prompt"))!) - expect(value.prompt[0].blob.id).toBe("1") - expect(value.prompt[0].blob.url).toStartWith("blob:") -}) - -test("does not let delayed blob migration overwrite a newer draft", async () => { - const documents = new Map() - const migration = Promise.withResolvers() - const store = createDraftStore({ - get: async () => null, - set: async (key, value) => void documents.set(key, value), - remove: async () => undefined, - putBlob: async () => { - await migration.promise - return "blob" - }, - getBlob: async () => null, - }) - const older = store.setItem( - "prompt", - JSON.stringify({ prompt: [{ type: "image", dataUrl: "data:image/png;base64,YQ==" }] }), - ) - await Bun.sleep(0) - await store.setItem("prompt", JSON.stringify({ prompt: [{ type: "text", content: "latest" }] })) - migration.resolve() - await older - - expect(documents.get("prompt")).toContain("latest") -}) diff --git a/packages/app/test-browser/prompt-transient-state.test.ts b/packages/app/test-browser/prompt-transient-state.test.ts index 5777c9c392..61b5c6a14f 100644 --- a/packages/app/test-browser/prompt-transient-state.test.ts +++ b/packages/app/test-browser/prompt-transient-state.test.ts @@ -18,6 +18,7 @@ test("resets transient prompt input state when the prompt session changes", () = draggingType: "image", mode: "shell", applyingHistory: true, + variantOpen: true, }) setIdentity("B") @@ -32,6 +33,7 @@ test("resets transient prompt input state when the prompt session changes", () = draggingType: null, mode: "normal", applyingHistory: false, + variantOpen: false, }) dispose() }) diff --git a/packages/app/test-browser/settings-keybinds.test.ts b/packages/app/test-browser/settings-keybinds.test.ts deleted file mode 100644 index 75f0b75543..0000000000 --- a/packages/app/test-browser/settings-keybinds.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { createRoot } from "solid-js" -import { createKeybindSettingsController } from "../src/components/settings-keybinds" - -function setup(overrides: Record = {}) { - const changes: [string, string][] = [] - const suppression: boolean[] = [] - const notifications: { title: string; description?: string }[] = [] - let resets = 0 - let controller: ReturnType - - const dispose = createRoot((dispose) => { - controller = createKeybindSettingsController( - { - command: { - catalog: [ - { id: "session.alpha", title: "Alpha", keybind: "mod+a" }, - { id: "session.beta", title: "Beta", keybind: "mod+b" }, - ], - options: [], - keybinds: (enabled) => suppression.push(enabled), - }, - settings: { - current: { keybinds: overrides }, - keybinds: { - get: (id) => overrides[id], - set: (id, value) => { - overrides[id] = value - changes.push([id, value]) - }, - resetAll: () => { - resets++ - }, - }, - }, - notify: (toast) => notifications.push(toast), - }, - { - locale: () => "en", - t: (key, params) => { - if (params) return `${key}:${Object.values(params).join("|")}` - if (key === "common.key.alt") return "Alt" - return String(key) - }, - }, - ) - return dispose - }) - - return { - controller: controller!, - changes, - suppression, - notifications, - resets: () => resets, - dispose, - } -} - -function modKey(key: string) { - const mac = /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) - return new KeyboardEvent("keydown", { key, ctrlKey: !mac, metaKey: mac, bubbles: true, cancelable: true }) -} - -describe("keybind settings controller", () => { - test("derives the catalog, effective bindings, and filtered groups", () => { - const state = setup({ "session.beta": "alt+k" }) - - expect(state.controller.catalog.title("session.alpha")).toBe("Alpha") - expect(state.controller.catalog.keybind("session.beta")).toBe("Alt+K") - expect(state.controller.catalog.filtered("alt k").get("Session")).toEqual(["session.beta"]) - expect(state.controller.settings.hasOverrides()).toBe(true) - - state.dispose() - }) - - test("captures bindings, rejects conflicts, and restores command handling", () => { - const state = setup() - - state.controller.capture.toggle("session.beta") - document.dispatchEvent(modKey("a")) - expect(state.changes).toEqual([]) - expect(state.notifications).toHaveLength(1) - expect(state.controller.capture.active()).toBe("session.beta") - - document.dispatchEvent(modKey("x")) - expect(state.changes).toEqual([["session.beta", "mod+x"]]) - expect(state.suppression).toEqual([false, true]) - expect(state.controller.capture.active()).toBeNull() - - state.controller.capture.toggle("session.alpha") - state.dispose() - expect(state.suppression).toEqual([false, true, false, true]) - document.dispatchEvent(modKey("z")) - expect(state.changes).toEqual([["session.beta", "mod+x"]]) - }) - - test("resets persisted overrides and reports success", () => { - const state = setup({ "session.alpha": "none" }) - - state.controller.settings.reset() - expect(state.resets()).toBe(1) - expect(state.notifications[0]?.title).toBe("settings.shortcuts.reset.toast.title") - - state.dispose() - }) -}) diff --git a/packages/app/test-browser/solid-router-cleanup.test.ts b/packages/app/test-browser/solid-router-cleanup.test.ts deleted file mode 100644 index 5795fa5648..0000000000 --- a/packages/app/test-browser/solid-router-cleanup.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { expect, test } from "bun:test" -import { MetaProvider, Title } from "@solidjs/meta" -import { MemoryRouter, Route, createMemoryHistory, useParams } from "@solidjs/router" -import { createMemo } from "solid-js" -import { createComponent, render } from "solid-js/web" - -test("route cleanup cannot invalidate an owner list being disposed", async () => { - const host = document.createElement("div") - document.body.append(host) - const history = createMemoryHistory() - - const RepoPage = () => { - const params = useParams<{ id?: string }>() - const title = createMemo(() => params.id ?? "") - const button = document.createElement("button") - button.textContent = "Back" - button.addEventListener("click", () => history.set({ value: "/", scroll: false, replace: false })) - return [ - createComponent(Title, { - get children() { - return title() - }, - }), - button, - ] - } - - const HomePage = () => { - const button = document.createElement("button") - button.textContent = "Go" - button.addEventListener("click", () => history.set({ value: "/project", scroll: false, replace: false })) - return button - } - - const App = () => - createComponent(MetaProvider, { - get children() { - return createComponent(MemoryRouter, { - history, - get children() { - return [ - createComponent(Route, { path: "/", component: HomePage }), - createComponent(Route, { path: "/:id", component: RepoPage }), - ] - }, - }) - }, - }) - - const dispose = render(() => createComponent(App, {}), host) - const go = host.querySelector("button") - expect(go?.textContent).toBe("Go") - go?.click() - await new Promise((resolve) => setTimeout(resolve, 0)) - - const back = host.querySelector("button") - expect(back?.textContent).toBe("Back") - back?.click() - await new Promise((resolve) => setTimeout(resolve, 0)) - - expect(host.querySelector("button")?.textContent).toBe("Go") - dispose() - host.remove() -}) diff --git a/packages/app/test-browser/toast-owner.test.ts b/packages/app/test-browser/toast-owner.test.ts index 25ba5c000e..104a505851 100644 --- a/packages/app/test-browser/toast-owner.test.ts +++ b/packages/app/test-browser/toast-owner.test.ts @@ -1,46 +1,8 @@ -import { beforeEach, describe, expect, test } from "bun:test" +import { describe, expect, test } from "bun:test" import { createSignal, type JSX } from "solid-js" import { showToastV2, toasterV2 } from "@opencode-ai/ui/v2/toast-v2" describe("showToastV2", () => { - // The toast registry is module state, so each test starts from an empty stack. - beforeEach(() => { - toasterV2.dismiss() - }) - - test("coalesces exact active content", () => { - const first = showToastV2({ title: "Repeated error", description: "Try again" }) - const second = showToastV2({ title: "Repeated error", description: "Try again" }) - const different = showToastV2({ title: "Repeated error", description: "A different error" }) - - expect(second).toBe(first) - expect(different).not.toBe(first) - - toasterV2.dismiss(first) - toasterV2.dismiss(different) - }) - - test("allows dismissed content to appear again", () => { - const first = showToastV2("Dismiss and retry") - toasterV2.dismiss(first) - - const second = showToastV2("Dismiss and retry") - expect(second).not.toBe(first) - - toasterV2.dismiss(second) - }) - - test("recreates matching content when it is not the topmost toast", () => { - const first = showToastV2("First toast") - const topmost = showToastV2("Topmost toast") - const repeated = showToastV2("First toast") - - expect(repeated).not.toBe(first) - - toasterV2.dismiss(topmost) - toasterV2.dismiss(repeated) - }) - test("creates no reactive computations at call time", () => { const [tick, setTick] = createSignal(0) let reads = 0 diff --git a/packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz b/packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz deleted file mode 100644 index bc1b664f82..0000000000 Binary files a/packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz and /dev/null differ diff --git a/packages/cli/AGENTS.md b/packages/cli/AGENTS.md new file mode 100644 index 0000000000..73bebeb94f --- /dev/null +++ b/packages/cli/AGENTS.md @@ -0,0 +1,7 @@ +# V2 CLI and TUI development guide + +## Migration context + +- The TUI is being ported from legacy APIs to the new V2 APIs. New and migrated TUI behavior should use `sdk.client.v2` and the location-scoped data in `packages/tui/src/context/data.tsx` instead of adding dependencies on legacy sync state. +- Preserve established TUI behavior unless the task intentionally changes it. +- Load the `opencode-dev` skill before interactively running, debugging, or verifying opencode's V2 CLI, TUI, or server. diff --git a/packages/cli/bin/lildax.cjs b/packages/cli/bin/opencode2.cjs old mode 100644 new mode 100755 similarity index 90% rename from packages/cli/bin/lildax.cjs rename to packages/cli/bin/opencode2.cjs index ab99b84b0f..d3f1c9260d --- a/packages/cli/bin/lildax.cjs +++ b/packages/cli/bin/opencode2.cjs @@ -31,11 +31,13 @@ function run(target) { const envPath = process.env.OPENCODE_BIN_PATH const scriptDir = path.dirname(fs.realpathSync(__filename)) -const cached = path.join(scriptDir, ".lildax") +const command = path.basename(__filename).replace(/\.cjs$/, "") +const nodeBuild = command === "opencode2-node" +const cached = path.join(scriptDir, `.${command}`) const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] || os.platform() const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch() -const base = "@opencode-ai/cli-" + platform + "-" + arch -const binary = platform === "windows" ? "lildax.exe" : "lildax" +const base = `@opencode-ai/cli${nodeBuild ? "-node" : ""}-` + platform + "-" + arch +const binary = platform === "windows" ? `${command}.exe` : command function supportsAvx2() { if (arch !== "x64") return false @@ -77,6 +79,7 @@ function supportsAvx2() { } const names = (() => { + if (nodeBuild) return [base] const baseline = arch === "x64" && !supportsAvx2() if (platform === "linux") { const musl = (() => { @@ -121,7 +124,7 @@ function findBinary(startDir) { const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir)) if (!resolved) { console.error( - "It seems that your package manager failed to install the right lildax CLI package. Try manually installing " + + `It seems that your package manager failed to install the right ${command} CLI package. Try manually installing ` + names.map((name) => `"${name}"`).join(" or ") + " package", ) diff --git a/packages/cli/bunfig.toml b/packages/cli/bunfig.toml index 7693482f3b..b16283cb5b 100644 --- a/packages/cli/bunfig.toml +++ b/packages/cli/bunfig.toml @@ -1 +1,4 @@ preload = ["@opentui/solid/preload"] + +[test] +preload = ["@opentui/solid/preload"] diff --git a/packages/cli/package.json b/packages/cli/package.json index be5d853de0..4859175a01 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,36 +1,79 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/cli", - "version": "1.18.11", + "version": "1.18.3", "type": "module", "license": "MIT", "bin": { - "lildax": "./bin/lildax.cjs" + "opencode2": "./bin/opencode2.cjs" }, "files": [ "bin" ], + "exports": { + "./daemon": "./src/daemon.ts", + "./mini": "./src/mini/index.ts", + "./mini/footer.command": "./src/mini/footer.command.tsx", + "./mini/footer.menu": "./src/mini/footer.menu.tsx", + "./mini/footer.permission": "./src/mini/footer.permission.tsx", + "./mini/footer.prompt": "./src/mini/footer.prompt.tsx", + "./mini/footer.question": "./src/mini/footer.question.tsx", + "./mini/footer.subagent": "./src/mini/footer.subagent.tsx", + "./mini/footer.view": "./src/mini/footer.view.tsx", + "./mini/scrollback.writer": "./src/mini/scrollback.writer.tsx", + "./mini/*": "./src/mini/*.ts", + "./server-process": "./src/server-process.ts" + }, "scripts": { "build": "bun run script/build.ts", + "build:node": "bun run script/build-node.ts", "dev": "bun run src/index.ts", + "test": "bun test --timeout 30000 --only-failures", "typecheck": "tsgo --noEmit" }, "dependencies": { "@effect/platform-node": "catalog:", + "@opencode-ai/client": "workspace:*", "@opencode-ai/core": "workspace:*", - "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/plugin": "workspace:*", + "@opencode-ai/schema": "workspace:*", "@opencode-ai/server": "workspace:*", "@opencode-ai/tui": "workspace:*", "@opentui/core": "catalog:", + "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", "@parcel/watcher": "2.5.1", "effect": "catalog:", - "solid-js": "catalog:" + "fuzzysort": "catalog:", + "immer": "11.1.4", + "jsonc-parser": "3.3.1", + "open": "10.1.2", + "opentui-spinner": "catalog:", + "semver": "catalog:", + "solid-js": "catalog:", + "strip-ansi": "7.1.2", + "uqr": "0.1.3", + "ws": "8.21.0" }, "devDependencies": { "@opencode-ai/script": "workspace:*", + "@opencode-ai/protocol": "workspace:*", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", - "@typescript/native-preview": "catalog:" + "@types/semver": "catalog:", + "@typescript/native-preview": "catalog:", + "@lydell/node-pty-darwin-arm64": "1.2.0-beta.12", + "@lydell/node-pty-darwin-x64": "1.2.0-beta.12", + "@lydell/node-pty-linux-arm64": "1.2.0-beta.12", + "@lydell/node-pty-linux-x64": "1.2.0-beta.12", + "@lydell/node-pty-win32-arm64": "1.2.0-beta.12", + "@lydell/node-pty-win32-x64": "1.2.0-beta.12", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1", + "vite": "catalog:", + "vite-plugin-solid": "catalog:" } } diff --git a/packages/cli/script/build-node.ts b/packages/cli/script/build-node.ts new file mode 100644 index 0000000000..9a67fd35d2 --- /dev/null +++ b/packages/cli/script/build-node.ts @@ -0,0 +1,203 @@ +#!/usr/bin/env bun + +import { spawnSync } from "node:child_process" +import { createHash } from "node:crypto" +import { chmod, copyFile, mkdir, mkdtemp, realpath, rename, rm, stat, writeFile } from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { build } from "vite" +import { Script } from "@opencode-ai/script" +import pkg from "../package.json" +import { modelsData } from "./generate" +import { collectNodeAssets, copyNodeAssets, hashNodeAssets, seaAssetMap } from "./node-assets" +import { mainConfig } from "../vite.node.config" +import { nodeExecArgv, nodeTarget, type NodeTarget } from "../src/node/target" + +const NODE_VERSION = "26.4.0" +const dir = path.resolve(import.meta.dirname, "..") +const outdir = path.resolve( + dir, + process.argv.find((arg) => arg.startsWith("--outdir="))?.slice("--outdir=".length) ?? "dist", +) +if (outdir === dir) throw new Error("--outdir must not be the package directory") +if (outdir === path.join(dir, "dist-node")) { + throw new Error("--outdir must not be dist-node because it contains temporary files") +} +const bundleOnly = process.argv.includes("--bundle-only") +const single = process.argv.includes("--single") +const skipInstall = process.argv.includes("--skip-install") +const requested = process.argv.find((arg) => arg.startsWith("--target="))?.slice("--target=".length) +const allTargets = [ + nodeTarget("linux", "arm64"), + nodeTarget("linux", "x64"), + nodeTarget("darwin", "arm64"), + nodeTarget("win32", "arm64"), + nodeTarget("win32", "x64"), +] +const targets = requested + ? allTargets.filter((target) => targetName(target) === requested) + : single || bundleOnly + ? [nodeTarget(process.platform, process.arch)] + : allTargets + +if (targets.length === 0) { + if (requested === "darwin-x64") throw new Error("Node 26.4 SEA does not support macOS x64") + throw new Error(`Unknown Node target: ${requested}`) +} +if (!bundleOnly && targets.some((target) => target.platform === "darwin" && target.arch === "x64")) { + throw new Error("Node 26.4 SEA does not support macOS x64") +} + +process.chdir(dir) +if (!skipInstall) run(process.execPath, ["install", "--os=*", "--cpu=*"]) +if (!bundleOnly) await rm(outdir, { recursive: true, force: true }) +const builder = + !bundleOnly || targets.some((target) => target.platform === process.platform && target.arch === process.arch) + ? await resolveHostNode() + : undefined + +for (const target of targets) { + console.log(`building cli-node-${targetName(target)}`) + const assets = await collectNodeAssets(target) + await rm("dist-node", { recursive: true, force: true }) + const assetHash = await hashNodeAssets(assets) + const input = { version: Script.version, channel: Script.channel, models: modelsData, assetHash, target } + await build(mainConfig(input)) + await copyNodeAssets(assets) + + const host = target.platform === process.platform && target.arch === process.arch + if (host) { + if (!builder) throw new Error("Node SEA builder is unavailable") + run(builder, [...nodeExecArgv, "dist-node/opencode.mjs", "--version"]) + run(builder, [...nodeExecArgv, "dist-node/opencode.mjs", "--help"]) + } + if (bundleOnly) continue + + const name = `cli-node-${targetName(target)}` + const binary = target.platform === "win32" ? "opencode2-node.exe" : "opencode2-node" + const output = path.join(outdir, name, "bin", binary) + if (!builder) throw new Error("Node SEA builder is unavailable") + await mkdir(path.dirname(output), { recursive: true }) + const config = { + main: "dist-node/opencode.mjs", + mainFormat: "module", + executable: await resolveTargetNode(target, builder), + output: path.relative(dir, output), + disableExperimentalSEAWarning: true, + useSnapshot: false, + useCodeCache: false, + execArgv: nodeExecArgv, + execArgvExtension: "none", + assets: await seaAssetMap(), + } + await writeFile("dist-node/sea.json", `${JSON.stringify(config, null, 2)}\n`) + run(builder, ["--build-sea", "dist-node/sea.json"]) + if (target.platform !== "win32") await chmod(output, 0o755) + if (target.platform === "darwin" && process.platform === "darwin") run("codesign", ["--sign", "-", output]) + if (target.platform === "darwin" && process.platform !== "darwin") { + console.warn(`${output} must be signed on macOS before it can run`) + } + await writeFile( + path.join(outdir, name, "package.json"), + `${JSON.stringify( + { + name: `@opencode-ai/${name}`, + version: Script.version, + license: pkg.license, + repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" }, + os: [target.platform], + cpu: [target.arch], + }, + null, + 2, + )}\n`, + ) + if (host) await smoke(output) +} + +async function resolveHostNode() { + const candidates = [process.env.NODE_BIN, "node"].filter((item): item is string => Boolean(item)) + for (const candidate of candidates) { + const result = spawnSync( + candidate, + ["-p", "JSON.stringify({version:process.versions.node,path:process.execPath})"], + { + encoding: "utf8", + }, + ) + if (result.status !== 0) continue + const info = JSON.parse(result.stdout) as { version: string; path: string } + if (info.version === NODE_VERSION) return realpath(info.path) + } + return resolveTargetNode(nodeTarget(process.platform, process.arch)) +} + +async function resolveTargetNode(target: NodeTarget, host?: string) { + if (host && target.platform === process.platform && target.arch === process.arch) return host + const cache = path.resolve(dir, ".cache", "node") + const platform = target.platform === "win32" ? "win" : target.platform + const archiveName = `node-v${NODE_VERSION}-${platform}-${target.arch}` + const targetDirectory = path.join(cache, archiveName) + const executable = path.join(targetDirectory, target.platform === "win32" ? "node.exe" : "bin/node") + if ( + (await stat(executable).then( + () => true, + () => false, + )) && + (await stat(path.join(targetDirectory, ".verified")).then( + () => true, + () => false, + )) + ) + return realpath(executable) + await mkdir(cache, { recursive: true }) + const extension = target.platform === "win32" ? "zip" : "tar.gz" + const filename = `${archiveName}.${extension}` + const archive = path.join(cache, filename) + const base = `https://nodejs.org/dist/v${NODE_VERSION}` + const [response, sums] = await Promise.all([fetch(`${base}/${filename}`), fetch(`${base}/SHASUMS256.txt`)]) + if (!response.ok) throw new Error(`Failed to download Node ${NODE_VERSION}: ${response.status}`) + if (!sums.ok) throw new Error(`Failed to download Node ${NODE_VERSION} checksums: ${sums.status}`) + const data = new Uint8Array(await response.arrayBuffer()) + const expected = (await sums.text()) + .split("\n") + .find((line) => line.endsWith(` ${filename}`)) + ?.split(/\s+/)[0] + if (!expected) throw new Error(`Missing checksum for ${filename}`) + if (createHash("sha256").update(data).digest("hex") !== expected) throw new Error(`Checksum mismatch for ${filename}`) + await writeFile(archive, data) + const temporary = path.join(cache, `${archiveName}.${process.pid}.tmp`) + await rm(temporary, { recursive: true, force: true }) + await mkdir(temporary) + if (target.platform !== "win32") run("tar", ["-xzf", archive, "-C", temporary]) + if (target.platform === "win32" && process.platform === "win32") { + run(path.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe"), ["-xf", archive, "-C", temporary]) + } + if (target.platform === "win32" && process.platform !== "win32") run("unzip", ["-q", archive, "-d", temporary]) + await rm(targetDirectory, { recursive: true, force: true }) + await rename(path.join(temporary, archiveName), targetDirectory) + await writeFile(path.join(targetDirectory, ".verified"), `${expected}\n`) + await rm(temporary, { recursive: true, force: true }) + await rm(archive, { force: true }) + return realpath(executable) +} + +async function smoke(output: string) { + const root = await mkdtemp(path.join(os.tmpdir(), "opencode-node-smoke-")) + const executable = path.join(root, path.basename(output)) + await copyFile(output, executable) + if (process.platform !== "win32") await chmod(executable, 0o755) + run(executable, ["--version"], root) + run(executable, ["--help"], root) + await rm(root, { recursive: true, force: true }) +} + +function targetName(target: NodeTarget) { + return `${target.platform === "win32" ? "windows" : target.platform}-${target.arch}` +} + +function run(command: string, args: readonly string[], cwd = dir) { + const result = spawnSync(command, args, { cwd, stdio: "inherit", env: process.env }) + if (result.error) throw result.error + if (result.status !== 0) throw new Error(`${command} exited with status ${result.status ?? "unknown"}`) +} diff --git a/packages/cli/script/build.ts b/packages/cli/script/build.ts index f42d8b07b0..ca3b84bb06 100755 --- a/packages/cli/script/build.ts +++ b/packages/cli/script/build.ts @@ -9,15 +9,19 @@ import pkg from "../package.json" import { modelsData } from "./generate" const dir = path.resolve(import.meta.dirname, "..") -const binary = "lildax" +const binary = "opencode2" +const outdir = path.resolve( + dir, + process.argv.find((arg) => arg.startsWith("--outdir="))?.slice("--outdir=".length) ?? "dist", +) +if (outdir === dir) throw new Error("--outdir must not be the package directory") process.chdir(dir) -await rm("dist", { recursive: true, force: true }) +await rm(outdir, { recursive: true, force: true }) const singleFlag = process.argv.includes("--single") const baselineFlag = process.argv.includes("--baseline") const skipInstall = process.argv.includes("--skip-install") -const sourcemapsFlag = process.argv.includes("--sourcemaps") const plugin = createSolidTransformPlugin() const allTargets: { @@ -69,7 +73,7 @@ for (const item of targets) { external: ["node-gyp"], format: "esm", minify: true, - sourcemap: sourcemapsFlag ? "linked" : "none", + sourcemap: "inline", splitting: true, compile: { autoloadBunfig: false, @@ -77,7 +81,7 @@ for (const item of targets) { autoloadTsconfig: true, autoloadPackageJson: true, target: target.replace(binary, "bun") as Bun.Build.CompileTarget, - outfile: `./dist/${name}/bin/${binary}`, + outfile: path.join(outdir, name, "bin", binary), execArgv: [`--user-agent=${binary}/${Script.version}`, "--use-system-ca", "--"], windows: {}, }, @@ -99,7 +103,7 @@ for (const item of targets) { } await Bun.write( - `./dist/${name}/package.json`, + path.join(outdir, name, "package.json"), JSON.stringify( { name: `@opencode-ai/${name}`, diff --git a/packages/cli/script/generate.ts b/packages/cli/script/generate.ts index e162f2ea7e..b8151b46cf 100755 --- a/packages/cli/script/generate.ts +++ b/packages/cli/script/generate.ts @@ -1,7 +1,9 @@ -const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.opencode.ai" +import { readFile } from "node:fs/promises" + +const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev" export const modelsData = process.env.MODELS_DEV_API_JSON - ? await Bun.file(process.env.MODELS_DEV_API_JSON).text() + ? await readFile(process.env.MODELS_DEV_API_JSON, "utf8") : await fetch(`${modelsUrl}/api.json`).then((response) => response.text()) console.log("Loaded models.dev snapshot") diff --git a/packages/cli/script/node-assets.ts b/packages/cli/script/node-assets.ts new file mode 100644 index 0000000000..749a0f74c2 --- /dev/null +++ b/packages/cli/script/node-assets.ts @@ -0,0 +1,83 @@ +import { createHash } from "node:crypto" +import { copyFile, mkdir, readdir, readFile, stat } from "node:fs/promises" +import { createRequire } from "node:module" +import path from "node:path" +import { fileURLToPath } from "node:url" +import { getNodeAssets } from "@opentui/core/node-assets" +import { attentionSoundAssets, type NodeTarget, photonWasmAsset } from "../src/node/target" + +const dir = path.resolve(import.meta.dirname, "..") + +// Bun's compiler discovers file imports and embeds them in its virtual filesystem. Vite only bundles the JavaScript +// portion of the Node executable, while SEA embeds only the assets explicitly listed in its build configuration. +// Collect and stage those files under stable keys so the SEA prelude can extract them to real paths at startup; +// native addons, helper executables, and other path-based consumers cannot use assets directly from SEA memory. +export type NodeAsset = { + readonly key: string + readonly source: string +} + +async function files(root: string, current = root): Promise { + return ( + await Promise.all( + (await readdir(current, { withFileTypes: true })).map((entry) => { + const target = path.join(current, entry.name) + return entry.isDirectory() ? files(root, target) : [path.relative(root, target)] + }), + ) + ).flat() +} + +export async function collectNodeAssets(target: NodeTarget) { + const ptyEntry = fileURLToPath(import.meta.resolve(target.nodePtyPackage)) + const ptyRoot = path.resolve(path.dirname(ptyEntry), "..") + const assets: NodeAsset[] = [ + ...getNodeAssets({ + platform: target.platform, + arch: target.arch, + ...(target.platform === "linux" ? { libc: "glibc" as const } : {}), + }), + { key: target.parcelWatcherAsset, source: fileURLToPath(import.meta.resolve(target.parcelWatcherPackage)) }, + { + key: photonWasmAsset, + source: createRequire(path.resolve(dir, "../core/package.json")).resolve(photonWasmAsset), + }, + ...attentionSoundAssets.map((key) => ({ + key, + source: path.resolve(dir, "../ui/src/assets/audio", path.basename(key)), + })), + ...(await files(ptyRoot)) + .filter((relative) => !relative.endsWith(".map") && !relative.endsWith(".pdb")) + .map((relative) => ({ + key: `${target.nodePtyPackage}/${relative}`, + source: path.join(ptyRoot, relative), + })), + ] + await Promise.all(assets.map((asset) => stat(asset.source))) + return assets +} + +export async function hashNodeAssets(assets: readonly NodeAsset[]) { + const hash = createHash("sha256") + for (const asset of assets.toSorted((left, right) => left.key.localeCompare(right.key))) { + hash.update(asset.key) + hash.update(await readFile(asset.source)) + } + return hash.digest("hex").slice(0, 16) +} + +export async function copyNodeAssets(assets: readonly NodeAsset[]) { + const root = path.join(dir, "dist-node", "assets") + await Promise.all( + assets.map(async (asset) => { + const target = path.join(root, asset.key) + await mkdir(path.dirname(target), { recursive: true }) + await copyFile(asset.source, target) + }), + ) +} + +export async function seaAssetMap() { + const root = path.join(dir, "dist-node", "assets") + return Object.fromEntries((await files(root)).map((key) => [key.replaceAll(path.sep, "/"), path.join(root, key)])) +} diff --git a/packages/cli/script/postinstall.mjs b/packages/cli/script/postinstall.mjs new file mode 100644 index 0000000000..c8619251cc --- /dev/null +++ b/packages/cli/script/postinstall.mjs @@ -0,0 +1,161 @@ +#!/usr/bin/env node + +import childProcess from "node:child_process" +import fs from "node:fs" +import os from "node:os" +import path from "node:path" +import { createRequire } from "node:module" +import { fileURLToPath } from "node:url" + +const directory = path.dirname(fileURLToPath(import.meta.url)) +const require = createRequire(import.meta.url) +const packageJson = JSON.parse(fs.readFileSync(path.join(directory, "package.json"), "utf8")) +const command = Object.keys(packageJson.bin ?? {})[0] +if (!command) throw new Error("OpenCode package does not declare a binary") + +const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] ?? os.platform() +const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] ?? os.arch() +const sourceBinary = platform === "windows" ? `${command}.exe` : command +const targetBinary = path.resolve(directory, packageJson.bin[command]) +const dependencies = packageJson.optionalDependencies ?? {} +const base = Object.keys(dependencies).find((name) => name.endsWith(`-${platform}-${arch}`)) +if (!base) throw new Error(`OpenCode does not provide a binary for ${platform}-${arch}`) + +function supportsAvx2() { + if (arch !== "x64") return false + if (platform === "linux") { + try { + return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8")) + } catch { + return false + } + } + if (platform === "darwin") { + try { + const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], { + encoding: "utf8", + timeout: 1500, + }) + return result.status === 0 && (result.stdout || "").trim() === "1" + } catch { + return false + } + } + if (platform === "windows") { + const script = + '(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)' + for (const executable of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) { + try { + const result = childProcess.spawnSync(executable, ["-NoProfile", "-NonInteractive", "-Command", script], { + encoding: "utf8", + timeout: 3000, + windowsHide: true, + }) + if (result.status !== 0) continue + const output = (result.stdout || "").trim().toLowerCase() + if (output === "true" || output === "1") return true + if (output === "false" || output === "0") return false + } catch { + continue + } + } + } + return false +} + +function isMusl() { + if (platform !== "linux") return false + try { + if (fs.existsSync("/etc/alpine-release")) return true + const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" }) + return `${result.stdout || ""}${result.stderr || ""}`.toLowerCase().includes("musl") + } catch { + return false + } +} + +function packageNames() { + const baseline = arch === "x64" && !supportsAvx2() + const names = + platform === "linux" + ? isMusl() + ? arch === "x64" + ? baseline + ? [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base] + : [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`] + : [`${base}-musl`, base] + : arch === "x64" + ? baseline + ? [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`] + : [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`] + : [base, `${base}-musl`] + : arch === "x64" + ? baseline + ? [`${base}-baseline`, base] + : [base, `${base}-baseline`] + : [base] + return names.filter((name) => dependencies[name]) +} + +function copyBinary(source) { + if (!fs.existsSync(source)) throw new Error(`Binary not found at ${source}`) + fs.mkdirSync(path.dirname(targetBinary), { recursive: true }) + if (fs.existsSync(targetBinary)) fs.unlinkSync(targetBinary) + try { + fs.linkSync(source, targetBinary) + } catch { + fs.copyFileSync(source, targetBinary) + } + fs.chmodSync(targetBinary, 0o755) +} + +function resolveBinary(name) { + const packagePath = require.resolve(`${name}/package.json`) + return path.join(path.dirname(packagePath), "bin", sourceBinary) +} + +function installPackage(name) { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-install-")) + try { + const result = childProcess.spawnSync( + "npm", + ["install", "--ignore-scripts", "--no-save", "--loglevel=error", "--prefix", temp, `${name}@${dependencies[name]}`], + { stdio: "inherit", windowsHide: true }, + ) + if (result.status !== 0) return false + copyBinary(path.join(temp, "node_modules", name, "bin", sourceBinary)) + return true + } finally { + fs.rmSync(temp, { recursive: true, force: true }) + } +} + +function verifyBinary() { + return ( + childProcess.spawnSync(targetBinary, ["--version"], { + stdio: "ignore", + windowsHide: true, + }).status === 0 + ) +} + +function main() { + const names = packageNames() + for (const name of names) { + try { + copyBinary(resolveBinary(name)) + if (verifyBinary()) return + } catch { + if (installPackage(name) && verifyBinary()) return + } + } + + throw new Error(`Failed to install OpenCode. Try manually installing ${names.map((name) => JSON.stringify(name)).join(" or ")}.`) +} + +try { + main() +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)) + process.exit(1) +} diff --git a/packages/cli/script/publish.ts b/packages/cli/script/publish.ts index d2855413ca..fd0f5cdb18 100755 --- a/packages/cli/script/publish.ts +++ b/packages/cli/script/publish.ts @@ -18,36 +18,66 @@ async function publish(dir: string, name: string, version: string) { await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir) } -const binaries: Record = {} -for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" })) { - const item = await Bun.file(`./dist/${filepath}`).json() - binaries[item.name] = item.version +async function publishDistribution(input: { root: string; name: string; binary: string; packagePrefix: string }) { + const binaries: Record = {} + for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: input.root })) { + const item = await Bun.file(`${input.root}/${filepath}`).json() + if (!item.name.startsWith(input.packagePrefix)) continue + binaries[item.name] = item.version + } + console.log(input.name, "binaries", binaries) + const versions = new Set(Object.values(binaries)) + if (versions.size > 1) throw new Error(`Binary package versions do not match for ${input.name}`) + const version = versions.values().next().value + if (!version) throw new Error(`No binary packages found for ${input.name}`) + + await $`mkdir -p ${input.root}/${input.name}/bin` + await $`cp ./script/postinstall.mjs ${input.root}/${input.name}/postinstall.mjs` + await Bun.file(`${input.root}/${input.name}/bin/${input.binary}.exe`).write( + [ + `echo "Error: ${input.name}'s postinstall script was not run." >&2`, + 'echo "" >&2', + 'echo "This occurs when installation scripts are disabled." >&2', + 'echo "Run the package postinstall script or reinstall with scripts enabled." >&2', + "exit 1", + "", + ].join("\n"), + ) + await Bun.file(`${input.root}/${input.name}/package.json`).write( + JSON.stringify( + { + name: input.name, + bin: { [input.binary]: `./bin/${input.binary}.exe` }, + scripts: { postinstall: "node ./postinstall.mjs" }, + version, + license: pkg.license, + repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" }, + os: ["darwin", "linux", "win32"], + cpu: ["arm64", "x64"], + optionalDependencies: binaries, + }, + null, + 2, + ), + ) + + await Promise.all( + Object.entries(binaries).map(([name, version]) => + publish(`${input.root}/${name.replace("@opencode-ai/", "")}`, name, version), + ), + ) + await publish(`${input.root}/${input.name}`, input.name, version) } -console.log("binaries", binaries) -const version = Object.values(binaries)[0] -await $`mkdir -p ./dist/${pkg.name}/bin` -await $`cp ./bin/lildax.cjs ./dist/${pkg.name}/bin/lildax` -await Bun.file(`./dist/${pkg.name}/package.json`).write( - JSON.stringify( - { - name: pkg.name, - bin: { lildax: "./bin/lildax" }, - version, - license: pkg.license, - repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" }, - os: ["darwin", "linux", "win32"], - cpu: ["arm64", "x64"], - optionalDependencies: binaries, - }, - null, - 2, - ), -) - -await Promise.all( - Object.entries(binaries).map(([name, version]) => - publish(`./dist/${name.replace("@opencode-ai/", "")}`, name, version), - ), -) -await publish(`./dist/${pkg.name}`, pkg.name, version) +await publishDistribution({ + root: "./dist", + name: pkg.name, + binary: "opencode2", + packagePrefix: "@opencode-ai/cli-", +}) +await publishDistribution({ + root: "./dist/node", + name: "opencode-node", + binary: "opencode2-node", + packagePrefix: "@opencode-ai/cli-node-", +}) diff --git a/packages/cli/script/service-smoke.ts b/packages/cli/script/service-smoke.ts new file mode 100644 index 0000000000..2a51fc22ff --- /dev/null +++ b/packages/cli/script/service-smoke.ts @@ -0,0 +1,135 @@ +#!/usr/bin/env bun + +import { Service } from "@opencode-ai/client/effect/service" +import { ServiceStatus } from "@opencode-ai/protocol/groups/health" +import { Schema } from "effect" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" + +const nodeBuild = process.argv.includes("--node") +const target = `cli${nodeBuild ? "-node" : ""}-${process.platform === "win32" ? "windows" : process.platform}-${process.arch}` +const directory = path.join(import.meta.dir, "..", "dist", ...(nodeBuild ? ["node"] : []), target, "bin") +const binary = path.join(directory, `opencode2${nodeBuild ? "-node" : ""}${process.platform === "win32" ? ".exe" : ""}`) +if (!(await Bun.file(binary).exists())) throw new Error(`Missing compiled CLI in ${directory}`) + +const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-smoke-")) +const env = { + ...process.env, + HOME: root, + USERPROFILE: root, + OPENCODE_DB: path.join(root, "opencode.db"), + OPENCODE_TEST_HOME: root, + XDG_CACHE_HOME: path.join(root, "cache"), + XDG_CONFIG_HOME: path.join(root, "config"), + XDG_DATA_HOME: path.join(root, "data"), + XDG_STATE_HOME: path.join(root, "state"), +} +const processes: Array> = [] +const errors: Array> = [] +let failure: unknown +try { + spawnService() + spawnService() + const registration = await waitForRegistration() + const info = await Schema.decodeUnknownPromise(Service.Info)(await Bun.file(registration).json()) + if (info.id === undefined || info.password === undefined) throw new Error("Registration is missing service identity") + const credential = btoa(`opencode:${info.password}`) + const headers = { authorization: "Basic " + credential } + const token = encodeURIComponent(credential) + const health = await waitForReady(info.url, headers) + if (health.pid !== info.pid) throw new Error("Health process does not match registration") + const tokenHealth = await fetch( + new URL(`/api/health?auth_token=${token}`, info.url), + { signal: AbortSignal.timeout(5_000) }, + ) + if (tokenHealth.status !== 200) throw new Error("Compiled service rejected query authentication") + const tokenOpenApi = await fetch( + new URL(`/openapi.json?auth_token=${token}`, info.url), + { signal: AbortSignal.timeout(5_000) }, + ) + if (tokenOpenApi.status !== 200) throw new Error("Compiled application rejected query authentication") + + const unauthorizedHealth = await fetch(new URL("/api/health", info.url), { + signal: AbortSignal.timeout(5_000), + }) + if (unauthorizedHealth.status !== 401) throw new Error("Compiled service exposed health without authentication") + const unauthorizedOpenApi = await fetch(new URL("/openapi.json", info.url), { + signal: AbortSignal.timeout(5_000), + }) + if (unauthorizedOpenApi.status !== 401) throw new Error("Compiled service exposed application routes without authentication") + const unauthorizedStop = await fetch(new URL("/api/service/stop", info.url), { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ instanceID: info.id }), + signal: AbortSignal.timeout(5_000), + }) + if (unauthorizedStop.status !== 401) throw new Error("Compiled service accepted unauthenticated stop") + + const winner = processes.find((process) => process.pid === info.pid) + const loser = processes.find((process) => process.pid !== info.pid) + if (!winner || !loser) throw new Error("Compiled contenders did not elect one registered owner") + if (!(await exitsWithin(loser, 10_000))) throw new Error("Losing compiled contender did not exit") + + const stopped = await Schema.decodeUnknownPromise(ServiceStatus.StopResponse)( + await fetch(new URL("/api/service/stop", info.url), { + method: "POST", + headers: { ...headers, "content-type": "application/json" }, + body: JSON.stringify({ instanceID: info.id }), + signal: AbortSignal.timeout(5_000), + }).then((response) => response.json()), + ) + if (!stopped.accepted) throw new Error("Compiled service rejected exact-instance stop") + if (!(await exitsWithin(winner, 10_000))) throw new Error("Compiled service did not stop") + for (let attempt = 0; attempt < 200 && (await Bun.file(registration).exists()); attempt++) await Bun.sleep(25) + if (await Bun.file(registration).exists()) throw new Error("Compiled service registration was not removed") +} catch (cause) { + failure = cause +} finally { + processes.forEach((process) => process.kill()) + await Promise.all(processes.map((process) => process.exited)) +} + +const output = await Promise.all(errors) +await fs.rm(root, { recursive: true, force: true }) +if (failure) + throw new Error(output.filter(Boolean).join("\n") || "Compiled service lifecycle smoke test failed", { + cause: failure, + }) + +function spawnService() { + const process = Bun.spawn([binary, "serve", "--service"], { env, stdout: "ignore", stderr: "pipe" }) + processes.push(process) + errors.push(new Response(process.stderr).text()) + return process +} + +async function waitForRegistration() { + const directory = path.join(root, "state", "opencode") + for (let attempt = 0; attempt < 400; attempt++) { + const files = await fs.readdir(directory).catch(() => []) + const file = files.find( + (file) => file === "service.json" || (file.startsWith("service-") && file.endsWith(".json")), + ) + if (file) return path.join(directory, file) + await Bun.sleep(25) + } + throw new Error("Compiled service did not publish registration") +} + +async function waitForReady(url: string, headers: HeadersInit) { + const deadline = Date.now() + 20_000 + while (Date.now() < deadline) { + const response = await fetch(new URL("/api/health", url), { + headers, + signal: AbortSignal.timeout(1_000), + }).catch(() => undefined) + if (response?.ok) return Schema.decodeUnknownPromise(ServiceStatus.Health)(await response.json()) + await Bun.sleep(25) + } + throw new Error("Compiled service did not become ready") +} + +function exitsWithin(process: Bun.Subprocess, milliseconds: number) { + return Promise.race([process.exited.then(() => true), Bun.sleep(milliseconds).then(() => false)]) +} diff --git a/packages/cli/src/commands/commands.ts b/packages/cli/src/commands/commands.ts index 19d1f5e68b..771f57f42a 100644 --- a/packages/cli/src/commands/commands.ts +++ b/packages/cli/src/commands/commands.ts @@ -3,12 +3,41 @@ import { Spec } from "../framework/spec" declare const OPENCODE_CLI_NAME: string | undefined +const ServerParams = { + standalone: Flag.boolean("standalone").pipe( + Flag.withDescription("Run with a private server instead of the background service"), + Flag.withDefault(false), + ), + server: Flag.string("server").pipe( + Flag.withDescription("Connect to a server URL instead of the background service"), + Flag.optional, + ), +} + export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME : "opencode", { description: "OpenCode 2.0 preview command line interface", + params: { + ...ServerParams, + directory: Argument.string("directory").pipe( + Argument.withDescription("Directory to start OpenCode in"), + Argument.optional, + ), + continue: Flag.boolean("continue").pipe( + Flag.withAlias("c"), + Flag.withDescription("Continue the last session"), + Flag.withDefault(false), + ), + session: Flag.string("session").pipe( + Flag.withAlias("s"), + Flag.withDescription("Session ID to continue"), + Flag.optional, + ), + }, commands: [ Spec.make("api", { description: "Make a request to the running server", params: { + ...ServerParams, request: Argument.string("operation | method path").pipe( Argument.withDescription("OpenAPI operation ID, or an HTTP method followed by a path"), Argument.variadic({ min: 1, max: 2 }), @@ -26,7 +55,152 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO description: "Debugging and troubleshooting tools", commands: [Spec.make("agents", { description: "List all agents" })], }), + Spec.make("console", { + description: "Manage OpenCode Console access", + commands: [ + Spec.make("login", { + description: "Log in to OpenCode Console", + params: { + url: Argument.string("url").pipe(Argument.withDescription("Console server URL"), Argument.optional), + }, + }), + ], + }), + Spec.make("auth", { + description: "Manage authentication", + commands: [ + Spec.make("connect", { + description: "Connect to a wellknown authentication provider", + params: { + url: Argument.string("url").pipe(Argument.withDescription("Wellknown provider URL")), + }, + }), + ], + }), + Spec.make("mcp", { + description: "Manage MCP (Model Context Protocol) servers", + commands: [ + Spec.make("list", { description: "List configured MCP servers and their status" }), + Spec.make("add", { + description: "Add an MCP server to your configuration", + params: { + name: Argument.string("name").pipe(Argument.withDescription("Name of the MCP server")), + command: Argument.string("command").pipe( + Argument.withDescription("Command and arguments for a local server, passed after --"), + Argument.variadic({ min: 0 }), + ), + url: Flag.string("url").pipe(Flag.withDescription("URL for a remote MCP server"), Flag.optional), + header: Flag.keyValuePair("header").pipe( + Flag.withDescription("HTTP header for a remote server, as name=value"), + Flag.optional, + ), + env: Flag.keyValuePair("env").pipe( + Flag.withDescription("Environment variable for a local server, as name=value"), + Flag.optional, + ), + global: Flag.boolean("global").pipe( + Flag.withDescription("Write to the global config instead of the project config"), + Flag.withDefault(false), + ), + }, + }), + Spec.make("auth", { + description: "Authenticate with an OAuth-capable remote MCP server", + params: { name: Argument.string("name").pipe(Argument.withDescription("Name of the MCP server")) }, + }), + Spec.make("logout", { + description: "Remove stored OAuth credentials for an MCP server", + params: { name: Argument.string("name").pipe(Argument.withDescription("Name of the MCP server")) }, + }), + ], + }), + Spec.make("plugin", { + description: "Manage plugins", + commands: [Spec.make("list", { description: "List active plugins" })], + }), Spec.make("migrate", { description: "Migrate v1 data to v2" }), + Spec.make("mini", { + description: "Start the minimal interactive interface", + params: { + ...ServerParams, + continue: Flag.boolean("continue").pipe( + Flag.withAlias("c"), + Flag.withDescription("Continue the last session"), + Flag.withDefault(false), + ), + session: Flag.string("session").pipe( + Flag.withAlias("s"), + Flag.withDescription("Session ID to continue"), + Flag.optional, + ), + fork: Flag.boolean("fork").pipe( + Flag.withDescription("Fork the session when continuing"), + Flag.withDefault(false), + ), + replay: Flag.boolean("replay").pipe( + Flag.withDescription("Replay session history on resume and after resize"), + Flag.withDefault(true), + ), + replayLimit: Flag.integer("replay-limit").pipe( + Flag.withDescription("Cap visible replay to the newest N messages"), + Flag.optional, + ), + model: Flag.string("model").pipe( + Flag.withAlias("m"), + Flag.withDescription("Model to use in the format provider/model"), + Flag.optional, + ), + agent: Flag.string("agent").pipe(Flag.withDescription("Agent to use"), Flag.optional), + prompt: Flag.string("prompt").pipe(Flag.withDescription("Prompt to use"), Flag.optional), + demo: Flag.boolean("demo").pipe(Flag.withDefault(false), Flag.withHidden), + }, + }), + Spec.make("run", { + description: "Run OpenCode with a message", + params: { + ...ServerParams, + message: Argument.string("message").pipe( + Argument.withDescription("Message to send"), + Argument.variadic({ min: 0 }), + ), + continue: Flag.boolean("continue").pipe( + Flag.withAlias("c"), + Flag.withDescription("Continue the last session"), + Flag.withDefault(false), + ), + session: Flag.string("session").pipe( + Flag.withAlias("s"), + Flag.withDescription("Session ID to continue"), + Flag.optional, + ), + fork: Flag.boolean("fork").pipe( + Flag.withDescription("Fork the session before continuing"), + Flag.withDefault(false), + ), + model: Flag.string("model").pipe( + Flag.withAlias("m"), + Flag.withDescription("Model to use in the format provider/model#variant"), + Flag.optional, + ), + agent: Flag.string("agent").pipe(Flag.withDescription("Agent to use"), Flag.optional), + format: Flag.choice("format", ["default", "json"]).pipe( + Flag.withDescription("Output format"), + Flag.withDefault("default"), + ), + file: Flag.string("file").pipe( + Flag.withAlias("f"), + Flag.withDescription("File to attach to the message"), + Flag.atMost(100), + ), + title: Flag.string("title").pipe(Flag.withDescription("Session title"), Flag.optional), + thinking: Flag.boolean("thinking").pipe(Flag.withDescription("Show thinking blocks"), Flag.withDefault(false)), + auto: Flag.boolean("auto").pipe( + Flag.withDescription("Auto-approve permissions that are not explicitly denied"), + Flag.withDefault(false), + ), + yolo: Flag.boolean("yolo").pipe(Flag.withDefault(false), Flag.withHidden), + }, + }), Spec.make("service", { description: "Manage the background server", commands: [ @@ -34,18 +208,28 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO Spec.make("restart", { description: "Restart the background server" }), Spec.make("status", { description: "Show background server status" }), Spec.make("stop", { description: "Stop the background server" }), - Spec.make("password", { - description: "Get or set the server password", - params: { value: Argument.string("value").pipe(Argument.optional) }, + Spec.make("get", { + description: "Get service configuration", + params: { key: Argument.string("key").pipe(Argument.optional) }, + }), + Spec.make("set", { + description: "Set service configuration", + params: { key: Argument.string("key"), value: Argument.string("value") }, + }), + Spec.make("unset", { + description: "Unset service configuration", + params: { key: Argument.string("key") }, }), ], }), + Spec.make("pair", { description: "Show server pairing information" }), Spec.make("serve", { description: "Start the v2 API server", params: { - hostname: Flag.string("hostname").pipe(Flag.withDefault("127.0.0.1")), + hostname: Flag.string("hostname").pipe(Flag.optional), port: Flag.integer("port").pipe(Flag.optional), - register: Flag.boolean("register").pipe(Flag.withDefault(false)), + service: Flag.boolean("service").pipe(Flag.withDefault(false)), + stdio: Flag.boolean("stdio").pipe(Flag.withDefault(false)), }, }), ], diff --git a/packages/cli/src/commands/handlers/api.ts b/packages/cli/src/commands/handlers/api.ts index cf00394cb9..aa612f67c1 100644 --- a/packages/cli/src/commands/handlers/api.ts +++ b/packages/cli/src/commands/handlers/api.ts @@ -2,7 +2,8 @@ import { EOL } from "node:os" import { Effect, Option } from "effect" import { Commands } from "../commands" import { Runtime } from "../../framework/runtime" -import { Daemon } from "../../services/daemon" +import { Service, type Endpoint } from "@opencode-ai/client/effect/service" +import { ServerConnection } from "../../services/server-connection" const methods = new Set(["delete", "get", "head", "options", "patch", "post", "put"]) @@ -17,11 +18,15 @@ type OpenApi = { export default Runtime.handler( Commands.commands.api, Effect.fn("cli.api")(function* (input) { - const daemon = yield* Daemon.Service - const transport = yield* daemon.transport() + const server = yield* ServerConnection.resolve({ + server: Option.getOrUndefined(input.server), + standalone: input.standalone, + mismatch: "ignore", + }) + const endpoint = server.endpoint const params = Option.getOrElse(input.param, () => ({})) - const request = yield* resolveRequest(transport, input.request, params) - const headers = new Headers(transport.headers) + const request = yield* resolveRequest(endpoint, input.request, params) + const headers = new Headers(Service.headers(endpoint)) for (const header of input.header) { const index = header.indexOf(":") if (index < 1) return yield* Effect.fail(new Error(`Invalid header, expected name:value: ${header}`)) @@ -31,7 +36,7 @@ export default Runtime.handler( if (body !== undefined && !headers.has("content-type")) headers.set("content-type", "application/json") const response = yield* Effect.tryPromise(() => - fetch(new URL(request.path, transport.url), { + fetch(new URL(request.path, endpoint.url), { method: request.method, headers, body, @@ -57,16 +62,12 @@ export function rawRequest(input: readonly string[]) { return { method: input[0].toUpperCase(), path: input[1] } } -function resolveRequest( - transport: { url: string; headers: RequestInit["headers"] }, - input: readonly string[], - params: Record, -) { +function resolveRequest(endpoint: Endpoint, input: readonly string[], params: Record) { const raw = rawRequest(input) if (raw) return Effect.succeed(raw) if (input.length !== 1) return Effect.fail(new Error("Expected an operation name or an HTTP method and path")) return Effect.tryPromise(async () => { - const response = await fetch(new URL("/openapi.json", transport.url), { headers: transport.headers }) + const response = await fetch(new URL("/openapi.json", endpoint.url), { headers: Service.headers(endpoint) }) if (!response.ok) throw new Error(`Failed to load OpenAPI document: HTTP ${response.status}`) return resolveOperation((await response.json()) as OpenApi, input[0], params) }) diff --git a/packages/cli/src/commands/handlers/auth/connect.ts b/packages/cli/src/commands/handlers/auth/connect.ts new file mode 100644 index 0000000000..f135145568 --- /dev/null +++ b/packages/cli/src/commands/handlers/auth/connect.ts @@ -0,0 +1,52 @@ +import { EOL } from "node:os" +import { Effect } from "effect" +import { Service } from "@opencode-ai/client/effect/service" +import { OpenCode, type IntegrationCommandStatusOutput, type OpenCodeClient } from "@opencode-ai/client/promise" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" +import { ServiceConfig } from "../../../services/service-config" + +const location = { directory: process.cwd() } + +export default Runtime.handler( + Commands.commands.auth.commands.connect, + Effect.fn("cli.auth.connect")(function* (input) { + process.stdout.write("Connecting..." + EOL + EOL) + const endpoint = yield* Service.ensure(yield* ServiceConfig.options()) + const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }) + yield* request(() => client.integration.wellknown.add({ url: input.url, location })) + const integrationID = input.url.replace(/\/+$/, "") + const started = yield* request(() => + client.integration.command.connect({ integrationID, methodID: "login", location }), + ) + yield* Effect.addFinalizer(() => + request(() => + client.integration.command.cancel({ integrationID, attemptID: started.data.attemptID, location }), + ).pipe(Effect.ignore), + ) + + const status = yield* wait(client, integrationID, started.data.attemptID) + if (status.status === "failed") return yield* Effect.fail(new Error(status.message)) + if (status.status === "expired") return yield* Effect.fail(new Error("Authentication expired")) + process.stdout.write("Connected" + EOL) + }), +) + +const wait = ( + client: OpenCodeClient, + integrationID: string, + attemptID: string, + shown = false, +): Effect.Effect, unknown> => + Effect.gen(function* () { + const response = yield* request(() => client.integration.command.status({ integrationID, attemptID, location })) + if (response.data.status !== "pending") return response.data + const output = response.data.message?.trim() + if (!shown && output) process.stdout.write(output + EOL + EOL) + yield* Effect.sleep(500) + return yield* wait(client, integrationID, attemptID, shown || !!output) + }) + +function request(task: () => Promise) { + return Effect.tryPromise({ try: task, catch: (cause) => cause }) +} diff --git a/packages/cli/src/commands/handlers/console/login.ts b/packages/cli/src/commands/handlers/console/login.ts new file mode 100644 index 0000000000..cb2eee5193 --- /dev/null +++ b/packages/cli/src/commands/handlers/console/login.ts @@ -0,0 +1,118 @@ +import { Cause, Effect, Exit, Option } from "effect" +import { Service } from "@opencode-ai/client/effect/service" +import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise" +import { AppProcess } from "@opencode-ai/core/process" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" +import { ServiceConfig } from "../../../services/service-config" +import { createTimelineHost, type TimelineHost } from "../../../ui/timeline" + +const integrationID = "opencode" +const location = { directory: process.cwd() } + +export default Runtime.handler( + Commands.commands.console.commands.login, + Effect.fn("cli.console.login")(function* (input) { + const timeline = yield* Effect.acquireRelease( + Effect.promise(() => createTimelineHost()), + (value) => request(() => value.close()).pipe(Effect.ignore), + ) + const exit = yield* login(timeline, Option.getOrUndefined(input.url)).pipe( + Effect.raceFirst(AppProcess.waitForAbort(timeline.signal)), + Effect.exit, + ) + if (Exit.isSuccess(exit)) return + + const cancelled = timeline.signal.aborted + yield* request(() => timeline.failure(cancelled ? "Authorization cancelled" : errorMessage(exit.cause))).pipe( + Effect.ignore, + ) + process.exitCode = cancelled ? 130 : 1 + }), +) + +const login = Effect.fn("cli.console.login.run")(function* (timeline: TimelineHost, server?: string) { + yield* request(() => timeline.intro("Log in")) + yield* request(() => timeline.pending("Connecting to OpenCode...")) + + const endpoint = yield* Service.ensure(yield* ServiceConfig.options()) + const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }) + const found = yield* request((signal) => client.integration.get({ integrationID, location }, { signal })) + const integration = yield* required(found.data, "OpenCode Console integration is unavailable") + const method = yield* required( + integration.methods.find((candidate) => candidate.type === "oauth"), + "OpenCode Console login is unavailable", + ) + + yield* request(() => timeline.pending("Starting authorization...")) + const started = yield* request((signal) => + client.integration.oauth.connect( + { + integrationID, + methodID: method.id, + inputs: server ? { server } : {}, + location, + }, + { signal }, + ), + ) + const attempt = started.data + yield* Effect.addFinalizer(() => + request(() => + client.integration.oauth.cancel( + { integrationID, attemptID: attempt.attemptID, location }, + { signal: AbortSignal.timeout(5_000) }, + ), + ).pipe(Effect.ignore), + ) + if (attempt.mode !== "auto") yield* Effect.fail(new Error("OpenCode Console requires a device login")) + + yield* request(() => timeline.item(`Go to: ${attempt.url}`)) + yield* request(() => timeline.item(attempt.instructions)) + yield* request(async () => { + const { default: open } = await import("open") + await open(attempt.url) + }).pipe(Effect.ignore) + yield* request(() => timeline.pending("Waiting for authorization...")) + + const status = yield* waitForConsoleLogin(client, integrationID, attempt.attemptID) + if (status.status === "failed") yield* Effect.fail(new Error(status.message)) + if (status.status === "expired") yield* Effect.fail(new Error("Device code expired")) + + yield* request(() => timeline.success("Connected to OpenCode Console")) + yield* request(() => timeline.outro("Done")) +}) + +const waitForConsoleLogin = Effect.fn("cli.console.login.wait")(function* ( + client: OpenCodeClient, + integrationID: string, + attemptID: string, +) { + while (true) { + const response = yield* request((signal) => + client.integration.oauth.status({ integrationID, attemptID, location }, { signal }), + ) + if (response.data.status !== "pending") return response.data + yield* Effect.sleep(500) + } +}) + +function request(task: (signal: AbortSignal) => Promise) { + return Effect.tryPromise({ + try: task, + catch: (cause) => cause, + }) +} + +function required(value: A | null | undefined, message: string) { + return value === null || value === undefined ? Effect.fail(new Error(message)) : Effect.succeed(value) +} + +function errorMessage(cause: Cause.Cause) { + const error = Cause.squash(cause) + if (error instanceof Error) return error.message + if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") { + return error.message + } + return String(error) +} diff --git a/packages/cli/src/commands/handlers/debug/agents.ts b/packages/cli/src/commands/handlers/debug/agents.ts index 3a0c20cb06..c178e90b45 100644 --- a/packages/cli/src/commands/handlers/debug/agents.ts +++ b/packages/cli/src/commands/handlers/debug/agents.ts @@ -1,18 +1,22 @@ import { EOL } from "os" -import * as Effect from "effect/Effect" +import { Effect } from "effect" +import { OpenCode } from "@opencode-ai/client" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" -import { Daemon } from "../../../services/daemon" +import { Service } from "@opencode-ai/client/effect/service" +import { ServiceConfig } from "../../../services/service-config" export default Runtime.handler( Commands.commands.debug.commands.agents, Effect.fn("cli.debug.agents")(function* () { - const daemon = yield* Daemon.Service - const client = yield* daemon.client() - const response = yield* Effect.promise(() => client.v2.agent.list({ location: { directory: process.cwd() } })) + const options = yield* ServiceConfig.options() + const found = yield* Service.discover(options) + const endpoint = found ?? (yield* Service.ensure(options)) + const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }) + const response = yield* Effect.promise(() => client.agent.list({ location: { directory: process.cwd() } })) process.stdout.write( JSON.stringify( - response.data?.data.toSorted((a, b) => a.id.localeCompare(b.id)), + response.data.toSorted((a, b) => a.id.localeCompare(b.id)), null, 2, ) + EOL, diff --git a/packages/cli/src/commands/handlers/default.ts b/packages/cli/src/commands/handlers/default.ts index d0a9968e5d..535003ed1a 100644 --- a/packages/cli/src/commands/handlers/default.ts +++ b/packages/cli/src/commands/handlers/default.ts @@ -1,13 +1,80 @@ +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { Global } from "@opencode-ai/core/global" +import { run } from "@opencode-ai/tui" import { Commands } from "../commands" import { Runtime } from "../../framework/runtime" -import { Effect } from "effect" -import { Daemon } from "../../services/daemon" +import { Config } from "../../config" +import { Context, Effect, FileSystem, Option } from "effect" +import { ServerConnection } from "../../services/server-connection" +import { Updater } from "../../services/updater" +import { UpdatePreflight } from "../../services/update-preflight" +import { Npm } from "@opencode-ai/core/npm" -export default Runtime.handler(Commands, () => +export default Runtime.handler(Commands, (input) => Effect.gen(function* () { - const daemon = yield* Daemon.Service - const transport = yield* daemon.transport() - const { runTui } = yield* Effect.promise(() => import("../../tui")) - yield* runTui(transport) + const requestedDirectory = Option.getOrUndefined(input.directory) + if (requestedDirectory !== undefined) process.chdir(requestedDirectory) + const updater = yield* Updater.Service + yield* updater.check().pipe(Effect.forkScoped) + const preflight = UpdatePreflight.make() + yield* Effect.addFinalizer(() => Effect.promise(() => preflight.close())) + const server = yield* ServerConnection.resolve({ + server: Option.getOrUndefined(input.server), + standalone: input.standalone, + onStart: (reason, previousVersion) => { + if (reason === "version-mismatch" && preflight.begin(previousVersion)) return + process.stderr.write( + reason === "version-mismatch" + ? "Restarting background server (version mismatch)...\n" + : "Starting background server...\n", + ) + }, + }).pipe( + Effect.tapError(() => + Effect.promise(() => preflight.fail("OpenCode update could not start the new background service")), + ), + ) + preflight.loading() + const config = yield* Config.Service + const npm = yield* Npm.Service + const fileSystem = yield* FileSystem.FileSystem + const runServicePromise = Effect.runPromiseWith(Context.make(FileSystem.FileSystem, fileSystem)) + const context = yield* Effect.context() + const runFork = Effect.runForkWith(context) + const runPromise = Effect.runPromiseWith(context) + const service = server.service + yield* run({ + server: { + endpoint: server.endpoint, + service: service + ? { + reconnect: (signal) => runServicePromise(service.reconnect(), { signal }), + restart: () => runServicePromise(service.restart()), + } + : undefined, + }, + args: { continue: input.continue, sessionID: Option.getOrUndefined(input.session) }, + config: { + path: config.path, + get: () => runPromise(config.get()), + update: (update) => runPromise(config.update(update)), + }, + packages: { + resolve: (spec) => + runPromise(npm.add(spec, { subpaths: ["tui"] }).pipe(Effect.map((result) => result.entrypoint))), + }, + terminalHandoff: () => preflight.finish(), + log: (level, message, tags) => { + const effect = + level === "debug" + ? Effect.logDebug(message, tags) + : level === "warn" + ? Effect.logWarning(message, tags) + : level === "error" + ? Effect.logError(message, tags) + : Effect.logInfo(message, tags) + runFork(effect) + }, + }).pipe(Effect.provide(LayerNode.compile(Global.node))) }), ) diff --git a/packages/cli/src/commands/handlers/mcp/add.ts b/packages/cli/src/commands/handlers/mcp/add.ts new file mode 100644 index 0000000000..dc582a0e00 --- /dev/null +++ b/packages/cli/src/commands/handlers/mcp/add.ts @@ -0,0 +1,67 @@ +import { EOL } from "node:os" +import path from "node:path" +import { readFile, stat, writeFile } from "node:fs/promises" +import { Effect, Option } from "effect" +import { applyEdits, modify } from "jsonc-parser" +import { Global } from "@opencode-ai/core/global" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" + +export default Runtime.handler( + Commands.commands.mcp.commands.add, + Effect.fn("cli.mcp.add")(function* (input) { + const url = Option.getOrUndefined(input.url) + const headers = Option.getOrUndefined(input.header) + const environment = Option.getOrUndefined(input.env) + // The CLI framework strands `--` operands on the root command, so read the local server command + // straight from argv after `--`. This also lets the command carry its own flags (e.g. `npx -y`). + const dash = process.argv.indexOf("--") + const command = dash === -1 ? [...input.command] : process.argv.slice(dash + 1) + + const hasCommand = command.length > 0 + if (url && hasCommand) + return yield* Effect.fail(new Error("Provide either --url or a command after --, not both")) + if (!url && !hasCommand) return yield* Effect.fail(new Error("Provide either --url or a command after --")) + if (url && !URL.canParse(url)) return yield* Effect.fail(new Error(`Invalid URL: ${url}`)) + if (url && environment) return yield* Effect.fail(new Error("--env is only valid for local MCP servers")) + if (hasCommand && headers) return yield* Effect.fail(new Error("--header is only valid for remote MCP servers")) + + const server = url + ? { type: "remote" as const, url, ...(headers ? { headers } : {}) } + : { type: "local" as const, command, ...(environment ? { environment } : {}) } + + const configPath = yield* Effect.promise(() => resolveConfigPath(input.global ? Global.Path.config : process.cwd())) + yield* Effect.promise(() => write(configPath, input.name, server)) + process.stdout.write(`MCP server "${input.name}" added to ${configPath}` + EOL) + }), +) + +export async function resolveConfigPath(directory: string) { + const candidates = [ + path.join(directory, "opencode.json"), + path.join(directory, "opencode.jsonc"), + path.join(directory, ".opencode", "opencode.json"), + path.join(directory, ".opencode", "opencode.jsonc"), + ] + for (const candidate of candidates) { + if ( + await stat(candidate).then( + (info) => info.isFile(), + () => false, + ) + ) + return candidate + } + return candidates[0] +} + +async function write(configPath: string, name: string, server: unknown) { + const text = await readFile(configPath, "utf8").catch((error) => { + if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return "{}" + throw error + }) + const edits = modify(text, ["mcp", "servers", name], server, { + formattingOptions: { tabSize: 2, insertSpaces: true }, + }) + await writeFile(configPath, applyEdits(text, edits)) +} diff --git a/packages/cli/src/commands/handlers/mcp/auth.ts b/packages/cli/src/commands/handlers/mcp/auth.ts new file mode 100644 index 0000000000..2ca5259953 --- /dev/null +++ b/packages/cli/src/commands/handlers/mcp/auth.ts @@ -0,0 +1,67 @@ +import { EOL } from "node:os" +import { Effect } from "effect" +import { + OpenCode, + type IntegrationAttemptStatus, + type IntegrationOAuthMethod, + type OpenCodeClient, +} from "@opencode-ai/client" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" +import { Service } from "@opencode-ai/client/effect/service" +import { ServiceConfig } from "../../../services/service-config" +import { resolveIntegration } from "./resolve" + +const location = { directory: process.cwd() } + +export default Runtime.handler( + Commands.commands.mcp.commands.auth, + Effect.fn("cli.mcp.auth")(function* (input) { + const options = yield* ServiceConfig.options() + const found = yield* Service.discover(options) + const endpoint = found ?? (yield* Service.ensure(options)) + const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }) + + const integration = yield* resolveIntegration(client, input.name, location) + if (!integration) + return yield* Effect.fail(new Error(`MCP server "${input.name}" is not an OAuth-capable remote server`)) + const method = integration.methods.find( + (candidate): candidate is IntegrationOAuthMethod => candidate.type === "oauth", + ) + if (!method) + return yield* Effect.fail(new Error(`MCP server "${input.name}" is not an OAuth-capable remote server`)) + + const started = yield* Effect.promise(() => + client.integration.oauth.connect({ integrationID: integration.id, methodID: method.id, inputs: {}, location }), + ) + const attempt = started.data + if (attempt.mode === "code") + return yield* Effect.fail(new Error("This server requires manual code entry, which the CLI does not support")) + + process.stdout.write(attempt.instructions + EOL + attempt.url + EOL) + + const result = yield* poll(client, integration.id, attempt.attemptID) + if (result.status === "complete") { + process.stdout.write(`Authenticated with ${input.name}` + EOL) + return + } + const reason = result.status === "failed" ? `: ${result.message}` : "" + return yield* Effect.fail(new Error(`Authentication ${result.status}${reason}`)) + }), +) + +const poll = ( + client: OpenCodeClient, + integrationID: string, + attemptID: string, +): Effect.Effect> => + Effect.gen(function* () { + const status = yield* Effect.promise(() => + client.integration.oauth.status({ integrationID, attemptID, location }), + ).pipe(Effect.map((result) => result.data)) + if (status.status === "pending") { + yield* Effect.sleep("1 second") + return yield* poll(client, integrationID, attemptID) + } + return status + }) diff --git a/packages/cli/src/commands/handlers/mcp/list.ts b/packages/cli/src/commands/handlers/mcp/list.ts new file mode 100644 index 0000000000..590fb818c5 --- /dev/null +++ b/packages/cli/src/commands/handlers/mcp/list.ts @@ -0,0 +1,55 @@ +import { EOL } from "node:os" +import { Effect } from "effect" +import { OpenCode, type McpServer } from "@opencode-ai/client" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" +import { Service } from "@opencode-ai/client/effect/service" +import { ServiceConfig } from "../../../services/service-config" + +export default Runtime.handler( + Commands.commands.mcp.commands.list, + Effect.fn("cli.mcp.list")(function* () { + const options = yield* ServiceConfig.options() + const found = yield* Service.discover(options) + const endpoint = found ?? (yield* Service.ensure(options)) + const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }) + const response = yield* Effect.promise(() => client.mcp.list({ location: { directory: process.cwd() } })) + const servers = response.data.toSorted((a, b) => a.name.localeCompare(b.name)) + if (servers.length === 0) { + process.stdout.write("No MCP servers configured" + EOL) + return + } + const width = Math.max(...servers.map((server) => server.name.length)) + const lines = servers.map( + (server) => `${icon(server.status)} ${server.name.padEnd(width)} ${describe(server.status)}`, + ) + process.stdout.write(lines.join(EOL) + EOL) + }), +) + +function icon(status: McpServer["status"]) { + switch (status.status) { + case "connected": + return "✓" + case "needs_auth": + return "⚠" + case "failed": + case "needs_client_registration": + return "✗" + default: + return "○" + } +} + +function describe(status: McpServer["status"]) { + switch (status.status) { + case "needs_auth": + return "needs authentication" + case "needs_client_registration": + return `needs client registration: ${status.error}` + case "failed": + return `failed: ${status.error}` + default: + return status.status + } +} diff --git a/packages/cli/src/commands/handlers/mcp/logout.ts b/packages/cli/src/commands/handlers/mcp/logout.ts new file mode 100644 index 0000000000..47dc3052a6 --- /dev/null +++ b/packages/cli/src/commands/handlers/mcp/logout.ts @@ -0,0 +1,39 @@ +import { EOL } from "node:os" +import { Effect } from "effect" +import { OpenCode } from "@opencode-ai/client" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" +import { Service } from "@opencode-ai/client/effect/service" +import { ServiceConfig } from "../../../services/service-config" +import { resolveIntegration } from "./resolve" + +const location = { directory: process.cwd() } + +export default Runtime.handler( + Commands.commands.mcp.commands.logout, + Effect.fn("cli.mcp.logout")(function* (input) { + const options = yield* ServiceConfig.options() + const found = yield* Service.discover(options) + const endpoint = found ?? (yield* Service.ensure(options)) + const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }) + + const integration = yield* resolveIntegration(client, input.name, location) + if (!integration) { + process.stdout.write(`No stored credentials for ${input.name}` + EOL) + return + } + + const credentials = integration.connections.filter((connection) => connection.type === "credential") + if (credentials.length === 0) { + process.stdout.write(`No stored credentials for ${input.name}` + EOL) + return + } + + yield* Effect.forEach( + credentials, + (connection) => Effect.promise(() => client.credential.remove({ credentialID: connection.id, location })), + { discard: true }, + ) + process.stdout.write(`Removed OAuth credentials for ${input.name}` + EOL) + }), +) diff --git a/packages/cli/src/commands/handlers/mcp/resolve.ts b/packages/cli/src/commands/handlers/mcp/resolve.ts new file mode 100644 index 0000000000..1ee5e19568 --- /dev/null +++ b/packages/cli/src/commands/handlers/mcp/resolve.ts @@ -0,0 +1,18 @@ +import { Effect } from "effect" +import type { OpenCodeClient } from "@opencode-ai/client" + +// Resolve through the MCP-owned integrationID rather than matching integration names: the shared +// integration registry also holds provider/plugin integrations, whose names could collide with a server. +// Fails when the server is unknown; returns undefined when the server has no integration (e.g. a local +// or anonymous server), leaving that case for the caller to interpret. +export const resolveIntegration = (client: OpenCodeClient, name: string, location: { directory: string }) => + Effect.gen(function* () { + const servers = yield* Effect.promise(() => client.mcp.list({ location })) + const server = servers.data.find((entry) => entry.name === name) + if (!server) return yield* Effect.fail(new Error(`MCP server not found: ${name}`)) + const integrationID = server.integrationID + if (!integrationID) return undefined + return yield* Effect.promise(() => client.integration.get({ integrationID, location })).pipe( + Effect.map((result) => result.data ?? undefined), + ) + }) diff --git a/packages/cli/src/commands/handlers/migrate.ts b/packages/cli/src/commands/handlers/migrate.ts index c73c7750df..6ff6939aa1 100644 --- a/packages/cli/src/commands/handlers/migrate.ts +++ b/packages/cli/src/commands/handlers/migrate.ts @@ -1,4 +1,4 @@ -import * as Effect from "effect/Effect" +import { Effect } from "effect" import { Commands } from "../commands" import { Runtime } from "../../framework/runtime" diff --git a/packages/cli/src/commands/handlers/mini.ts b/packages/cli/src/commands/handlers/mini.ts new file mode 100644 index 0000000000..e44f7e0b2d --- /dev/null +++ b/packages/cli/src/commands/handlers/mini.ts @@ -0,0 +1,27 @@ +import { Effect, Option } from "effect" +import { Commands } from "../commands" +import { Runtime } from "../../framework/runtime" +import { ServerConnection } from "../../services/server-connection" + +export default Runtime.handler(Commands.commands.mini, (input) => + Effect.gen(function* () { + const { runMini, validateMiniTerminal } = yield* Effect.promise(() => import("../../mini")) + yield* Effect.promise(async () => validateMiniTerminal()) + const serverURL = Option.getOrUndefined(input.server) + const server = yield* ServerConnection.resolve({ server: serverURL, standalone: input.standalone }) + yield* Effect.promise(() => + runMini({ + server, + continue: input.continue, + session: Option.getOrUndefined(input.session), + fork: input.fork, + model: Option.getOrUndefined(input.model), + agent: Option.getOrUndefined(input.agent), + prompt: Option.getOrUndefined(input.prompt), + replay: input.replay, + replayLimit: Option.getOrUndefined(input.replayLimit), + demo: input.demo, + }), + ) + }), +) diff --git a/packages/cli/src/commands/handlers/pair.ts b/packages/cli/src/commands/handlers/pair.ts new file mode 100644 index 0000000000..edbae68f6c --- /dev/null +++ b/packages/cli/src/commands/handlers/pair.ts @@ -0,0 +1,43 @@ +import { EOL } from "os" +import { Effect } from "effect" +import { Service } from "@opencode-ai/client/effect/service" +import { OpenCode } from "@opencode-ai/client/promise" +import { renderUnicodeCompact } from "uqr" +import { Commands } from "../commands" +import { Runtime } from "../../framework/runtime" +import { ServiceConfig } from "../../services/service-config" + +export default Runtime.handler( + Commands.commands.pair, + Effect.fn("cli.pair")(function* () { + const endpoint = yield* Service.ensure(yield* ServiceConfig.options()) + const password = yield* ServiceConfig.password() + const server = yield* Effect.tryPromise(() => + OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }).server.get(), + ) + const info = { urls: server.urls, username: "opencode", password } + process.stdout.write( + [ + "", + ` URLs ${info.urls[0] ?? "(none)"}`, + ...info.urls.slice(1).map((url) => ` ${url}`), + ` Username ${info.username}`, + ` Password ${info.password}`, + "", + " Scan to pair", + "", + renderUnicodeCompact(JSON.stringify(info), { border: 2 }) + .split(EOL) + .map((line) => " " + line) + .join(EOL), + "", + ].join(EOL) + EOL, + ) + + const hostname = new URL(endpoint.url).hostname + if (!["localhost", "127.0.0.1", "[::1]"].includes(hostname)) return + process.stderr.write( + ` Run \`opencode service set hostname 0.0.0.0\` to access the service remotely.${EOL}${EOL}`, + ) + }), +) diff --git a/packages/cli/src/commands/handlers/plugin/list.ts b/packages/cli/src/commands/handlers/plugin/list.ts new file mode 100644 index 0000000000..f7f9a258ca --- /dev/null +++ b/packages/cli/src/commands/handlers/plugin/list.ts @@ -0,0 +1,24 @@ +import { EOL } from "node:os" +import { Effect } from "effect" +import { OpenCode } from "@opencode-ai/client" +import { Service } from "@opencode-ai/client/effect/service" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" +import { ServiceConfig } from "../../../services/service-config" + +export default Runtime.handler( + Commands.commands.plugin.commands.list, + Effect.fn("cli.plugin.list")(function* () { + const options = yield* ServiceConfig.options() + const found = yield* Service.discover(options) + const endpoint = found ?? (yield* Service.ensure(options)) + const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }) + const response = yield* Effect.promise(() => client.plugin.list({ location: { directory: process.cwd() } })) + const plugins = response.data.toSorted((a, b) => a.id.localeCompare(b.id)) + if (plugins.length === 0) { + process.stdout.write("No plugins loaded" + EOL) + return + } + process.stdout.write(plugins.map((plugin) => plugin.id).join(EOL) + EOL) + }), +) diff --git a/packages/cli/src/commands/handlers/run.ts b/packages/cli/src/commands/handlers/run.ts new file mode 100644 index 0000000000..50fbaa4e2c --- /dev/null +++ b/packages/cli/src/commands/handlers/run.ts @@ -0,0 +1,31 @@ +import { Effect, Option } from "effect" +import { Commands } from "../commands" +import { Runtime } from "../../framework/runtime" +import { ServerConnection } from "../../services/server-connection" + +export default Runtime.handler(Commands.commands.run, (input) => + Effect.gen(function* () { + const { runNonInteractive } = yield* Effect.promise(() => import("../../mini")) + const separator = process.argv.indexOf("--", 2) + const server = yield* ServerConnection.resolve({ + server: Option.getOrUndefined(input.server), + standalone: input.standalone, + }) + yield* Effect.promise(() => + runNonInteractive({ + server, + message: [...input.message, ...(separator === -1 ? [] : process.argv.slice(separator + 1))], + continue: input.continue, + session: Option.getOrUndefined(input.session), + fork: input.fork, + model: Option.getOrUndefined(input.model), + agent: Option.getOrUndefined(input.agent), + format: input.format, + file: [...input.file], + title: Option.getOrUndefined(input.title), + thinking: input.thinking, + auto: input.auto || input.yolo, + }), + ) + }), +) diff --git a/packages/cli/src/commands/handlers/serve.ts b/packages/cli/src/commands/handlers/serve.ts index 19b097453d..0e4343f916 100644 --- a/packages/cli/src/commands/handlers/serve.ts +++ b/packages/cli/src/commands/handlers/serve.ts @@ -1,46 +1,16 @@ -import { NodeHttpServer } from "@effect/platform-node" -import { Credential } from "@opencode-ai/core/credential" -import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { LayerNode } from "@opencode-ai/core/effect/layer-node" -import { PermissionSaved } from "@opencode-ai/core/permission/saved" -import { Context, Layer, Option } from "effect" -import * as Effect from "effect/Effect" -import { HttpRouter, HttpServer } from "effect/unstable/http" -import { createServer } from "node:http" -import { createRoutes } from "@opencode-ai/server/routes" +import { Effect, Option } from "effect" import { Commands } from "../commands" import { Runtime } from "../../framework/runtime" -import { Daemon } from "../../services/daemon" +import { ServerProcess } from "../../server-process" export default Runtime.handler( Commands.commands.serve, - Effect.fn("cli.serve")(function* (input) { - return yield* Effect.scoped( - Effect.gen(function* () { - const daemon = yield* Daemon.Service - const address = yield* listen(input.hostname, input.port, yield* daemon.password()) - if (input.register) yield* daemon.register(address) - console.log(`server listening on ${HttpServer.formatAddress(address)}`) - return yield* Effect.never - }), - ) + Effect.fnUntraced(function* (input) { + if (input.service && input.stdio) return yield* Effect.fail(new Error("--service and --stdio cannot be combined")) + return yield* ServerProcess.run({ + mode: input.service ? "service" : input.stdio ? "stdio" : "default", + hostname: Option.getOrUndefined(input.hostname), + port: Option.getOrUndefined(input.port), + }) }), ) - -function listen(hostname: string, port: Option.Option, password: string) { - if (Option.isSome(port)) return bind(hostname, port.value, password) - const next = (port: number): ReturnType => - bind(hostname, port, password).pipe( - Effect.catch((error) => (port === 65_535 ? Effect.fail(error) : next(port + 1))), - ) - return next(4096) -} - -function bind(hostname: string, port: number, password: string) { - return Layer.build( - HttpRouter.serve(createRoutes(password), { disableListenLog: true, disableLogger: true }).pipe( - Layer.provideMerge(NodeHttpServer.layer(() => createServer(), { port, host: hostname })), - Layer.provide(AppNodeBuilder.build(LayerNode.group([Credential.node, PermissionSaved.node]))), - ), - ).pipe(Effect.map((context) => Context.get(context, HttpServer.HttpServer).address)) -} diff --git a/packages/cli/src/commands/handlers/service/get.ts b/packages/cli/src/commands/handlers/service/get.ts new file mode 100644 index 0000000000..fd4b9af384 --- /dev/null +++ b/packages/cli/src/commands/handlers/service/get.ts @@ -0,0 +1,12 @@ +import { EOL } from "os" +import { Effect, Option } from "effect" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" +import { ServiceConfig } from "../../../services/service-config" + +export default Runtime.handler( + Commands.commands.service.commands.get, + Effect.fn("cli.service.get")(function* (input) { + process.stdout.write((yield* ServiceConfig.get(Option.getOrUndefined(input.key))) + EOL) + }), +) diff --git a/packages/cli/src/commands/handlers/service/password.ts b/packages/cli/src/commands/handlers/service/password.ts deleted file mode 100644 index 6bf49d50d0..0000000000 --- a/packages/cli/src/commands/handlers/service/password.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { EOL } from "os" -import { Option } from "effect" -import * as Effect from "effect/Effect" -import { Commands } from "../../commands" -import { Runtime } from "../../../framework/runtime" -import { Daemon } from "../../../services/daemon" - -export default Runtime.handler( - Commands.commands.service.commands.password, - Effect.fn("cli.service.password")(function* (input) { - const daemon = yield* Daemon.Service - const value = Option.getOrUndefined(input.value) - if (value !== undefined) yield* daemon.stop() - process.stdout.write((yield* daemon.password(value)) + EOL) - }), -) diff --git a/packages/cli/src/commands/handlers/service/restart.ts b/packages/cli/src/commands/handlers/service/restart.ts index d348987d16..fe306dcb02 100644 --- a/packages/cli/src/commands/handlers/service/restart.ts +++ b/packages/cli/src/commands/handlers/service/restart.ts @@ -1,14 +1,16 @@ import { EOL } from "os" -import * as Effect from "effect/Effect" +import { Effect } from "effect" +import { Service } from "@opencode-ai/client/effect/service" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" -import { Daemon } from "../../../services/daemon" +import { ServiceConfig } from "../../../services/service-config" export default Runtime.handler( Commands.commands.service.commands.restart, Effect.fn("cli.service.restart")(function* () { - const daemon = yield* Daemon.Service - yield* daemon.stop() - process.stdout.write((yield* daemon.start()) + EOL) + const options = yield* ServiceConfig.options() + yield* Service.stop(options) + const transport = yield* Service.ensure(options) + process.stdout.write(transport.url + EOL) }), ) diff --git a/packages/cli/src/commands/handlers/service/set.ts b/packages/cli/src/commands/handlers/service/set.ts new file mode 100644 index 0000000000..f761c02411 --- /dev/null +++ b/packages/cli/src/commands/handlers/service/set.ts @@ -0,0 +1,11 @@ +import { Effect } from "effect" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" +import { ServiceConfig } from "../../../services/service-config" + +export default Runtime.handler( + Commands.commands.service.commands.set, + Effect.fn("cli.service.set")(function* (input) { + yield* ServiceConfig.set(input.key, input.value) + }), +) diff --git a/packages/cli/src/commands/handlers/service/start.ts b/packages/cli/src/commands/handlers/service/start.ts index 0d6fbaada9..dfdbac3997 100644 --- a/packages/cli/src/commands/handlers/service/start.ts +++ b/packages/cli/src/commands/handlers/service/start.ts @@ -1,12 +1,14 @@ import { EOL } from "os" -import * as Effect from "effect/Effect" +import { Effect } from "effect" +import { Service } from "@opencode-ai/client/effect/service" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" -import { Daemon } from "../../../services/daemon" +import { ServiceConfig } from "../../../services/service-config" export default Runtime.handler( Commands.commands.service.commands.start, Effect.fn("cli.service.start")(function* () { - process.stdout.write((yield* (yield* Daemon.Service).start()) + EOL) + const transport = yield* Service.ensure(yield* ServiceConfig.options()) + process.stdout.write(transport.url + EOL) }), ) diff --git a/packages/cli/src/commands/handlers/service/status.ts b/packages/cli/src/commands/handlers/service/status.ts index d409970e8b..8f27ab2ec2 100644 --- a/packages/cli/src/commands/handlers/service/status.ts +++ b/packages/cli/src/commands/handlers/service/status.ts @@ -1,13 +1,15 @@ import { EOL } from "os" -import * as Effect from "effect/Effect" +import { Effect } from "effect" +import { Service } from "@opencode-ai/client/effect/service" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" -import { Daemon } from "../../../services/daemon" +import { ServiceConfig } from "../../../services/service-config" export default Runtime.handler( Commands.commands.service.commands.status, Effect.fn("cli.service.status")(function* () { - const url = yield* (yield* Daemon.Service).status() - process.stdout.write((url ? `running ${url}` : "stopped") + EOL) + const options = yield* ServiceConfig.options() + const found = yield* Service.discover({ ...options, version: undefined }) + process.stdout.write((found?.url ?? "stopped") + EOL) }), ) diff --git a/packages/cli/src/commands/handlers/service/stop.ts b/packages/cli/src/commands/handlers/service/stop.ts index 8da9b04cff..ca2c164165 100644 --- a/packages/cli/src/commands/handlers/service/stop.ts +++ b/packages/cli/src/commands/handlers/service/stop.ts @@ -1,11 +1,12 @@ -import * as Effect from "effect/Effect" +import { Effect } from "effect" +import { Service } from "@opencode-ai/client/effect/service" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" -import { Daemon } from "../../../services/daemon" +import { ServiceConfig } from "../../../services/service-config" export default Runtime.handler( Commands.commands.service.commands.stop, Effect.fn("cli.service.stop")(function* () { - yield* (yield* Daemon.Service).stop() + yield* Service.stop(yield* ServiceConfig.options()) }), ) diff --git a/packages/cli/src/commands/handlers/service/unset.ts b/packages/cli/src/commands/handlers/service/unset.ts new file mode 100644 index 0000000000..cc738125d3 --- /dev/null +++ b/packages/cli/src/commands/handlers/service/unset.ts @@ -0,0 +1,11 @@ +import { Effect } from "effect" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" +import { ServiceConfig } from "../../../services/service-config" + +export default Runtime.handler( + Commands.commands.service.commands.unset, + Effect.fn("cli.service.unset")(function* (input) { + yield* ServiceConfig.unset(input.key) + }), +) diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts new file mode 100644 index 0000000000..d9f01b3fce --- /dev/null +++ b/packages/cli/src/config/config.ts @@ -0,0 +1,109 @@ +export * as Config from "./config" + +import { Global } from "@opencode-ai/core/global" +import { Context, Effect, FileSystem, Layer, Option, Schema, Semaphore } from "effect" +import { produce, type Draft } from "immer" +import { applyEdits, modify, parse, type ParseError } from "jsonc-parser" +import path from "path" +import { ConfigMigration } from "./migrate" +import { Info } from "./schema" + +export * from "./schema" + +export interface Interface { + readonly path: string + readonly get: () => Effect.Effect + readonly update: (update: (draft: Draft) => void) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/cli/config/Config") {} + +const decode = Schema.decodeUnknownOption(Info) +const decodeRecord = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Any)) +const empty: Info = {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + const global = yield* Global.Service + const file = path.join(global.config, "cli.json") + const lock = yield* Semaphore.make(1) + + const readJson = Effect.fnUntraced(function* () { + const text = yield* fs.readFileString(file).pipe(Effect.catch(() => Effect.succeed(undefined))) + if (text === undefined) return undefined + const errors: ParseError[] = [] + const value: any = parse(text, errors, { allowTrailingComma: true }) + if (errors.length) return undefined + return Option.getOrUndefined(decodeRecord(value)) + }) + + const write = Effect.fnUntraced(function* (text: string) { + const temp = file + ".tmp" + yield* fs.makeDirectory(path.dirname(file), { recursive: true }) + yield* fs.writeFileString(temp, text, { mode: 0o600 }) + yield* fs.rename(temp, file) + }) + + const migrate = ConfigMigration.run({ file, config: global.config, state: global.state }).pipe( + Effect.provideService(FileSystem.FileSystem, fs), + ) + + const get = Effect.fn("cli.config.get")(function* () { + yield* migrate.pipe(Effect.catchCause((cause) => Effect.logWarning("failed to migrate cli config", { cause }))) + return Option.getOrElse(decode(yield* readJson()), () => empty) + }) + + const update = Effect.fn("cli.config.update")((update: (draft: Draft) => void) => + lock + .withPermits(1)( + Effect.gen(function* () { + yield* migrate + const current = Option.getOrElse(decode(yield* readJson()), () => empty) + const next = produce(current, update) + const edits = changes(current, next) + if (!edits.length) return current + const text = yield* fs.readFileString(file).pipe(Effect.catch(() => Effect.succeed("{}"))) + const updated = edits.reduce( + (text, edit) => + applyEdits( + text, + modify(text, edit.path, edit.value, { formattingOptions: { tabSize: 2, insertSpaces: true } }), + ), + text, + ) + const errors: ParseError[] = [] + const config = Option.getOrUndefined(decode(parse(updated, errors, { allowTrailingComma: true }))) + if (errors.length || config === undefined) return yield* Effect.fail(new Error("Invalid CLI config update")) + yield* write(updated.endsWith("\n") ? updated : updated + "\n") + return config + }), + ) + .pipe(Effect.mapError((cause) => new Error("Failed to update CLI config", { cause }))), + ) + + return Service.of({ path: file, get, update }) + }), +) + +type Edit = { readonly path: (string | number)[]; readonly value: any } + +function changes(before: any, after: any, path: (string | number)[] = []): Edit[] { + if (Object.is(before, after)) return [] + if ( + before !== null && + after !== null && + typeof before === "object" && + typeof after === "object" && + !Array.isArray(before) && + !Array.isArray(after) + ) { + return [...new Set([...Object.keys(before), ...Object.keys(after)])].flatMap((key) => { + if (!(key in after)) return [{ path: [...path, key], value: undefined }] + if (!(key in before)) return [{ path: [...path, key], value: after[key] }] + return changes(before[key], after[key], [...path, key]) + }) + } + return [{ path, value: after }] +} diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts new file mode 100644 index 0000000000..60e39c3163 --- /dev/null +++ b/packages/cli/src/config/index.ts @@ -0,0 +1 @@ +export * as Config from "./config" diff --git a/packages/cli/src/config/migrate.ts b/packages/cli/src/config/migrate.ts new file mode 100644 index 0000000000..ca8070e25c --- /dev/null +++ b/packages/cli/src/config/migrate.ts @@ -0,0 +1,139 @@ +export * as ConfigMigration from "./migrate" + +import { TuiConfigV1 } from "@opencode-ai/tui/config/v1" +import { Effect, FileSystem, Option, Schema } from "effect" +import { parse, type ParseError } from "jsonc-parser" +import path from "path" +import type { Info } from "./schema" + +const decodeV1 = Schema.decodeUnknownOption(TuiConfigV1.Info) +const decodeRecord = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Any)) + +export const run = Effect.fn("cli.config.migrate")(function* (input: { + readonly file: string + readonly config: string + readonly state: string +}) { + const fs = yield* FileSystem.FileSystem + if (yield* fs.exists(input.file).pipe(Effect.orElseSucceed(() => false))) return + + const legacyValue = yield* readJson(path.join(input.config, "tui.json")) + const legacy = Option.getOrUndefined(decodeV1(legacyValue)) + const kv = yield* readJson(path.join(input.state, "kv.json")) + const migrated = migrateV1(legacy, kv ?? {}) + if (!Object.keys(migrated).length) return + + const temp = input.file + ".tmp" + yield* fs.makeDirectory(path.dirname(input.file), { recursive: true }) + yield* fs.writeFileString(temp, JSON.stringify(migrated, null, 2) + "\n", { mode: 0o600 }) + yield* fs.rename(temp, input.file) + yield* Effect.logInfo("migrated cli config", { + from: [ + legacyValue === undefined ? undefined : path.join(input.config, "tui.json"), + kv === undefined ? undefined : path.join(input.state, "kv.json"), + ].filter(Boolean), + to: input.file, + }) +}) + +export function migrateV1(legacy: TuiConfigV1.Info | undefined, kv: Record): Info { + const plugins = [ + ...(legacy?.plugin?.map((plugin) => + typeof plugin === "string" ? plugin : { package: plugin[0], options: plugin[1] }, + ) ?? []), + ...Object.entries(legacy?.plugin_enabled ?? {}).map(([id, enabled]) => (enabled ? id : `-${id}`)), + ] + const themeName = legacy?.theme ?? kv.theme + const themeMode = kv.theme_mode_lock + const attentionSoundPack = kv.attention_sound_pack + const diffView = kv.diff_viewer_view ?? (legacy?.diff_style === "stacked" ? "unified" : undefined) + const thinking = + kv.thinking_mode ?? + (kv.thinking_visibility === undefined ? undefined : kv.thinking_visibility ? "show" : "hide") + + return { + ...(themeName !== undefined || themeMode !== undefined + ? { theme: { ...(themeName === undefined ? {} : { name: themeName }), ...(themeMode === undefined ? {} : { mode: themeMode }) } } + : {}), + ...(legacy?.keybinds === undefined ? {} : { keybinds: legacy.keybinds }), + ...(plugins.length ? { plugins } : {}), + ...(legacy?.leader_timeout === undefined ? {} : { leader: { timeout: legacy.leader_timeout } }), + ...(legacy?.scroll_speed === undefined && legacy?.scroll_acceleration?.enabled === undefined + ? {} + : { + scroll: { + ...(legacy.scroll_speed === undefined ? {} : { speed: legacy.scroll_speed }), + ...(legacy.scroll_acceleration?.enabled === undefined + ? {} + : { acceleration: legacy.scroll_acceleration.enabled }), + }, + }), + ...(legacy?.attention === undefined && attentionSoundPack === undefined + ? {} + : { + attention: { + ...legacy?.attention, + ...(attentionSoundPack === undefined ? {} : { sound_pack: attentionSoundPack }), + }, + }), + ...(legacy?.diff_style === undefined && + kv.diff_wrap_mode === undefined && + kv.diff_viewer_show_file_tree === undefined && + kv.diff_viewer_single_patch === undefined && + diffView === undefined + ? {} + : { + diffs: { + ...(kv.diff_wrap_mode === undefined ? {} : { wrap: kv.diff_wrap_mode }), + ...(kv.diff_viewer_show_file_tree === undefined ? {} : { tree: kv.diff_viewer_show_file_tree }), + ...(kv.diff_viewer_single_patch === undefined ? {} : { single: kv.diff_viewer_single_patch }), + ...(diffView === undefined ? {} : { view: diffView }), + }, + }), + ...(kv.terminal_title_enabled === undefined ? {} : { terminal: { title: kv.terminal_title_enabled } }), + ...(kv.file_context_enabled === undefined && kv.paste_summary_enabled === undefined + ? {} + : { + prompt: { + ...(kv.file_context_enabled === undefined ? {} : { editor: kv.file_context_enabled }), + ...(kv.paste_summary_enabled === undefined + ? {} + : { paste: kv.paste_summary_enabled ? ("compact" as const) : ("full" as const) }), + }, + }), + ...(kv.sidebar === undefined && + kv.scrollbar_visible === undefined && + thinking === undefined && + kv.exploration_grouping === undefined + ? {} + : { + session: { + ...(kv.sidebar === undefined ? {} : { sidebar: kv.sidebar }), + ...(kv.scrollbar_visible === undefined ? {} : { scrollbar: kv.scrollbar_visible }), + ...(thinking === undefined ? {} : { thinking }), + ...(kv.exploration_grouping === undefined + ? {} + : { grouping: kv.exploration_grouping ? ("auto" as const) : ("none" as const) }), + }, + }), + ...(kv.dismissed_getting_started === undefined + ? {} + : { + hints: { + onboarding: !kv.dismissed_getting_started, + }, + }), + ...(kv.animations_enabled === undefined ? {} : { animations: kv.animations_enabled }), + ...(legacy?.mouse === undefined ? {} : { mouse: legacy.mouse }), + } +} + +const readJson = Effect.fnUntraced(function* (target: string) { + const fs = yield* FileSystem.FileSystem + const text = yield* fs.readFileString(target).pipe(Effect.catch(() => Effect.succeed(undefined))) + if (text === undefined) return undefined + const errors: ParseError[] = [] + const value: any = parse(text, errors, { allowTrailingComma: true }) + if (errors.length) return undefined + return Option.getOrUndefined(decodeRecord(value)) +}) diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts new file mode 100644 index 0000000000..fc6b9724c6 --- /dev/null +++ b/packages/cli/src/config/schema.ts @@ -0,0 +1,5 @@ +import { Config } from "@opencode-ai/tui/config" +import { Schema } from "effect" + +export const Info = Schema.Struct({ ...Config.Info.fields }) +export type Info = Schema.Schema.Type diff --git a/packages/cli/src/env.ts b/packages/cli/src/env.ts new file mode 100644 index 0000000000..6cc76b793f --- /dev/null +++ b/packages/cli/src/env.ts @@ -0,0 +1,15 @@ +import { Config } from "effect" + +// Every environment variable the CLI reads, in one place. Consumers yield +// these instead of touching process.env so the full surface stays visible, +// typed, and redacted where secret. + +// The opencode server password: sent by clients connecting to an explicit +// --server, and adopted by a manually run or standalone server. The legacy +// name is still honored. +export const password = Config.redacted("OPENCODE_PASSWORD").pipe( + Config.orElse(() => Config.redacted("OPENCODE_SERVER_PASSWORD")), + Config.withDefault(undefined), +) + +export * as Env from "./env" diff --git a/packages/cli/src/framework/runtime.ts b/packages/cli/src/framework/runtime.ts index 97247e4d6b..a353a38a20 100644 --- a/packages/cli/src/framework/runtime.ts +++ b/packages/cli/src/framework/runtime.ts @@ -1,7 +1,10 @@ -import * as Effect from "effect/Effect" -import * as Command from "effect/unstable/cli/Command" +import { Effect, FileSystem, Scope } from "effect" +import { Command } from "effect/unstable/cli" import { Spec } from "./spec" -import { Daemon } from "../services/daemon" +import { Global } from "@opencode-ai/core/global" +import { Updater } from "../services/updater" +import { Config } from "../config" +import { Npm } from "@opencode-ai/core/npm" export type Input = Value extends Spec.Node @@ -10,11 +13,29 @@ export type Input = ? Input : never -type RuntimeHandler = (input: unknown) => Effect.Effect +type RuntimeHandler = ( + input: unknown, +) => Effect.Effect< + void, + unknown, + FileSystem.FileSystem | Global.Service | Npm.Service | Updater.Service | Config.Service | Scope.Scope +> type Loader = () => Promise<{ - default: (input: Input) => Effect.Effect + default: ( + input: Input, + ) => Effect.Effect< + void, + any, + FileSystem.FileSystem | Global.Service | Npm.Service | Updater.Service | Config.Service | Scope.Scope + > }> -type ProvidedCommand = Command.Command +type ProvidedCommand = Command.Command< + string, + unknown, + unknown, + unknown, + FileSystem.FileSystem | Global.Service | Npm.Service | Updater.Service | Config.Service | Scope.Scope +> export type Handlers = keyof Node["commands"] extends never ? Loader diff --git a/packages/cli/src/framework/spec.ts b/packages/cli/src/framework/spec.ts index 3bb47e5e5e..345a0cfa85 100644 --- a/packages/cli/src/framework/spec.ts +++ b/packages/cli/src/framework/spec.ts @@ -1,4 +1,4 @@ -import * as Command from "effect/unstable/cli/Command" +import { Command } from "effect/unstable/cli" type Options> = { readonly description?: string diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 4b9303f7c3..7ee80846ed 100755 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,32 +1,69 @@ #!/usr/bin/env bun -import * as NodeRuntime from "@effect/platform-node/NodeRuntime" -import * as NodeServices from "@effect/platform-node/NodeServices" -import * as Effect from "effect/Effect" +import { NodeRuntime, NodeServices } from "@effect/platform-node" +import { Effect } from "effect" import { Commands } from "./commands/commands" import { Runtime } from "./framework/runtime" -import { Daemon } from "./services/daemon" +import { Observability } from "@opencode-ai/core/observability" +import { Updater } from "./services/updater" +import { InstallationChannel, InstallationVersion, InstallationLocal } from "@opencode-ai/core/installation/version" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { Global } from "@opencode-ai/core/global" +import { AppProcess } from "@opencode-ai/core/process" +import { Config } from "./config" +import { Npm } from "@opencode-ai/core/npm" const Handlers = Runtime.handlers(Commands, { $: () => import("./commands/handlers/default"), api: () => import("./commands/handlers/api"), + auth: { + connect: () => import("./commands/handlers/auth/connect"), + }, debug: { agents: () => import("./commands/handlers/debug/agents"), }, + console: { + login: () => import("./commands/handlers/console/login"), + }, + mcp: { + list: () => import("./commands/handlers/mcp/list"), + add: () => import("./commands/handlers/mcp/add"), + auth: () => import("./commands/handlers/mcp/auth"), + logout: () => import("./commands/handlers/mcp/logout"), + }, + plugin: { + list: () => import("./commands/handlers/plugin/list"), + }, migrate: () => import("./commands/handlers/migrate"), + mini: () => import("./commands/handlers/mini"), + run: () => import("./commands/handlers/run"), + pair: () => import("./commands/handlers/pair"), service: { start: () => import("./commands/handlers/service/start"), restart: () => import("./commands/handlers/service/restart"), status: () => import("./commands/handlers/service/status"), stop: () => import("./commands/handlers/service/stop"), - password: () => import("./commands/handlers/service/password"), + get: () => import("./commands/handlers/service/get"), + set: () => import("./commands/handlers/service/set"), + unset: () => import("./commands/handlers/service/unset"), }, serve: () => import("./commands/handlers/serve"), }) -Runtime.run(Commands, Handlers, { version: "local" }).pipe( - Effect.provide(Daemon.layer), +Effect.logInfo("cli starting", { + version: InstallationVersion, + channel: InstallationChannel, + local: InstallationLocal, + args: process.argv.slice(2), +}).pipe( + Effect.flatMap(() => Runtime.run(Commands, Handlers, { version: InstallationVersion })), + Effect.annotateLogs({ role: "cli" }), + Effect.provide(Config.layer), + Effect.provide(Updater.layer), + Effect.provide(LayerNode.compile(LayerNode.group([Global.node, AppProcess.node, Npm.node]))), + Effect.provide(Observability.layer), Effect.provide(NodeServices.layer), Effect.scoped, + Effect.tap(() => Effect.sync(() => process.exit(process.exitCode ?? 0))), NodeRuntime.runMain, ) diff --git a/packages/cli/src/mini/catalog.shared.ts b/packages/cli/src/mini/catalog.shared.ts new file mode 100644 index 0000000000..0868d3a94b --- /dev/null +++ b/packages/cli/src/mini/catalog.shared.ts @@ -0,0 +1,159 @@ +import type { + AgentListOutput, + CommandListOutput, + ModelListOutput, + OpenCodeClient, + ProviderListOutput, + SkillListOutput, +} from "@opencode-ai/client/promise" +import type { RunAgent, RunCommand, RunProvider, RunReference } from "./types" + +type CurrentAgent = AgentListOutput["data"][number] +type CurrentCommand = CommandListOutput["data"][number] +type CurrentSkill = SkillListOutput["data"][number] +type CurrentProvider = ProviderListOutput["data"][number] +type CurrentModel = ModelListOutput["data"][number] + +function location(directory: string, workspace?: string) { + return { + location: { + directory, + workspace, + }, + } +} + +function defaultCost(model: CurrentModel) { + const picked = model.cost.find((cost) => cost.tier === undefined) ?? model.cost[0] + if (!picked) { + return undefined + } + + return { + ...picked, + input: model.cost.every((cost) => cost.input === 0) ? 0 : picked.input, + } +} + +export function runAgent(input: CurrentAgent): RunAgent { + return { + id: input.id, + name: input.name, + description: input.description, + mode: input.mode, + hidden: input.hidden, + } +} + +export function runCommand(input: CurrentCommand): RunCommand { + return { + name: input.name, + description: input.description, + } +} + +export function runSkill(input: CurrentSkill): RunCommand { + return { + name: input.id, + description: input.description, + source: "skill", + } +} + +export function runProviders(providers: CurrentProvider[], models: CurrentModel[]): RunProvider[] { + const grouped = new Map() + + for (const provider of providers) { + grouped.set(provider.id, { + id: provider.id, + name: provider.name, + models: {}, + }) + } + + for (const model of models) { + const provider = grouped.get(model.providerID) ?? { + id: model.providerID, + name: model.providerID, + models: {}, + } + provider.models[model.id] = { + id: model.id, + providerID: model.providerID, + name: model.name, + capabilities: model.capabilities, + cost: defaultCost(model), + limit: model.limit, + status: model.status, + variants: Object.fromEntries((model.variants ?? []).map((variant) => [variant.id, {}])), + } + grouped.set(provider.id, provider) + } + + return [...grouped.values()] +} + +// A location boots its plugins in a deferred background batch after the layer +// is built, so first-turn model resolution can observe empty catalog state. +// For explicit --model flows, wait for that exact ref to appear before prompt +// admission. On timeout, return and let the real execution error surface. +export async function waitForCatalogReady(input: { + sdk: OpenCodeClient + directory: string + workspace?: string + model: { providerID: string; modelID: string } + timeoutMs?: number +}) { + const deadline = Date.now() + (input.timeoutMs ?? 5_000) + while (Date.now() < deadline) { + const models = await input.sdk.model + .list(location(input.directory, input.workspace)) + .then((result) => result.data) + .catch(() => undefined) + if (models?.some((model) => model.providerID === input.model.providerID && model.id === input.model.modelID)) return + await new Promise((resolve) => setTimeout(resolve, 25)) + } +} + +export async function waitForDefaultModel(input: { + sdk: OpenCodeClient + directory: string + timeoutMs?: number + active?: () => boolean +}): Promise<{ providerID: string; modelID: string } | undefined> { + const deadline = Date.now() + (input.timeoutMs ?? 5_000) + while (Date.now() < deadline && (input.active?.() ?? true)) { + const model = await input.sdk.model + .default(location(input.directory)) + .then((result) => result.data) + .catch(() => undefined) + if (model) return { providerID: model.providerID, modelID: model.id } + await new Promise((resolve) => setTimeout(resolve, 25)) + } +} + +export async function loadRunAgents(sdk: OpenCodeClient, directory: string): Promise { + const result = await sdk.agent.list(location(directory)) + return result.data.map(runAgent) +} + +export async function loadRunCommands(sdk: OpenCodeClient, directory: string): Promise { + const [commands, skills] = await Promise.all([ + sdk.command.list(location(directory)), + sdk.skill.list(location(directory)), + ]) + return [...commands.data.map(runCommand), ...skills.data.filter((skill) => skill.slash !== false).map(runSkill)] +} + +export async function loadRunReferences(sdk: OpenCodeClient, directory: string): Promise { + const result = await sdk.reference.list(location(directory)) + return result.data.filter((reference) => !reference.hidden) +} + +export async function loadRunProviders(sdk: OpenCodeClient, directory: string): Promise { + const [providers, models] = await Promise.all([ + sdk.provider.list(location(directory)), + sdk.model.list(location(directory)), + ]) + return runProviders([...providers.data], [...models.data]) +} diff --git a/packages/opencode/src/cli/cmd/run/demo.ts b/packages/cli/src/mini/demo.ts similarity index 74% rename from packages/opencode/src/cli/cmd/run/demo.ts rename to packages/cli/src/mini/demo.ts index 94450f1242..930f278ef0 100644 --- a/packages/opencode/src/cli/cmd/run/demo.ts +++ b/packages/cli/src/mini/demo.ts @@ -1,7 +1,7 @@ // Demo mode for testing direct interactive mode without a real SDK. // -// Enabled with `--demo`. Intercepts prompt submissions and generates synthetic -// SDK events that feed through the real reducer and footer pipeline. This +// Enabled with `--demo`. Intercepts prompt submissions and drives the same +// presentation commits and footer actions as the live transport. This // lets you test scrollback formatting, permission UI, question UI, and tool // snapshots without making actual model calls. Pass a demo slash command as // the initial interactive message to trigger a preview immediately. @@ -9,33 +9,40 @@ // Slash commands: // /permission [kind] → triggers a permission request variant // /question [kind] → triggers a question request variant -// /fmt → emits a specific tool/text type (text, reasoning, bash, -// write, edit, patch, task, todo, question, error, mix) +// /fmt → emits a specific tool/text type (text, reasoning, shell, +// write, edit, patch, task, question, error, mix) // // Demo mode also handles permission and question replies locally, completing // or failing the synthetic tool parts as appropriate. import path from "path" -import type { Event, ToolPart } from "@opencode-ai/sdk/v2" -import { createSessionData, reduceSessionData, type SessionData } from "./session-data" +import type { PermissionV2Request, QuestionV2Request } from "@opencode-ai/client/promise" import { writeSessionOutput } from "./stream" -import type { FooterApi, PermissionReply, QuestionReject, QuestionReply, RunPrompt, StreamCommit } from "./types" +import { toolCommit } from "./stream-v2.subagent" +import type { + FooterApi, + MiniToolPart, + PermissionReply, + QuestionReject, + QuestionReply, + RunPrompt, + StreamCommit, +} from "./types" const KINDS = [ "markdown", "table", "text", "reasoning", - "bash", + "shell", "write", "edit", "patch", "task", - "todo", "question", "error", "mix", ] -const PERMISSIONS = ["edit", "bash", "read", "task", "external", "doom"] as const +const PERMISSIONS = ["edit", "shell", "read", "task", "external", "doom"] as const const QUESTIONS = ["multi", "single", "checklist", "custom"] as const type PermissionKind = (typeof PERMISSIONS)[number] @@ -125,7 +132,7 @@ type Permit = { ref: Ref permission: string patterns: string[] - metadata?: Record + metadata?: PermissionV2Request["metadata"] always: string[] done: Perm["done"] } @@ -133,9 +140,7 @@ type Permit = { type State = { id: string thinking: boolean - data: SessionData footer: FooterApi - limits: () => Record msg: number part: number call: number @@ -143,12 +148,12 @@ type State = { ask: number perms: Map asks: Map + started: Set } type Input = { sessionID: string thinking: boolean - limits: () => Record footer: FooterApi } @@ -256,185 +261,69 @@ function take(state: State, key: "msg" | "part" | "call" | "perm" | "ask", prefi return `demo_${prefix}_${state[key]}` } -function feed(state: State, event: Event): void { - const out = reduceSessionData({ - data: state.data, - event, - sessionID: state.id, - thinking: state.thinking, - limits: state.limits(), - }) - state.data = out.data +function present(state: State, commits: StreamCommit[], view?: QuestionV2Request | PermissionV2Request): void { writeSessionOutput( + { footer: state.footer }, { - footer: state.footer, + commits, + footer: view + ? { + view: "action" in view ? { type: "permission", request: view } : { type: "question", request: view }, + patch: { status: "action" in view ? "awaiting permission" : "awaiting answer" }, + } + : undefined, }, - out, + ) +} + +function clearBlocker(state: State): void { + writeSessionOutput( + { footer: state.footer }, + { commits: [], footer: { view: { type: "prompt" }, patch: { status: "" } } }, ) } function open(state: State): string { - const id = take(state, "msg", "msg") - feed(state, { - type: "message.updated", - properties: { - sessionID: state.id, - info: { - id, - sessionID: state.id, - role: "assistant", - time: { - created: Date.now(), - }, - parentID: `user_${id}`, - modelID: "demo", - providerID: "demo", - mode: "demo", - agent: "demo", - path: { - cwd: process.cwd(), - root: process.cwd(), - }, - cost: 0.001, - tokens: { - input: 120, - output: 320, - reasoning: 80, - cache: { - read: 0, - write: 0, - }, - }, - }, - }, - } as Event) - return id + return take(state, "msg", "msg") } async function emitText(state: State, body: string, signal?: AbortSignal): Promise { const msg = open(state) const part = take(state, "part", "part") - const start = Date.now() - - feed(state, { - type: "message.part.updated", - properties: { - sessionID: state.id, - time: Date.now(), - part: { - id: part, - sessionID: state.id, - messageID: msg, - type: "text", - text: "", - time: { - start, - }, - }, - }, - } as Event) - - let next = "" for (const item of split(body)) { if (signal?.aborted) { return } - next += item - feed(state, { - type: "message.part.delta", - properties: { - sessionID: state.id, - messageID: msg, - partID: part, - field: "text", - delta: item, - }, - } as Event) + present(state, [{ kind: "assistant", source: "assistant", text: item, phase: "progress", messageID: msg, partID: part }]) await wait(45, signal) } - - feed(state, { - type: "message.part.updated", - properties: { - sessionID: state.id, - time: Date.now(), - part: { - id: part, - sessionID: state.id, - messageID: msg, - type: "text", - text: next, - time: { - start, - end: Date.now(), - }, - }, - }, - } as Event) } async function emitReasoning(state: State, body: string, signal?: AbortSignal): Promise { const msg = open(state) const part = take(state, "part", "part") - const start = Date.now() - - feed(state, { - type: "message.part.updated", - properties: { - sessionID: state.id, - time: Date.now(), - part: { - id: part, - sessionID: state.id, - messageID: msg, - type: "reasoning", - text: "", - time: { - start, - }, - }, - }, - } as Event) - - let next = "" + let first = true for (const item of split(body)) { if (signal?.aborted) { return } - next += item - feed(state, { - type: "message.part.delta", - properties: { - sessionID: state.id, - messageID: msg, - partID: part, - field: "text", - delta: item, - }, - } as Event) + if (state.thinking) { + present(state, [ + { + kind: "reasoning", + source: "reasoning", + text: first ? `Thinking: ${item.replace(/\[REDACTED\]/g, "")}` : item.replace(/\[REDACTED\]/g, ""), + phase: "progress", + messageID: msg, + partID: part, + }, + ]) + first = false + } await wait(45, signal) } - - feed(state, { - type: "message.part.updated", - properties: { - sessionID: state.id, - time: Date.now(), - part: { - id: part, - sessionID: state.id, - messageID: msg, - type: "reasoning", - text: next, - time: { - start, - end: Date.now(), - }, - }, - }, - } as Event) } function make(state: State, tool: string, input: Record): Ref { @@ -449,29 +338,23 @@ function make(state: State, tool: string, input: Record): Ref { } function startTool(state: State, ref: Ref, metadata: Record = {}): void { - feed(state, { - type: "message.part.updated", - properties: { - sessionID: state.id, - time: Date.now(), - part: { - id: ref.part, - sessionID: state.id, - messageID: ref.msg, - type: "tool", - callID: ref.call, - tool: ref.tool, - state: { - status: "running", - input: ref.input, - metadata, - time: { - start: ref.start, - }, + state.started.add(ref.part) + present( + state, + [ + toolCommit( + { + id: ref.part, + sessionID: state.id, + messageID: ref.msg, + callID: ref.call, + tool: ref.tool, + state: { status: "running", input: ref.input, metadata, time: { start: ref.start } }, }, - }, - }, - } as Event) + "start", + ), + ], + ) } function askPermission(state: State, item: Permit): void { @@ -483,21 +366,15 @@ function askPermission(state: State, item: Permit): void { done: item.done, }) - feed(state, { - type: "permission.asked", - properties: { - id, - sessionID: state.id, - permission: item.permission, - patterns: item.patterns, - metadata: item.metadata ?? {}, - always: item.always, - tool: { - messageID: item.ref.msg, - callID: item.ref.call, - }, - }, - } as Event) + present(state, [], { + id, + sessionID: state.id, + action: item.permission, + resources: item.patterns, + metadata: item.metadata ?? {}, + save: item.always, + source: { type: "tool", messageID: item.ref.msg, callID: item.ref.call }, + }) } function doneTool( @@ -509,81 +386,57 @@ function doneTool( metadata?: Record }, ): void { - feed(state, { - type: "message.part.updated", - properties: { - sessionID: state.id, - time: Date.now(), - part: { - id: ref.part, - sessionID: state.id, - messageID: ref.msg, - type: "tool", - callID: ref.call, - tool: ref.tool, - state: { - status: "completed", - input: ref.input, - output: output.output, - title: output.title, - metadata: output.metadata ?? {}, - time: { - start: ref.start, - end: Date.now(), - }, - }, - }, + if (!state.started.has(ref.part)) startTool(state, ref) + const part: MiniToolPart = { + id: ref.part, + sessionID: state.id, + messageID: ref.msg, + callID: ref.call, + tool: ref.tool, + state: { + status: "completed", + input: ref.input, + output: output.output, + title: output.title, + metadata: output.metadata ?? {}, + time: { start: ref.start, end: Date.now() }, }, - } as Event) + } + present(state, [toolCommit(part, output.output ? "progress" : "final")]) } function failTool(state: State, ref: Ref, error: string): void { - feed(state, { - type: "message.part.updated", - properties: { - sessionID: state.id, - time: Date.now(), - part: { - id: ref.part, - sessionID: state.id, - messageID: ref.msg, - type: "tool", - callID: ref.call, - tool: ref.tool, - state: { - status: "error", - input: ref.input, - error, - metadata: {}, - time: { - start: ref.start, - end: Date.now(), + if (!state.started.has(ref.part)) startTool(state, ref) + present( + state, + [ + toolCommit( + { + id: ref.part, + sessionID: state.id, + messageID: ref.msg, + callID: ref.call, + tool: ref.tool, + state: { + status: "error", + input: ref.input, + error, + metadata: {}, + time: { start: ref.start, end: Date.now() }, }, }, - }, - }, - } as Event) + "final", + ), + ], + ) } function emitError(state: State, text: string): void { - const event = { - id: `session.error:${state.id}:${Date.now()}`, - type: "session.error", - properties: { - sessionID: state.id, - error: { - name: "UnknownError", - data: { - message: text, - }, - }, - }, - } satisfies Event - feed(state, event) + present(state, [{ kind: "error", source: "system", text, phase: "start" }]) } async function emitBash(state: State, signal?: AbortSignal): Promise { - const ref = make(state, "bash", { + const ref = make(state, "shell", { command: "git status", workdir: process.cwd(), description: "Show git status", @@ -628,11 +481,11 @@ function emitEdit(state: State): void { function emitPatch(state: State): void { const file = path.join(process.cwd(), "src", "demo-format.ts") - const ref = make(state, "apply_patch", { + const ref = make(state, "patch", { patchText: "*** Begin Patch\n*** End Patch", }) doneTool(state, ref, { - title: "apply_patch", + title: "patch", output: "", metadata: { files: [ @@ -678,7 +531,7 @@ function emitTask(state: State): void { state: { status: "running", input: { - filePath: "packages/opencode/src/cli/cmd/run/stream.ts", + filePath: "packages/cli/src/mini/stream.ts", offset: 1, limit: 200, }, @@ -686,7 +539,7 @@ function emitTask(state: State): void { start: Date.now(), }, }, - } satisfies ToolPart + } satisfies MiniToolPart showSubagent(state, { sessionID: "sub_demo_1", partID: ref.part, @@ -733,30 +586,6 @@ function emitTask(state: State): void { }) } -function emitTodo(state: State): void { - const ref = make(state, "todowrite", { - todos: [ - { - content: "Trigger permission UI", - status: "completed", - }, - { - content: "Trigger question UI", - status: "in_progress", - }, - { - content: "Tune tool formatting", - status: "pending", - }, - ], - }) - doneTool(state, ref, { - title: "todowrite", - output: "", - metadata: {}, - }) -} - function emitQuestionTool(state: State): void { const ref = make(state, "question", { questions: [ @@ -794,16 +623,16 @@ function emitPermission(state: State, kind: PermissionKind = "edit"): void { const root = process.cwd() const file = path.join(root, "src", "demo-format.ts") - if (kind === "bash") { + if (kind === "shell") { const command = "git status --short" - const ref = make(state, "bash", { + const ref = make(state, "shell", { command, workdir: root, description: "Inspect worktree changes", }) askPermission(state, { ref, - permission: "bash", + permission: "shell", patterns: [command], always: ["*"], done: { @@ -952,7 +781,6 @@ function emitQuestion(state: State, kind: QuestionKind = "multi"): void { options: [ { label: "Diff", description: "Show an edit diff in the footer" }, { label: "Task", description: "Show a structured task summary" }, - { label: "Todo", description: "Show a todo snapshot" }, { label: "Error", description: "Show an error transcript row" }, ], multiple: true, @@ -992,7 +820,6 @@ function emitQuestion(state: State, kind: QuestionKind = "multi"): void { options: [ { label: "Diff", description: "Emit edit diff" }, { label: "Task", description: "Emit task card" }, - { label: "Todo", description: "Emit todo card" }, ], multiple: true, custom: true, @@ -1006,18 +833,12 @@ function emitQuestion(state: State, kind: QuestionKind = "multi"): void { const id = take(state, "ask", "ask") state.asks.set(id, { ref }) - feed(state, { - type: "question.asked", - properties: { - id, - sessionID: state.id, - questions, - tool: { - messageID: ref.msg, - callID: ref.call, - }, - }, - } as Event) + present(state, [], { + id, + sessionID: state.id, + questions, + tool: { messageID: ref.msg, callID: ref.call }, + }) } async function emitFmt(state: State, kind: string, body: string, signal?: AbortSignal): Promise { @@ -1041,7 +862,7 @@ async function emitFmt(state: State, kind: string, body: string, signal?: AbortS return true } - if (kind === "bash") { + if (kind === "shell") { await emitBash(state, signal) return true } @@ -1066,11 +887,6 @@ async function emitFmt(state: State, kind: string, body: string, signal?: AbortS return true } - if (kind === "todo") { - emitTodo(state) - return true - } - if (kind === "question") { emitQuestionTool(state) return true @@ -1091,7 +907,6 @@ async function emitFmt(state: State, kind: string, body: string, signal?: AbortS emitEdit(state) emitPatch(state) emitTask(state) - emitTodo(state) emitQuestionTool(state) emitError(state, "demo mixed scenario error") return true @@ -1109,7 +924,7 @@ function intro(state: State): void { `- /question [kind] (${QUESTIONS.join(", ")})`, `- /fmt (${KINDS.join(", ")})`, "Examples:", - "- /permission bash", + "- /permission shell", "- /question custom", "- /fmt markdown", "- /fmt table", @@ -1122,9 +937,7 @@ export function createRunDemo(input: Input) { const state: State = { id: input.sessionID, thinking: input.thinking, - data: createSessionData(), footer: input.footer, - limits: input.limits, msg: 0, part: 0, call: 0, @@ -1132,6 +945,7 @@ export function createRunDemo(input: Input) { ask: 0, perms: new Map(), asks: new Map(), + started: new Set(), } const start = async (): Promise => { @@ -1199,16 +1013,7 @@ export function createRunDemo(input: Input) { } state.perms.delete(input.requestID) - const event = { - id: `permission.replied:${input.requestID}:${Date.now()}`, - type: "permission.replied", - properties: { - sessionID: state.id, - requestID: input.requestID, - reply: input.reply, - }, - } satisfies Event - feed(state, event) + clearBlocker(state) if (input.reply === "reject") { failTool(state, item.ref, input.message || "permission rejected") @@ -1226,16 +1031,7 @@ export function createRunDemo(input: Input) { } state.asks.delete(input.requestID) - const event = { - id: `question.replied:${input.requestID}:${Date.now()}`, - type: "question.replied", - properties: { - sessionID: state.id, - requestID: input.requestID, - answers: input.answers, - }, - } satisfies Event - feed(state, event) + clearBlocker(state) doneTool(state, ask.ref, { title: "question", output: "", @@ -1253,13 +1049,7 @@ export function createRunDemo(input: Input) { } state.asks.delete(input.requestID) - feed(state, { - type: "question.rejected", - properties: { - sessionID: state.id, - requestID: input.requestID, - }, - } as Event) + clearBlocker(state) failTool(state, ask.ref, "question rejected") return true } diff --git a/packages/opencode/src/cli/cmd/run/entry.body.ts b/packages/cli/src/mini/entry.body.ts similarity index 100% rename from packages/opencode/src/cli/cmd/run/entry.body.ts rename to packages/cli/src/mini/entry.body.ts diff --git a/packages/opencode/src/cli/cmd/run/footer.command.tsx b/packages/cli/src/mini/footer.command.tsx similarity index 100% rename from packages/opencode/src/cli/cmd/run/footer.command.tsx rename to packages/cli/src/mini/footer.command.tsx diff --git a/packages/opencode/src/cli/cmd/run/footer.menu.tsx b/packages/cli/src/mini/footer.menu.tsx similarity index 96% rename from packages/opencode/src/cli/cmd/run/footer.menu.tsx rename to packages/cli/src/mini/footer.menu.tsx index 7e820cc6a7..1350d1f52f 100644 --- a/packages/opencode/src/cli/cmd/run/footer.menu.tsx +++ b/packages/cli/src/mini/footer.menu.tsx @@ -3,7 +3,8 @@ import { TextAttributes, type ColorInput } from "@opentui/core" import { useTerminalDimensions } from "@opentui/solid" import { createEffect, createMemo, createSignal, type Accessor } from "solid-js" import { transparent, type RunFooterTheme } from "./theme" -import * as Locale from "@/util/locale" +import { Locale } from "@opencode-ai/tui/util/locale" +import { stringWidth } from "@opencode-ai/tui/util/string-width" export const FOOTER_MENU_ROWS = 8 @@ -196,7 +197,7 @@ export function RunFooterMenu(props: { ...props .items() .filter((item) => item.description) - .map((item) => Bun.stringWidth(item.display)), + .map((item) => stringWidth(item.display)), ) return width === 0 ? 0 : width + 2 }) @@ -205,14 +206,14 @@ export function RunFooterMenu(props: { return "" } - return " ".repeat(Math.max(1, descriptionColumn() - Bun.stringWidth(item.display))) + return " ".repeat(Math.max(1, descriptionColumn() - stringWidth(item.display))) } const descriptionText = (item: RunFooterMenuItem) => { if (!item.description) { return } - const footerWidth = item.footer ? Bun.stringWidth(item.footer) + 1 : 0 + const footerWidth = item.footer ? stringWidth(item.footer) + 1 : 0 const available = term().width - (border() ? 1 : 0) - diff --git a/packages/opencode/src/cli/cmd/run/footer.permission.tsx b/packages/cli/src/mini/footer.permission.tsx similarity index 97% rename from packages/opencode/src/cli/cmd/run/footer.permission.tsx rename to packages/cli/src/mini/footer.permission.tsx index 70cc2064fc..c9629364b4 100644 --- a/packages/opencode/src/cli/cmd/run/footer.permission.tsx +++ b/packages/cli/src/mini/footer.permission.tsx @@ -14,7 +14,7 @@ import type { TextareaRenderable } from "@opentui/core" import { useKeyboard, useTerminalDimensions } from "@opentui/solid" import { For, Match, Show, Switch, createEffect, createMemo, createSignal } from "solid-js" -import type { PermissionRequest } from "@opencode-ai/sdk/v2" +import type { PermissionV2Request } from "@opencode-ai/client/promise" import { createPermissionBodyState, permissionAlwaysLines, @@ -130,7 +130,7 @@ export function RejectField(props: { } export function RunPermissionBody(props: { - request: PermissionRequest + request: PermissionV2Request theme: RunFooterTheme block: RunBlockTheme diffStyle?: RunDiffStyle @@ -141,7 +141,9 @@ export function RunPermissionBody(props: { const info = createMemo(() => permissionInfo(props.request)) const ft = createMemo(() => toolFiletype(info().file)) const narrow = createMemo(() => footerWidthPolicy(dims().width).dialog.narrow) - const opts = createMemo(() => permissionOptions(state().stage)) + const opts = createMemo(() => + permissionOptions(state().stage).filter((option) => option !== "always" || (props.request.save?.length ?? 0) > 0), + ) const busy = createMemo(() => state().submitting) const title = createMemo(() => { if (state().stage === "always") { @@ -165,7 +167,7 @@ export function RunPermissionBody(props: { }) const shift = (dir: -1 | 1) => { - setState((prev) => permissionShift(prev, dir)) + setState((prev) => permissionShift(prev, dir, opts())) } const submit = async (next: PermissionReply) => { diff --git a/packages/opencode/src/cli/cmd/run/footer.prompt.tsx b/packages/cli/src/mini/footer.prompt.tsx similarity index 87% rename from packages/opencode/src/cli/cmd/run/footer.prompt.tsx rename to packages/cli/src/mini/footer.prompt.tsx index 0280982d50..324cb62de9 100644 --- a/packages/opencode/src/cli/cmd/run/footer.prompt.tsx +++ b/packages/cli/src/mini/footer.prompt.tsx @@ -1,18 +1,19 @@ // Prompt composer and its state machine for direct interactive mode. // // createPromptState() wires keymap command layers, history navigation, and -// `@` autocomplete for files, subagents, and MCP resources. +// `@` autocomplete for files, subagents, and project references. // It produces a PromptState that RunPromptBody renders as a slim single-line // composer while the footer view renders any active menus below it. /** @jsxImportSource @opentui/solid */ -import { pathToFileURL } from "bun" import { StyledText, fg, type ColorInput, type KeyEvent, type TextareaRenderable } from "@opentui/core" import { useRenderer } from "@opentui/solid" -import { normalizePromptContent } from "@opencode-ai/tui/editor" +import { normalizePromptContent } from "@opencode-ai/tui/prompt/content" import fuzzysort from "fuzzysort" import path from "path" +import { pathToFileURL } from "node:url" import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, type Accessor } from "solid-js" -import * as Locale from "@/util/locale" +import { Locale } from "@opencode-ai/tui/util/locale" +import { stringWidth } from "@opencode-ai/tui/util/string-width" import { createPromptHistory, displayCharAt, @@ -23,11 +24,11 @@ import { movePromptHistory, pushPromptHistory, } from "./prompt.shared" -import { OPENCODE_BASE_MODE, useBindings } from "@opencode-ai/tui/keymap" +import { Keymap } from "@opencode-ai/tui/context/keymap" import { realignEditorPromptParts, resolveEditorSlashValue } from "./prompt.editor" import { FOOTER_MENU_ROWS, createFooterMenuState, type RunFooterMenuItem } from "./footer.menu" import type { RunFooterTheme } from "./theme" -import type { FooterState, RunAgent, RunCommand, RunPrompt, RunPromptPart, RunResource, RunTuiConfig } from "./types" +import type { FooterState, RunAgent, RunCommand, RunPrompt, RunPromptPart, RunReference, RunTuiConfig } from "./types" const AUTOCOMPLETE_ROWS = FOOTER_MENU_ROWS const AUTOCOMPLETE_BOTTOM_ROWS = 1 @@ -59,7 +60,7 @@ type PromptInput = { directory: string findFiles: (query: string) => Promise agents: Accessor - resources: Accessor + references: Accessor commands: Accessor tuiConfig: RunTuiConfig state: Accessor @@ -67,7 +68,7 @@ type PromptInput = { prompt: Accessor width: Accessor theme: Accessor - history?: RunPrompt[] + history?: Accessor onSubmit: (input: RunPrompt) => boolean | Promise onCycle: () => void onInterrupt: () => boolean @@ -175,14 +176,18 @@ function parseSlashCommand(text: string, commands: RunCommand[] | undefined) { return { type: "pending" as const } } - if (!commands.some((item) => item.name === head.name)) { + const item = commands.find((entry) => entry.name === head.name) + if (!item) { return { type: "none" as const } } - return { type: "command" as const, command: { name: head.name, arguments: head.arguments } } + return { + type: "command" as const, + command: { name: head.name, arguments: head.arguments, ...(item.source ? { source: item.source } : {}) }, + } } -function selectedCommand(text: string, command: RunPrompt["command"]) { +export function selectedCommand(text: string, command: RunPrompt["command"], commands?: RunCommand[]) { if (!command) { return } @@ -192,9 +197,14 @@ function selectedCommand(text: string, command: RunPrompt["command"]) { return } + // Bound drafts (e.g. the skill picker) may predate or omit the catalog + // source; resolve it at submit time so routing never degrades to a plain + // command for a skill entry. + const source = command.source ?? commands?.find((item) => item.name === command.name)?.source return { name: command.name, arguments: head.arguments, + ...(source ? { source } : {}), } } @@ -293,7 +303,10 @@ export function createPromptState(input: PromptInput): PromptState { return new StyledText([fg(input.theme().muted)('Ask anything... "Fix a TODO in the codebase"')]) }) - let history = createPromptHistory(input.history) + let history = createPromptHistory(input.history?.()) + createEffect(() => { + history = createPromptHistory(input.history?.()) + }) let draft: RunPrompt = { text: "", parts: [] } let stash: RunPrompt = { text: "", parts: [] } let area: TextareaRenderable | undefined @@ -321,10 +334,10 @@ export function createPromptState(input: PromptInput): PromptState { .map((item) => ({ kind: "mention", display: "@" + item.name, - value: item.name, + value: item.id, part: { type: "agent", - name: item.name, + name: item.id, source: { start: 0, end: 0, @@ -333,21 +346,20 @@ export function createPromptState(input: PromptInput): PromptState { }, })) }) - const resources = createMemo(() => { - return input.resources().map((item) => ({ + const references = createMemo(() => { + return input.references().map((item) => ({ kind: "mention", - display: Locale.truncateMiddle(`@${item.name} (${item.uri})`, width()), + display: Locale.truncateMiddle("@" + item.name, width()), value: item.name, - description: item.description, + description: item.description ?? (item.source.type === "git" ? item.source.repository : item.source.path), part: { type: "file", - mime: item.mimeType ?? "text/plain", + mime: "application/x-directory", filename: item.name, - url: item.uri, + url: pathToFileURL(item.path).href, source: { - type: "resource", - clientName: item.client, - uri: item.uri, + type: "file", + path: item.name, text: { start: 0, end: 0, @@ -402,7 +414,7 @@ export function createPromptState(input: PromptInput): PromptState { }, { initialValue: [] as Auto[] }, ) - const mentionOptions = createMemo(() => [...agents(), ...files(), ...resources()]) + const mentionOptions = createMemo(() => [...agents(), ...files(), ...references()]) const skillCommands = createMemo(() => (input.commands() ?? []).filter((item) => item.source === "skill")) const hasSkillsCommand = createMemo(() => (input.commands() ?? []).some((item) => item.source !== "skill" && item.name === "skills"), @@ -462,7 +474,7 @@ export function createPromptState(input: PromptInput): PromptState { return [ ...fuzzysort.go(next, agents(), { keys: ["value", "display", "description"] }).map((item) => item.obj), ...files(), - ...fuzzysort.go(next, resources(), { keys: ["value", "display", "description"] }).map((item) => item.obj), + ...fuzzysort.go(next, references(), { keys: ["value", "display", "description"] }).map((item) => item.obj), ] } @@ -591,7 +603,7 @@ export function createPromptState(input: PromptInput): PromptState { }) } - const restore = (value: RunPrompt, cursor = Bun.stringWidth(value.text)) => { + const restore = (value: RunPrompt, cursor = stringWidth(value.text)) => { draft = clonePrompt(value) setShell(value.mode === "shell") if (!area || area.isDestroyed) { @@ -601,7 +613,7 @@ export function createPromptState(input: PromptInput): PromptState { hide() area.setText(value.text) restoreParts(value.parts) - area.cursorOffset = Math.min(cursor, Bun.stringWidth(area.plainText)) + area.cursorOffset = Math.min(cursor, stringWidth(area.plainText)) scheduleRows() area.focus() } @@ -632,7 +644,7 @@ export function createPromptState(input: PromptInput): PromptState { area.setText(text) clearParts() draft = shell() ? { text: area.plainText, parts: [], mode: "shell" } : { text: area.plainText, parts: [] } - area.cursorOffset = Math.min(Bun.stringWidth(text), Bun.stringWidth(area.plainText)) + area.cursorOffset = Math.min(stringWidth(text), stringWidth(area.plainText)) scheduleRows() area.focus() } @@ -766,20 +778,27 @@ export function createPromptState(input: PromptInput): PromptState { if (move(dir, event)) return if (!area || area.isDestroyed) return false - const endOffset = Bun.stringWidth(area.plainText) - if (dir === -1 && area.visualCursor.visualRow === 0) { - area.cursorOffset = 0 + const endOffset = stringWidth(area.plainText) + if (dir === -1) { + if (area.cursorOffset === 0) return false + if (area.visualCursor.visualRow === 0) { + area.cursorOffset = 0 + return + } + area.moveCursorUp() + return } const end = typeof area.height === "number" && Number.isFinite(area.height) && area.height > 0 ? area.height - 1 : Math.max(0, (area.virtualLineCount ?? 1) - 1) - if (dir === 1 && area.visualCursor.visualRow === end) { + if (area.cursorOffset === endOffset) return false + if (area.visualCursor.visualRow === end) { area.cursorOffset = endOffset + return } - - return false + area.moveCursorDown() } const requestExit = () => { @@ -868,16 +887,12 @@ export function createPromptState(input: PromptInput): PromptState { area.cursorOffset = 0 const start = area.logicalCursor area.cursorOffset = - shell() || !head - ? cursor - : local - ? Bun.stringWidth(area.plainText) - : Bun.stringWidth(area.plainText.slice(0, head.end)) + shell() || !head ? cursor : local ? stringWidth(area.plainText) : stringWidth(area.plainText.slice(0, head.end)) const end = area.logicalCursor area.deleteRange(start.row, start.col, end.row, end.col) area.insertText(text) - area.cursorOffset = Bun.stringWidth(text) + area.cursorOffset = stringWidth(text) hide() syncDraft() if (!shell()) { @@ -902,7 +917,7 @@ export function createPromptState(input: PromptInput): PromptState { const text = "@" + next.value const startOffset = at() - const endOffset = startOffset + Bun.stringWidth(text) + const endOffset = startOffset + stringWidth(text) const part = structuredClone(next.part) if (part.type === "agent") { part.source = { @@ -975,92 +990,83 @@ export function createPromptState(input: PromptInput): PromptState { return true } - useBindings(() => ({ - mode: OPENCODE_BASE_MODE, + Keymap.createLayer(() => ({ enabled: baseBindingsEnabled(), commands: [ { - name: "prompt.clear", + id: "prompt.clear", title: "Clear prompt or exit", - category: "Prompt", + group: "Prompt", run() { if (requestExit()) return return false }, }, ], - bindings: input.tuiConfig.keybinds.get("prompt.clear"), })) - useBindings(() => ({ - mode: OPENCODE_BASE_MODE, + Keymap.createLayer(() => ({ enabled: input.prompt(), commands: [ { - name: "session.interrupt", + id: "session.interrupt", title: "Interrupt session", - category: "Session", + group: "Session", run() { if (input.onInterrupt()) return return false }, }, ], - bindings: input.tuiConfig.keybinds.get("session.interrupt"), })) - useBindings(() => ({ - mode: OPENCODE_BASE_MODE, + Keymap.createLayer(() => ({ enabled: input.prompt() && !visible(), commands: [ { - name: "prompt.editor", + id: "prompt.editor", title: "Open editor", - category: "Prompt", + group: "Prompt", run() { void openEditor() }, }, ], - bindings: input.tuiConfig.keybinds.get("prompt.editor"), })) - useBindings(() => ({ - mode: OPENCODE_BASE_MODE, + Keymap.createLayer(() => ({ + priority: 1, enabled: input.prompt() && !visible(), commands: [ { - name: "prompt.history.previous", + id: "prompt.history.previous", title: "Previous prompt history", - category: "Prompt", - run(ctx: { event: KeyEvent }) { - return historyCommand(-1, ctx.event) + group: "Prompt", + run(_input: string | undefined, event?: KeyEvent) { + if (!event) return false + return historyCommand(-1, event) }, }, { - name: "prompt.history.next", + id: "prompt.history.next", title: "Next prompt history", - category: "Prompt", - run(ctx: { event: KeyEvent }) { - return historyCommand(1, ctx.event) + group: "Prompt", + run(_input: string | undefined, event?: KeyEvent) { + if (!event) return false + return historyCommand(1, event) }, }, ], - bindings: [ - ...input.tuiConfig.keybinds.get("prompt.history.previous"), - ...input.tuiConfig.keybinds.get("prompt.history.next"), - ], })) - useBindings(() => ({ - mode: OPENCODE_BASE_MODE, + Keymap.createLayer(() => ({ enabled: input.prompt() && !visible(), - bindings: [ + commands: [ { - key: "!", - desc: "Shell mode", + bind: "!", + title: "Shell mode", group: "Prompt", - cmd() { + run() { if (shell()) return false if (!area || area.isDestroyed) return false if (area.cursorOffset !== 0) return false @@ -1070,21 +1076,20 @@ export function createPromptState(input: PromptInput): PromptState { ], })) - useBindings(() => ({ - mode: OPENCODE_BASE_MODE, + Keymap.createLayer(() => ({ enabled: input.prompt() && shell() && !visible(), - bindings: [ + commands: [ { - key: "escape", - desc: "Exit shell mode", + bind: "escape", + title: "Exit shell mode", group: "Prompt", - cmd: () => setShellMode(false), + run: () => setShellMode(false), }, { - key: "backspace", - desc: "Exit shell mode", + bind: "backspace", + title: "Exit shell mode", group: "Prompt", - cmd() { + run() { if (!area || area.isDestroyed) return false if (area.cursorOffset !== 0) return false setShellMode(false) @@ -1093,32 +1098,31 @@ export function createPromptState(input: PromptInput): PromptState { ], })) - useBindings(() => ({ - mode: OPENCODE_BASE_MODE, + Keymap.createLayer(() => ({ enabled: input.prompt() && visible(), commands: [ { - name: "prompt.autocomplete.prev", + id: "prompt.autocomplete.prev", title: "Previous autocomplete item", - category: "Autocomplete", + group: "Autocomplete", run: () => menu.move(-1), }, { - name: "prompt.autocomplete.next", + id: "prompt.autocomplete.next", title: "Next autocomplete item", - category: "Autocomplete", + group: "Autocomplete", run: () => menu.move(1), }, { - name: "prompt.autocomplete.hide", + id: "prompt.autocomplete.hide", title: "Hide autocomplete", - category: "Autocomplete", + group: "Autocomplete", run: cancelAutocomplete, }, { - name: "prompt.autocomplete.select", + id: "prompt.autocomplete.select", title: "Select autocomplete item", - category: "Autocomplete", + group: "Autocomplete", run() { if (mode() === "slash" && options().length === 0) { hide() @@ -1128,9 +1132,9 @@ export function createPromptState(input: PromptInput): PromptState { }, }, { - name: "prompt.autocomplete.complete", + id: "prompt.autocomplete.complete", title: "Complete autocomplete item", - category: "Autocomplete", + group: "Autocomplete", run() { if (mode() === "slash" && options().length === 0) { hide() @@ -1145,13 +1149,6 @@ export function createPromptState(input: PromptInput): PromptState { }, }, ], - bindings: input.tuiConfig.keybinds.gather("run.prompt.autocomplete", [ - "prompt.autocomplete.prev", - "prompt.autocomplete.next", - "prompt.autocomplete.hide", - "prompt.autocomplete.select", - "prompt.autocomplete.complete", - ]), })) const onKeyDown = (event: KeyEvent) => { @@ -1179,7 +1176,7 @@ export function createPromptState(input: PromptInput): PromptState { return } - const command = next.mode === "shell" ? undefined : selectedCommand(next.text, next.command) + const command = next.mode === "shell" ? undefined : selectedCommand(next.text, next.command, input.commands()) if (!command && next.mode !== "shell" && isExitCommand(next.text)) { input.onExit() return diff --git a/packages/opencode/src/cli/cmd/run/footer.question.tsx b/packages/cli/src/mini/footer.question.tsx similarity index 99% rename from packages/opencode/src/cli/cmd/run/footer.question.tsx rename to packages/cli/src/mini/footer.question.tsx index 6b0b40bbd0..f6014c1168 100644 --- a/packages/opencode/src/cli/cmd/run/footer.question.tsx +++ b/packages/cli/src/mini/footer.question.tsx @@ -16,7 +16,7 @@ import type { TextareaRenderable } from "@opentui/core" import { useKeyboard, useTerminalDimensions } from "@opentui/solid" import { For, Show, createEffect, createMemo, createSignal } from "solid-js" -import type { QuestionRequest } from "@opencode-ai/sdk/v2" +import type { QuestionV2Request } from "@opencode-ai/client/promise" import { createQuestionBodyState, questionConfirm, @@ -45,7 +45,7 @@ import type { RunFooterTheme } from "./theme" import type { QuestionReject, QuestionReply } from "./types" export function RunQuestionBody(props: { - request: QuestionRequest + request: QuestionV2Request theme: RunFooterTheme onReply: (input: QuestionReply) => void | Promise onReject: (input: QuestionReject) => void | Promise diff --git a/packages/opencode/src/cli/cmd/run/footer.subagent.tsx b/packages/cli/src/mini/footer.subagent.tsx similarity index 89% rename from packages/opencode/src/cli/cmd/run/footer.subagent.tsx rename to packages/cli/src/mini/footer.subagent.tsx index a755b18cd3..f5c73d56e8 100644 --- a/packages/opencode/src/cli/cmd/run/footer.subagent.tsx +++ b/packages/cli/src/mini/footer.subagent.tsx @@ -55,6 +55,9 @@ export function RunFooterSubagentBody(props: { diffStyle?: RunDiffStyle onCycle: (dir: -1 | 1) => void onClose: () => void + // Formatted interrupt shortcut from the registered keymap binding; the + // command itself is dispatched through the keymap in footer.view. + interrupt?: () => string | undefined }) { const theme = createMemo(() => props.theme()) const footer = createMemo(() => theme().footer) @@ -91,6 +94,11 @@ export function RunFooterSubagentBody(props: { )) let scroll: ScrollBoxRenderable | undefined + const interruptHint = createMemo(() => { + if (tab()?.status !== "running") return undefined + return props.interrupt?.() + }) + useKeyboard((event) => { if (!props.active()) { return @@ -141,6 +149,13 @@ export function RunFooterSubagentBody(props: { {" " + subtitle()} + + {(hint) => ( + + {hint()} interrupt + + )} + 1 && props.index() > 0}> {props.index()} of {props.total()} diff --git a/packages/opencode/src/cli/cmd/run/footer.ts b/packages/cli/src/mini/footer.ts similarity index 96% rename from packages/opencode/src/cli/cmd/run/footer.ts rename to packages/cli/src/mini/footer.ts index 0d9da6f297..f55aa1cea2 100644 --- a/packages/opencode/src/cli/cmd/run/footer.ts +++ b/packages/cli/src/mini/footer.ts @@ -24,12 +24,11 @@ // Ctrl-c clears a live prompt draft first; otherwise interrupt and exit use a // two-press pattern where the first press shows a hint and the second press // within 5 seconds actually fires the action. -import { CliRenderEvents, type CliRenderer, type KeyEvent, type Renderable, type TreeSitterClient } from "@opentui/core" -import type { Keymap } from "@opentui/keymap" +import { CliRenderEvents, type CliRenderer, type TreeSitterClient } from "@opentui/core" import { render } from "@opentui/solid" import { createComponent, createSignal, type Accessor, type Setter } from "solid-js" import { createStore, reconcile } from "solid-js/store" -import { OpencodeKeymapProvider } from "@opencode-ai/tui/keymap" +import { Keymap } from "@opencode-ai/tui/context/keymap" import { RUN_COMMAND_PANEL_ROWS, RUN_SUBAGENT_PANEL_ROWS } from "./footer.command" import { SUBAGENT_INSPECTOR_ROWS } from "./footer.subagent" import { PROMPT_MAX_ROWS, TEXTAREA_MIN_ROWS } from "./footer.prompt" @@ -55,7 +54,7 @@ import type { RunInput, RunPrompt, RunProvider, - RunResource, + RunReference, RunTuiConfig, StreamCommit, } from "./types" @@ -71,7 +70,7 @@ type RunFooterOptions = { directory: string findFiles: (query: string) => Promise agents: RunAgent[] - resources: RunResource[] + references: RunReference[] commands?: RunCommand[] wrote?: boolean sessionID: () => string | undefined @@ -82,9 +81,7 @@ type RunFooterOptions = { first: boolean history?: RunPrompt[] theme: RunTheme - keymap: Keymap tuiConfig: RunTuiConfig - backgroundSubagents: boolean diffStyle: RunDiffStyle onPermissionReply: (input: PermissionReply) => void | Promise onQuestionReply: (input: QuestionReply) => void | Promise @@ -97,6 +94,7 @@ type RunFooterOptions = { onEditorOpen: (input: { value: string }) => Promise onExit?: () => void onSubagentSelect?: (sessionID: string | undefined) => void + onSubagentInterrupt?: (sessionID: string) => void treeSitterClient?: TreeSitterClient } @@ -180,8 +178,8 @@ export class RunFooter implements FooterApi { private rows = TEXTAREA_MIN_ROWS private agents: Accessor private setAgents: Setter - private resources: Accessor - private setResources: Setter + private references: Accessor + private setReferences: Setter private commands: Accessor private setCommands: Setter private providers: Accessor @@ -202,6 +200,8 @@ export class RunFooter implements FooterApi { private setSubagent: (next: FooterSubagentState) => void private queuedPrompts: Accessor private setQueuedPrompts: Setter + private history: Accessor + private setHistory: Setter private promptRoute: FooterPromptRoute = { type: "composer" } private subagentMenuRows = SUBAGENT_ROWS private autocomplete = false @@ -255,9 +255,9 @@ export class RunFooter implements FooterApi { const [agents, setAgents] = createSignal(options.agents) this.agents = agents this.setAgents = setAgents - const [resources, setResources] = createSignal(options.resources) - this.resources = resources - this.setResources = setResources + const [references, setReferences] = createSignal(options.references) + this.references = references + this.setReferences = setReferences const [commands, setCommands] = createSignal(options.commands) this.commands = commands this.setCommands = setCommands @@ -288,6 +288,9 @@ export class RunFooter implements FooterApi { const [queuedPrompts, setQueuedPrompts] = createSignal([]) this.queuedPrompts = queuedPrompts this.setQueuedPrompts = setQueuedPrompts + const [history, setHistory] = createSignal(options.history ?? []) + this.history = history + this.setHistory = setHistory this.base = Math.max(1, renderer.footerHeight - TEXTAREA_MIN_ROWS) this.scrollback = this.createScrollback(options.wrote ?? false) @@ -300,8 +303,8 @@ export class RunFooter implements FooterApi { const footer = this void render( () => - createComponent(OpencodeKeymapProvider, { - keymap: options.keymap, + createComponent(Keymap.Provider, { + config: options.tuiConfig, get children() { return createComponent(RunFooterView, { directory: options.directory, @@ -311,7 +314,7 @@ export class RunFooter implements FooterApi { queuedPrompts: footer.queuedPrompts, findFiles: options.findFiles, agents: footer.agents, - resources: footer.resources, + references: footer.references, commands: footer.commands, providers: footer.providers, currentModel: footer.currentModel, @@ -320,8 +323,7 @@ export class RunFooter implements FooterApi { theme: footer.theme, diffStyle: options.diffStyle, tuiConfig: options.tuiConfig, - backgroundSubagents: options.backgroundSubagents, - history: options.history, + history: footer.history, agent: options.agentLabel, onSubmit: footer.handlePrompt, onPermissionReply: footer.handlePermissionReply, @@ -341,6 +343,7 @@ export class RunFooter implements FooterApi { onLayout: footer.syncLayout, onStatus: footer.setStatus, onSubagentSelect: options.onSubagentSelect, + onSubagentInterrupt: options.onSubagentInterrupt, onQueuedRemove: footer.handleQueuedRemove, }) }, @@ -388,6 +391,15 @@ export class RunFooter implements FooterApi { } public event(next: FooterEvent): void { + if (next.type === "history") { + this.setHistory(next.history) + return + } + + if (next.type === "model") { + this.setCurrentModel(next.selection) + } + if (next.type === "turn.duration") { const current = this.currentModel() this.flush() @@ -411,7 +423,7 @@ export class RunFooter implements FooterApi { } this.setAgents(next.agents) - this.setResources(next.resources) + this.setReferences(next.references) if (next.commands !== undefined) { this.setCommands(next.commands) } @@ -613,7 +625,6 @@ export class RunFooter implements FooterApi { this.themes.splice(index, 1) theme.block.syntax?.destroy() - theme.block.subtleSyntax?.destroy() } public close(): void { @@ -1009,7 +1020,6 @@ export class RunFooter implements FooterApi { void resolveRunTheme(this.renderer).then((theme) => { if (this.isGone) { theme.block.syntax?.destroy() - theme.block.subtleSyntax?.destroy() return } diff --git a/packages/opencode/src/cli/cmd/run/footer.view.tsx b/packages/cli/src/mini/footer.view.tsx similarity index 89% rename from packages/opencode/src/cli/cmd/run/footer.view.tsx rename to packages/cli/src/mini/footer.view.tsx index 4b4c00f9a8..c60e0a40d1 100644 --- a/packages/opencode/src/cli/cmd/run/footer.view.tsx +++ b/packages/cli/src/mini/footer.view.tsx @@ -27,14 +27,8 @@ import { RunPromptBody, createPromptState } from "./footer.prompt" import { RunPermissionBody } from "./footer.permission" import { RunQuestionBody } from "./footer.question" import { footerWidthPolicy } from "./footer.width" -import { - OPENCODE_BASE_MODE, - formatKeyBindings, - formatKeySequence, - useBindings, - useKeymapSelector, - type OpenTuiKeymap, -} from "@opencode-ai/tui/keymap" +import { Keymap } from "@opencode-ai/tui/context/keymap" + import type { FooterPromptRoute, FooterQueuedPrompt, @@ -50,7 +44,7 @@ import type { RunInput, RunPrompt, RunProvider, - RunResource, + RunReference, RunTuiConfig, } from "./types" import type { RunTheme } from "./theme" @@ -76,7 +70,7 @@ type RunFooterViewProps = { directory: string findFiles: (query: string) => Promise agents: () => RunAgent[] - resources: () => RunResource[] + references: () => RunReference[] commands: () => RunCommand[] | undefined providers: () => RunProvider[] | undefined currentModel: () => RunInput["model"] @@ -89,8 +83,7 @@ type RunFooterViewProps = { theme: () => RunTheme diffStyle?: RunDiffStyle tuiConfig: RunTuiConfig - backgroundSubagents: boolean - history?: RunPrompt[] + history?: () => RunPrompt[] agent: string onSubmit: (input: RunPrompt) => boolean onPermissionReply: (input: PermissionReply) => void | Promise @@ -110,6 +103,7 @@ type RunFooterViewProps = { onLayout: (input: { route: FooterPromptRoute; autocomplete: boolean; subagentRows: number }) => void onStatus: (text: string) => void onSubagentSelect?: (sessionID: string | undefined) => void + onSubagentInterrupt?: (sessionID: string) => void onQueuedRemove: (messageID: string) => Promise } @@ -168,9 +162,7 @@ export function RunFooterView(props: RunFooterViewProps) { return tabs().findIndex((item) => item.sessionID === sessionID) + 1 }) - const foregroundSubagents = createMemo( - () => props.backgroundSubagents && activeTabs().some((item) => !item.background), - ) + const foregroundSubagents = createMemo(() => activeTabs().some((item) => !item.background)) const model = createMemo(() => { const current = props.currentModel() return current ? modelInfo(props.providers(), current) : { model: props.state().model, provider: undefined } @@ -179,66 +171,15 @@ export function RunFooterView(props: RunFooterViewProps) { const current = route() return current.type === "subagent" ? subagent().details[current.sessionID] : undefined }) - const command = useKeymapSelector( - (keymap: OpenTuiKeymap) => - formatKeySequence( - keymap - .getCommandBindings({ visibility: "registered", commands: ["command.palette.show"] }) - .get("command.palette.show")?.[0]?.sequence, - props.tuiConfig, - ) ?? "", - ) - const subagentShortcut = useKeymapSelector( - (keymap: OpenTuiKeymap) => - formatKeySequence( - keymap - .getCommandBindings({ visibility: "registered", commands: ["session.child.first"] }) - .get("session.child.first")?.[0]?.sequence, - props.tuiConfig, - ) ?? "", - ) - const queuedShortcut = useKeymapSelector( - (keymap: OpenTuiKeymap) => - formatKeySequence( - keymap - .getCommandBindings({ visibility: "registered", commands: ["session.queued_prompts"] }) - .get("session.queued_prompts")?.[0]?.sequence, - props.tuiConfig, - ) ?? "", - ) - const backgroundShortcut = useKeymapSelector( - (keymap: OpenTuiKeymap) => - formatKeySequence( - keymap - .getCommandBindings({ visibility: "registered", commands: ["session.background"] }) - .get("session.background")?.[0]?.sequence, - props.tuiConfig, - ) ?? "", - ) - const interrupt = useKeymapSelector( - (keymap: OpenTuiKeymap) => - formatKeySequence( - keymap - .getCommandBindings({ visibility: "registered", commands: ["session.interrupt"] }) - .get("session.interrupt")?.[0]?.sequence, - props.tuiConfig, - ) ?? "", - ) - const variantCycle = useKeymapSelector( - (keymap: OpenTuiKeymap) => - formatKeyBindings( - keymap.getCommandBindings({ visibility: "registered", commands: ["variant.cycle"] }).get("variant.cycle"), - props.tuiConfig, - ) ?? "", - ) - const clearShortcut = useKeymapSelector( - (keymap: OpenTuiKeymap) => - formatKeySequence( - keymap.getCommandBindings({ visibility: "registered", commands: ["prompt.clear"] }).get("prompt.clear")?.[0] - ?.sequence, - props.tuiConfig, - ) ?? "", - ) + const shortcuts = Keymap.useShortcuts() + const command = () => shortcuts.get("command.palette.show") ?? "" + const subagentShortcut = () => shortcuts.get("session.child.first") ?? "" + const queuedShortcut = () => shortcuts.get("session.queued_prompts") ?? "" + const backgroundShortcut = () => shortcuts.get("session.background") ?? "" + const subagentInterruptShortcut = () => shortcuts.get("subagent.interrupt") ?? "" + const interrupt = () => shortcuts.get("session.interrupt") ?? "" + const variantCycle = () => shortcuts.all("variant.cycle") ?? "" + const clearShortcut = () => shortcuts.get("prompt.clear") ?? "" const busy = createMemo(() => props.state().phase === "running") const armed = createMemo(() => props.state().interrupt > 0) const exiting = createMemo(() => props.state().exit > 0) @@ -360,7 +301,7 @@ export function RunFooterView(props: RunFooterViewProps) { directory: props.directory, findFiles: props.findFiles, agents: props.agents, - resources: props.resources, + references: props.references, commands: props.commands, tuiConfig: props.tuiConfig, state: props.state, @@ -497,70 +438,84 @@ export function RunFooterView(props: RunFooterViewProps) { props.onRequestExit?.(undefined) }) - useBindings(() => ({ - mode: OPENCODE_BASE_MODE, + Keymap.createLayer(() => ({ enabled: active().type === "prompt" && route().type === "composer" && !composer.visible(), commands: [ { - name: "command.palette.show", + id: "command.palette.show", title: "Open command palette", - category: "Prompt", + group: "Prompt", run: openCommand, }, { - name: "variant.cycle", + id: "variant.cycle", title: "Cycle model variant", - category: "Model", + group: "Model", run: props.onCycle, }, ], - bindings: [ - ...props.tuiConfig.keybinds.get("command.palette.show"), - ...props.tuiConfig.keybinds.get("variant.cycle"), - ], })) - useBindings(() => ({ - mode: OPENCODE_BASE_MODE, - enabled: active().type === "prompt" && route().type === "composer" && foregroundSubagents(), + Keymap.createLayer(() => ({ + enabled: active().type === "prompt" && route().type === "composer" && foregroundSubagents() && !!props.onBackground, priority: 1, commands: [ { - name: "session.background", + id: "session.background", title: "Background subagents", - category: "Session", + group: "Session", run: () => props.onBackground?.(), }, ], - bindings: props.tuiConfig.keybinds.get("session.background"), })) - useBindings(() => ({ - mode: OPENCODE_BASE_MODE, + Keymap.createLayer(() => ({ enabled: active().type === "prompt" && route().type === "composer" && tabs().length > 0, commands: [ { - name: "session.child.first", + id: "session.child.first", title: "View subagents", - category: "Session", + group: "Session", run: openSubagentMenu, }, ], - bindings: props.tuiConfig.keybinds.get("session.child.first"), })) - useBindings(() => ({ - mode: OPENCODE_BASE_MODE, + Keymap.createLayer(() => ({ enabled: active().type === "prompt" && route().type === "composer" && queuedPrompts().length > 0, commands: [ { - name: "session.queued_prompts", + id: "session.queued_prompts", title: "Manage queued prompts", - category: "Session", + group: "Session", run: openQueuedMenu, }, ], - bindings: props.tuiConfig.keybinds.get("session.queued_prompts"), + })) + + Keymap.createLayer(() => ({ + enabled: + active().type === "prompt" && + route().type === "subagent" && + selectedTab()?.status === "running" && + !!props.onSubagentInterrupt, + priority: 1, + commands: [ + { + id: "subagent.interrupt", + title: "Interrupt subagent", + group: "Session", + bind: "ctrl+d", + run: () => { + const current = selectedTab() + if (current?.status !== "running") { + return + } + + props.onSubagentInterrupt?.(current.sessionID) + }, + }, + ], })) createEffect(() => { @@ -747,6 +702,7 @@ export function RunFooterView(props: RunFooterViewProps) { command: { name, arguments: "", + source: "skill", }, }) closePanel() @@ -937,6 +893,7 @@ export function RunFooterView(props: RunFooterViewProps) { diffStyle={props.diffStyle} onCycle={cycleTab} onClose={closeTab} + interrupt={() => subagentInterruptShortcut() || undefined} /> diff --git a/packages/opencode/src/cli/cmd/run/footer.width.ts b/packages/cli/src/mini/footer.width.ts similarity index 100% rename from packages/opencode/src/cli/cmd/run/footer.width.ts rename to packages/cli/src/mini/footer.width.ts diff --git a/packages/cli/src/mini/index.ts b/packages/cli/src/mini/index.ts new file mode 100644 index 0000000000..4a6ef986fa --- /dev/null +++ b/packages/cli/src/mini/index.ts @@ -0,0 +1,8 @@ +export { runMini, validateMiniTerminal, mergeInput as mergeInteractiveInput, type MiniCommandInput } from "./mini" +export { + runNonInteractive, + mergeInput as mergeNonInteractiveInput, + pickRunModel, + parseRunModel, + type RunCommandInput, +} from "./run" diff --git a/packages/cli/src/mini/mini.ts b/packages/cli/src/mini/mini.ts new file mode 100644 index 0000000000..3a8d4a48cc --- /dev/null +++ b/packages/cli/src/mini/mini.ts @@ -0,0 +1,175 @@ +import { Service } from "@opencode-ai/client/effect/service" +import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise" +import { ServerConnection } from "../services/server-connection" +import { waitForCatalogReady } from "./catalog.shared" +import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./runtime.stdin" +import type { RunInput, RunTuiConfig } from "./types" +import { readStdin } from "../util/io" +import { setTimeout } from "node:timers/promises" + +export type MiniCommandInput = { + server: ServerConnection.Resolved + continue?: boolean + session?: string + fork?: boolean + model?: string + agent?: string + prompt?: string + replay?: boolean + replayLimit?: number + demo?: boolean + tuiConfig?: RunTuiConfig | Promise +} + +type Session = Awaited> +export async function runMini(input: MiniCommandInput) { + validate(input) + const initialInput = mergeInput(process.stdin.isTTY ? undefined : await readStdin(), input.prompt) + const runtimeTask = import("./runtime") + const directory = localDirectory() + + try { + const sdk = OpenCode.make({ + baseUrl: input.server.endpoint.url, + headers: Service.headers(input.server.endpoint), + }) + const model = parseModel(input.model) + let agentTask: Promise | undefined + const resolveAgent = () => { + agentTask ??= validateAgent(sdk, directory, input.agent) + return agentTask + } + const resolveSession = async () => { + const [agent, selected] = await Promise.all([resolveAgent(), selectSession(sdk, directory, input)]) + const readyModel = + model ?? (selected?.model ? { providerID: selected.model.providerID, modelID: selected.model.id } : undefined) + if (readyModel) await waitForCatalogReady({ sdk, directory, model: readyModel }) + const session = selected ?? (await createSession(sdk, directory, agent, model)) + return { id: session.id, title: session.title, resume: selected !== undefined } + } + const create = ( + _sdk: OpenCodeClient, + next: { agent: string | undefined; model: RunInput["model"]; variant: string | undefined }, + ) => createSession(sdk, directory, next.agent, next.model, next.variant) + const runtime = await runtimeTask + await runtime.runInteractiveDeferredMode({ + sdk, + directory, + resolveAgent, + session: resolveSession, + createSession: create, + agent: input.agent, + model, + variant: undefined, + files: [], + initialInput, + thinking: true, + replay: input.replay ?? true, + replayLimit: input.replayLimit, + demo: input.demo, + tuiConfig: input.tuiConfig, + }) + } catch (error) { + if (error instanceof Error && error.message === INTERACTIVE_INPUT_ERROR) fail(error.message) + throw error + } +} + +export function validateMiniTerminal() { + if (!process.stdout.isTTY) fail("opencode mini requires a TTY stdout") +} + +/** @internal Exported for testing. */ +export function mergeInput(piped: string | undefined, prompt: string | undefined) { + if (!prompt) return piped || undefined + if (!piped) return prompt + return piped + "\n" + prompt +} + +function validate(input: MiniCommandInput) { + validateMiniTerminal() + if (input.replayLimit !== undefined && (!Number.isInteger(input.replayLimit) || input.replayLimit <= 0)) { + fail("--replay-limit must be a positive integer") + } + if (input.fork && !input.continue && !input.session) fail("--fork requires --continue or --session") + resolveInteractiveStdin().cleanup?.() +} + +function localDirectory(): string { + const root = process.env.PWD ?? process.cwd() + try { + process.chdir(root) + return process.cwd() + } catch { + fail(`Failed to change directory to ${root}`) + } +} + +function parseModel(value?: string): RunInput["model"] { + if (!value) return + const [providerID, ...rest] = value.split("/") + const modelID = rest.join("/") + if (!providerID || !modelID) fail("--model must use the format provider/model") + return { providerID, modelID } +} + +async function validateAgent(sdk: OpenCodeClient, directory: string, name?: string) { + if (!name) return + const deadline = Date.now() + 5_000 + let agents: Awaited> | undefined + while (Date.now() < deadline) { + agents = await sdk.agent.list({ location: { directory } }).catch(() => undefined) + const agent = agents?.data.find((item) => item.id === name) + if (agent?.mode === "subagent") { + warning(`agent "${name}" is a subagent, not a primary agent. Falling back to default agent`) + return + } + if (agent) return name + await setTimeout(25) + } + if (!agents) { + warning("failed to list agents. Falling back to default agent") + return + } + warning(`agent "${name}" not found. Falling back to default agent`) +} + +async function selectSession(sdk: OpenCodeClient, directory: string, input: MiniCommandInput, preselected?: Session) { + const selected = + preselected ?? + (input.session + ? await sdk.session.get({ sessionID: input.session }).catch(() => undefined) + : input.continue + ? await sdk.session + .list({ directory, parentID: null, limit: 1, order: "desc" }) + .then((result) => result.data[0]) + : undefined) + if (input.session && !selected) fail("Session not found") + if (!selected) return + if (!input.fork) return selected + return sdk.session.fork({ sessionID: selected.id }) +} + +async function createSession( + sdk: OpenCodeClient, + directory: string, + agent: string | undefined, + model: RunInput["model"], + variant?: string, +): Promise { + if (model) await waitForCatalogReady({ sdk, directory, model }) + return sdk.session.create({ + agent, + model: model ? { providerID: model.providerID, id: model.modelID, variant } : undefined, + location: { directory }, + }) +} + +function warning(message: string) { + process.stderr.write(`\x1b[93m\x1b[1m!\x1b[0m ${message}\n`) +} + +function fail(message: string): never { + process.stderr.write(`\x1b[91m\x1b[1mError: \x1b[0m${message}\n`) + process.exit(1) +} diff --git a/packages/cli/src/mini/noninteractive.ts b/packages/cli/src/mini/noninteractive.ts new file mode 100644 index 0000000000..86e7cead2a --- /dev/null +++ b/packages/cli/src/mini/noninteractive.ts @@ -0,0 +1,489 @@ +import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise" +import { SessionMessage } from "@opencode-ai/schema/session-message" +import { EOL } from "node:os" +import { readFile } from "node:fs/promises" +import { UI } from "./ui" +import type { MiniToolPart } from "./types" + +type Model = { + providerID: string + modelID: string +} + +type File = { + url: string + filename: string + mime: string +} + +type Input = { + client: OpenCodeClient + sessionID: string + message: string + files: File[] + agent?: string + model?: Model + variant?: string + thinking: boolean + format: "default" | "json" + auto: boolean + /** True when the client is attached to a shared server rather than an exclusive in-process one. */ + attached: boolean + renderTool: (part: MiniToolPart) => Promise + renderToolError: (part: MiniToolPart) => Promise +} + +type StartedPart = { + id: string + timestamp: number +} + +type ToolState = StartedPart & { + assistantMessageID: string + tool: string + input: Record + raw?: string + provider?: unknown +} + +type V2Event = EventSubscribeOutput +type FormRequest = Extract["data"]["form"] + +// MCP elicitations are temporarily owned by the "global" sentinel instead of a real +// session. An exclusive local process may treat them as this run's blockers; an +// attached client must not cancel input that may belong to another session. +const GLOBAL_FORM_SESSION_ID = "global" + +export async function runNonInteractivePrompt(input: Input) { + const controller = new AbortController() + const stream = input.client.event.subscribe({ signal: controller.signal })[Symbol.asyncIterator]() + const connected = await stream.next() + if (connected.done) throw new Error("Event stream disconnected before prompt admission") + + const messageID = SessionMessage.ID.create() + const starts = new Map() + const tools = new Map() + let submitted = false + let promoted = false + let emittedError = false + let questionRejected = false + let permissionRejected = false + let formCancelled = false + let interrupted = false + let admission: AbortController | undefined + + const emit = (type: string, timestamp: number, data: Record) => { + if (input.format !== "json") return false + process.stdout.write(JSON.stringify({ type, timestamp, sessionID: input.sessionID, ...data }) + EOL) + return true + } + + const writeText = (part: { text: string; [key: string]: unknown }, timestamp: number) => { + if (emit("text", timestamp, { part })) return + const text = part.text.trim() + if (!text) return + if (!process.stdout.isTTY) { + process.stdout.write(text + EOL) + return + } + UI.empty() + UI.println(text) + UI.empty() + } + + const replyPermission = async (request: { id: string; action: string; resources: ReadonlyArray }) => { + if (!input.auto) { + permissionRejected = true + UI.println( + UI.Style.TEXT_WARNING_BOLD + "!", + UI.Style.TEXT_NORMAL + + `permission requested: ${request.action} (${request.resources.join(", ")}); auto-rejecting`, + ) + } + await input.client.permission + .reply({ + sessionID: input.sessionID, + requestID: request.id, + reply: input.auto ? "once" : "reject", + }) + .catch(() => {}) + if (!input.auto) { + await input.client.session.interrupt({ sessionID: input.sessionID }).catch(() => {}) + } + } + + const rejectQuestion = async (request: { id: string }) => { + questionRejected = true + await input.client.question.reject({ sessionID: input.sessionID, requestID: request.id }).catch(() => {}) + } + + const cancelForm = async (request: Pick) => { + formCancelled = true + await input.client.form.cancel({ sessionID: request.sessionID, formID: request.id }).catch(() => {}) + } + + const consume = async () => { + while (!controller.signal.aborted) { + const next = await stream.next().catch((error) => { + if (!emittedError) throw error + return { done: true as const, value: undefined } + }) + if (next.done) { + if (emittedError) return + throw new Error("Event stream disconnected during prompt execution") + } + const event = next.value + + if (event.type === "permission.v2.asked" && submitted && event.data.sessionID === input.sessionID) { + await replyPermission(event.data) + continue + } + if (event.type === "question.v2.asked" && submitted && event.data.sessionID === input.sessionID) { + await rejectQuestion(event.data) + continue + } + if ( + event.type === "form.created" && + submitted && + (event.data.form.sessionID === input.sessionID || + (!input.attached && event.data.form.sessionID === GLOBAL_FORM_SESSION_ID)) + ) { + await cancelForm(event.data.form) + continue + } + if (!("sessionID" in event.data) || event.data.sessionID !== input.sessionID) continue + const time = toMillis("created" in event ? event.created : undefined) + + if (event.type === "session.input.promoted") { + if (event.data.inputID === messageID) { + promoted = true + continue + } + } + if ( + event.type === "session.execution.interrupted" && + event.data.reason === "user" && + (interrupted || permissionRejected || questionRejected || formCancelled) + ) { + return + } + if (!promoted) continue + + if (event.type === "session.step.started") { + const part = { + id: partID(event.id), + sessionID: input.sessionID, + messageID: event.data.assistantMessageID, + type: "step-start", + snapshot: event.data.snapshot, + } + if (!emit("step_start", time, { part }) && input.format !== "json") { + UI.empty() + UI.println(`> ${event.data.agent} · ${event.data.model.id}`) + UI.empty() + } + continue + } + + if (event.type === "session.text.started") { + starts.set("text", { id: partID(event.id), timestamp: time }) + continue + } + if (event.type === "session.text.ended") { + const started = starts.get("text") + starts.delete("text") + const part = { + id: started?.id ?? partID(event.id), + sessionID: input.sessionID, + messageID: event.data.assistantMessageID, + type: "text", + text: event.data.text, + time: { start: started?.timestamp ?? time, end: time }, + } + writeText(part, time) + continue + } + + if (event.type === "session.reasoning.started") { + starts.set("reasoning", { id: partID(event.id), timestamp: time }) + continue + } + if (event.type === "session.reasoning.ended" && input.thinking) { + const started = starts.get("reasoning") + starts.delete("reasoning") + const part = { + id: started?.id ?? partID(event.id), + sessionID: input.sessionID, + messageID: event.data.assistantMessageID, + type: "reasoning", + text: event.data.text, + metadata: event.data.state, + time: { start: started?.timestamp ?? time, end: time }, + } + if (emit("reasoning", time, { part })) continue + const text = part.text.trim() + if (!text) continue + const line = `Thinking: ${text}` + if (!process.stdout.isTTY) { + process.stdout.write(line + EOL) + continue + } + UI.empty() + UI.println(`${UI.Style.TEXT_DIM}\u001b[3m${line}\u001b[0m${UI.Style.TEXT_NORMAL}`) + UI.empty() + continue + } + + if (event.type === "session.tool.input.started") { + tools.set(event.data.callID, { + id: partID(event.id), + timestamp: time, + assistantMessageID: event.data.assistantMessageID, + tool: event.data.name, + input: {}, + }) + continue + } + if (event.type === "session.tool.input.ended") { + const current = tools.get(event.data.callID) + if (current) current.raw = event.data.text + continue + } + if (event.type === "session.tool.called") { + const current = tools.get(event.data.callID) + tools.set(event.data.callID, { + id: current?.id ?? partID(event.id), + timestamp: current?.timestamp ?? time, + assistantMessageID: event.data.assistantMessageID, + tool: current?.tool ?? "tool", + input: event.data.input, + raw: current?.raw, + provider: { executed: event.data.executed, state: event.data.state }, + }) + continue + } + if (event.type === "session.tool.success") { + const current = tools.get(event.data.callID) ?? fallbackTool(event) + const part: MiniToolPart = { + id: current.id, + sessionID: input.sessionID, + messageID: event.data.assistantMessageID, + type: "tool", + callID: event.data.callID, + tool: current.tool, + state: { + status: "completed", + input: current.input, + output: event.data.content + .filter((item) => item.type === "text") + .map((item) => item.text) + .join("\n"), + title: current.tool, + metadata: { + structured: event.data.structured, + content: event.data.content, + result: event.data.result, + providerCall: current.provider, + providerResult: { executed: event.data.executed, state: event.data.resultState }, + rawInput: current.raw, + }, + time: { start: current.timestamp, end: time }, + }, + } + tools.delete(event.data.callID) + if (!emit("tool_use", time, { part })) await input.renderTool(part) + continue + } + if (event.type === "session.tool.failed") { + const current = tools.get(event.data.callID) ?? fallbackTool(event) + const error = event.data.error.message + const part: MiniToolPart = { + id: current.id, + sessionID: input.sessionID, + messageID: event.data.assistantMessageID, + type: "tool", + callID: event.data.callID, + tool: current.tool, + state: { + status: "error", + input: current.input, + error, + metadata: { + result: event.data.result, + providerCall: current.provider, + providerResult: { executed: event.data.executed, state: event.data.resultState }, + rawInput: current.raw, + }, + time: { start: current.timestamp, end: time }, + }, + } + tools.delete(event.data.callID) + if (!emit("tool_use", time, { part })) { + await input.renderToolError(part) + UI.error(error) + } + continue + } + + if (event.type === "session.step.ended") { + const part = { + id: partID(event.id), + sessionID: input.sessionID, + messageID: event.data.assistantMessageID, + type: "step-finish", + reason: event.data.finish, + snapshot: event.data.snapshot, + cost: event.data.cost, + tokens: event.data.tokens, + } + emit("step_finish", time, { part }) + continue + } + if (event.type === "session.step.failed") { + if (interrupted || permissionRejected || questionRejected || formCancelled) continue + emittedError = true + process.exitCode = 1 + if (!emit("error", time, { error: event.data.error })) UI.error(event.data.error.message) + continue + } + if (event.type === "session.execution.failed") { + if (!emittedError && !questionRejected && !formCancelled) { + emittedError = true + process.exitCode = 1 + if (!emit("error", time, { error: event.data.error })) UI.error(event.data.error.message) + } + return + } + if (event.type === "session.execution.interrupted") { + if (event.data.reason === "user" && interrupted) process.exitCode = 130 + if (event.data.reason !== "user" && !emittedError) { + emittedError = true + process.exitCode = 1 + const error = { type: "aborted" as const, message: `Session interrupted: ${event.data.reason}` } + if (!emit("error", time, { error })) UI.error(error.message) + } + return + } + if (event.type === "session.execution.succeeded") return + } + } + + const interrupt = () => { + if (interrupted) process.exit(130) + interrupted = true + process.exitCode = 130 + admission?.abort() + void input.client.session.interrupt({ sessionID: input.sessionID }).catch(() => {}) + } + process.on("SIGINT", interrupt) + + let completed: Promise | undefined + try { + if (input.agent) { + await input.client.session.switchAgent({ sessionID: input.sessionID, agent: input.agent }) + } + const selected = input.model + ? { providerID: input.model.providerID, id: input.model.modelID, variant: input.variant } + : input.variant + ? await input.client.session + .get({ sessionID: input.sessionID }) + .then((result) => result.model) + .then(async (model) => { + if (model) return { ...model, variant: input.variant } + const result = await input.client.model.default() + const fallback = result.data + return fallback ? { providerID: fallback.providerID, id: fallback.id, variant: input.variant } : undefined + }) + : undefined + if (input.variant && !selected) throw new Error("Cannot select a variant before selecting a model") + if (selected) { + await input.client.session.switchModel({ sessionID: input.sessionID, model: selected }) + } + + const prepared = await Promise.all(input.files.map(prepareFile)) + if (interrupted) return + submitted = true + completed = consume() + admission = new AbortController() + const response = await input.client.session + .prompt( + { + sessionID: input.sessionID, + id: messageID, + text: [input.message, ...prepared.flatMap((file) => (file.text ? [file.text] : []))].join("\n\n"), + files: prepared.flatMap((file) => (file.attachment ? [file.attachment] : [])), + delivery: "steer", + }, + { signal: admission.signal }, + ) + .catch(async (error) => { + if (interrupted) { + await input.client.session.interrupt({ sessionID: input.sessionID }).catch(() => {}) + } + controller.abort() + await completed?.catch(() => {}) + if (interrupted || emittedError) return undefined + throw error + }) + admission = undefined + if (!response) return + if (interrupted) await input.client.session.interrupt({ sessionID: input.sessionID }).catch(() => {}) + + const [permissions, questions, forms] = await Promise.all([ + input.client.permission.list({ sessionID: input.sessionID }).catch(() => undefined), + input.client.question.list({ sessionID: input.sessionID }).catch(() => undefined), + Promise.all( + (input.attached ? [input.sessionID] : [input.sessionID, GLOBAL_FORM_SESSION_ID]).map((sessionID) => + input.client.form.list({ sessionID }).catch(() => undefined), + ), + ), + ]) + await Promise.all([ + ...(permissions ?? []).map(replyPermission), + ...(questions ?? []).map(rejectQuestion), + ...forms.flatMap((response) => response ?? []).map(cancelForm), + ]) + await completed + } finally { + process.off("SIGINT", interrupt) + controller.abort() + await stream.return?.(undefined).catch(() => {}) + } +} + +function partID(eventID: string) { + return `prt_${eventID.replace(/^evt_/, "")}` +} + +function fallbackTool(event: { + id: string + created: number + data: { assistantMessageID: string; callID: string } +}): ToolState { + return { + id: partID(event.id), + timestamp: toMillis(event.created), + assistantMessageID: event.data.assistantMessageID, + tool: "tool", + input: {}, + } +} + +function toMillis(value: unknown) { + if (typeof value === "number") return value + if (typeof value === "string") return new Date(value).getTime() + return Date.now() +} + +async function prepareFile(file: File) { + if (file.mime !== "text/plain") { + const uri = file.url.startsWith("data:") + ? file.url + : `data:${file.mime};base64,${(await readFile(new URL(file.url))).toString("base64")}` + return { attachment: { uri, mime: file.mime, name: file.filename } } + } + const content = file.url.startsWith("data:") + ? Buffer.from(file.url.slice(file.url.indexOf(",") + 1), "base64").toString("utf8") + : await readFile(new URL(file.url), "utf8") + return { text: `\n${content}\n` } +} diff --git a/packages/opencode/src/cli/cmd/run/permission.shared.ts b/packages/cli/src/mini/permission.shared.ts similarity index 83% rename from packages/opencode/src/cli/cmd/run/permission.shared.ts rename to packages/cli/src/mini/permission.shared.ts index 6ebdbd090c..8dc79d1631 100644 --- a/packages/opencode/src/cli/cmd/run/permission.shared.ts +++ b/packages/cli/src/mini/permission.shared.ts @@ -13,7 +13,7 @@ // // permissionInfo() extracts display info (icon, title, lines, diff) from // the request, delegating to tool.ts for tool-specific formatting. -import type { PermissionRequest } from "@opencode-ai/sdk/v2" +import type { PermissionV2Request } from "@opencode-ai/client/promise" import type { PermissionReply } from "./types" import { toolPath, toolPermissionInfo } from "./tool" @@ -55,7 +55,7 @@ function text(v: unknown): string { return typeof v === "string" ? v : "" } -function data(request: PermissionRequest): Dict { +function data(request: PermissionV2Request): Dict { const meta = dict(request.metadata) return { ...meta, @@ -63,8 +63,8 @@ function data(request: PermissionRequest): Dict { } } -function patterns(request: PermissionRequest): string[] { - return request.patterns.filter((item): item is string => typeof item === "string") +function patterns(request: PermissionV2Request): string[] { + return request.resources.filter((item): item is string => typeof item === "string") } export function createPermissionBodyState(requestID: string): PermissionBodyState { @@ -89,15 +89,15 @@ export function permissionOptions(stage: PermissionStage): PermissionOption[] { return [] } -export function permissionInfo(request: PermissionRequest): PermissionInfo { +export function permissionInfo(request: PermissionV2Request): PermissionInfo { const pats = patterns(request) const input = data(request) - const info = toolPermissionInfo(request.permission, input, dict(request.metadata), pats) + const info = toolPermissionInfo(request.action, input, dict(request.metadata), pats) if (info) { return info } - if (request.permission === "external_directory") { + if (request.action === "external_directory") { const meta = dict(request.metadata) const raw = text(meta.parentDir) || text(meta.filepath) || pats[0] || "" const dir = raw.includes("*") ? raw.slice(0, raw.indexOf("*")).replace(/[\\/]+$/, "") : raw @@ -108,7 +108,7 @@ export function permissionInfo(request: PermissionRequest): PermissionInfo { } } - if (request.permission === "doom_loop") { + if (request.action === "doom_loop") { return { icon: "⟳", title: "Continue after repeated failures", @@ -118,19 +118,20 @@ export function permissionInfo(request: PermissionRequest): PermissionInfo { return { icon: "⚙", - title: `Call tool ${request.permission}`, - lines: [`Tool: ${request.permission}`], + title: `Call tool ${request.action}`, + lines: [`Tool: ${request.action}`], } } -export function permissionAlwaysLines(request: PermissionRequest): string[] { - if (request.always.length === 1 && request.always[0] === "*") { - return [`This will allow ${request.permission} until OpenCode is restarted.`] +export function permissionAlwaysLines(request: PermissionV2Request): string[] { + const save = request.save ?? [] + if (save.length === 1 && save[0] === "*") { + return [`This will allow ${request.action} until OpenCode is restarted.`] } return [ "This will allow the following patterns until OpenCode is restarted.", - ...request.always.map((item) => `- ${item}`), + ...save.map((item) => `- ${item}`), ] } @@ -150,8 +151,11 @@ export function permissionReply(requestID: string, reply: PermissionReply["reply } } -export function permissionShift(state: PermissionBodyState, dir: -1 | 1): PermissionBodyState { - const list = permissionOptions(state.stage) +export function permissionShift( + state: PermissionBodyState, + dir: -1 | 1, + list = permissionOptions(state.stage), +): PermissionBodyState { if (list.length === 0) { return state } diff --git a/packages/opencode/src/cli/cmd/run/prompt.editor.ts b/packages/cli/src/mini/prompt.editor.ts similarity index 100% rename from packages/opencode/src/cli/cmd/run/prompt.editor.ts rename to packages/cli/src/mini/prompt.editor.ts diff --git a/packages/opencode/src/cli/cmd/run/prompt.shared.ts b/packages/cli/src/mini/prompt.shared.ts similarity index 93% rename from packages/opencode/src/cli/cmd/run/prompt.shared.ts rename to packages/cli/src/mini/prompt.shared.ts index 63c33aa34f..359cac3dc1 100644 --- a/packages/opencode/src/cli/cmd/run/prompt.shared.ts +++ b/packages/cli/src/mini/prompt.shared.ts @@ -7,7 +7,8 @@ // the current browse position. When the user arrows up at cursor offset 0, // the current draft is saved and history begins. Arrowing past the end // restores the draft. -export { displayCharAt, displaySlice, mentionTriggerIndex } from "../prompt-display" +export { displayCharAt, displaySlice, mentionTriggerIndex } from "@opencode-ai/tui/prompt/display" +import { stringWidth } from "@opencode-ai/tui/util/string-width" import type { RunPrompt } from "./types" const HISTORY_LIMIT = 200 @@ -102,7 +103,7 @@ export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text: return { state, apply: false } } - if (dir === 1 && cursor !== Bun.stringWidth(text)) { + if (dir === 1 && cursor !== stringWidth(text)) { return { state, apply: false } } @@ -136,7 +137,7 @@ export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text: index: null, }, text: state.draft, - cursor: Bun.stringWidth(state.draft), + cursor: stringWidth(state.draft), apply: true, } } @@ -147,7 +148,7 @@ export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text: index: idx, }, text: state.items[idx].text, - cursor: dir === -1 ? 0 : Bun.stringWidth(state.items[idx].text), + cursor: dir === -1 ? 0 : stringWidth(state.items[idx].text), apply: true, } } diff --git a/packages/opencode/src/cli/cmd/run/question.shared.ts b/packages/cli/src/mini/question.shared.ts similarity index 86% rename from packages/opencode/src/cli/cmd/run/question.shared.ts rename to packages/cli/src/mini/question.shared.ts index 2821240d58..3a21b668e6 100644 --- a/packages/opencode/src/cli/cmd/run/question.shared.ts +++ b/packages/cli/src/mini/question.shared.ts @@ -13,7 +13,7 @@ // // Custom answers: if a question has custom=true, an extra "Type your own // answer" option appears. Selecting it enters editing mode with a text field. -import type { QuestionInfo, QuestionRequest } from "@opencode-ai/sdk/v2" +import type { QuestionV2Info, QuestionV2Request } from "@opencode-ai/client/promise" import type { QuestionReject, QuestionReply } from "./types" export type QuestionBodyState = { @@ -51,23 +51,23 @@ export function questionSync(state: QuestionBodyState, requestID: string): Quest return createQuestionBodyState(requestID) } -export function questionSingle(request: QuestionRequest): boolean { +export function questionSingle(request: QuestionV2Request): boolean { return request.questions.length === 1 && request.questions[0]?.multiple !== true } -export function questionTabs(request: QuestionRequest): number { +export function questionTabs(request: QuestionV2Request): number { return questionSingle(request) ? 1 : request.questions.length + 1 } -export function questionConfirm(request: QuestionRequest, state: QuestionBodyState): boolean { +export function questionConfirm(request: QuestionV2Request, state: QuestionBodyState): boolean { return !questionSingle(request) && state.tab === request.questions.length } -export function questionInfo(request: QuestionRequest, state: QuestionBodyState): QuestionInfo | undefined { +export function questionInfo(request: QuestionV2Request, state: QuestionBodyState): QuestionV2Info | undefined { return request.questions[state.tab] } -export function questionCustom(request: QuestionRequest, state: QuestionBodyState): boolean { +export function questionCustom(request: QuestionV2Request, state: QuestionBodyState): boolean { return questionInfo(request, state)?.custom !== false } @@ -84,7 +84,7 @@ export function questionPicked(state: QuestionBodyState): boolean { return state.answers[state.tab]?.includes(value) ?? false } -export function questionOther(request: QuestionRequest, state: QuestionBodyState): boolean { +export function questionOther(request: QuestionV2Request, state: QuestionBodyState): boolean { const info = questionInfo(request, state) if (!info || info.custom === false) { return false @@ -93,7 +93,7 @@ export function questionOther(request: QuestionRequest, state: QuestionBodyState return state.selected === info.options.length } -export function questionTotal(request: QuestionRequest, state: QuestionBodyState): number { +export function questionTotal(request: QuestionV2Request, state: QuestionBodyState): number { const info = questionInfo(request, state) if (!info) { return 0 @@ -156,7 +156,7 @@ export function questionStoreCustom(state: QuestionBodyState, tab: number, text: function questionPick( state: QuestionBodyState, - request: QuestionRequest, + request: QuestionV2Request, answer: string, custom = false, ): QuestionStep { @@ -204,7 +204,7 @@ function questionToggle(state: QuestionBodyState, answer: string): QuestionBodyS return storeAnswers(state, state.tab, list) } -export function questionMove(state: QuestionBodyState, request: QuestionRequest, dir: -1 | 1): QuestionBodyState { +export function questionMove(state: QuestionBodyState, request: QuestionV2Request, dir: -1 | 1): QuestionBodyState { const total = questionTotal(request, state) if (total === 0) { return state @@ -216,7 +216,7 @@ export function questionMove(state: QuestionBodyState, request: QuestionRequest, } } -export function questionSelect(state: QuestionBodyState, request: QuestionRequest): QuestionStep { +export function questionSelect(state: QuestionBodyState, request: QuestionV2Request): QuestionStep { const info = questionInfo(request, state) if (!info) { return { state } @@ -255,7 +255,7 @@ export function questionSelect(state: QuestionBodyState, request: QuestionReques return questionPick(state, request, option.label) } -export function questionSave(state: QuestionBodyState, request: QuestionRequest): QuestionStep { +export function questionSave(state: QuestionBodyState, request: QuestionV2Request): QuestionStep { const info = questionInfo(request, state) if (!info) { return { state } @@ -305,20 +305,20 @@ export function questionSave(state: QuestionBodyState, request: QuestionRequest) return questionPick(state, request, value, true) } -export function questionSubmit(request: QuestionRequest, state: QuestionBodyState): QuestionReply { +export function questionSubmit(request: QuestionV2Request, state: QuestionBodyState): QuestionReply { return { requestID: request.id, answers: questionAnswers(state, request.questions.length), } } -export function questionReject(request: QuestionRequest): QuestionReject { +export function questionReject(request: QuestionV2Request): QuestionReject { return { requestID: request.id, } } -export function questionHint(request: QuestionRequest, state: QuestionBodyState): string { +export function questionHint(request: QuestionV2Request, state: QuestionBodyState): string { if (state.submitting) { return "Waiting for question event..." } diff --git a/packages/cli/src/mini/run.ts b/packages/cli/src/mini/run.ts new file mode 100644 index 0000000000..fd8f8c8f3a --- /dev/null +++ b/packages/cli/src/mini/run.ts @@ -0,0 +1,270 @@ +import { Service, type Endpoint } from "@opencode-ai/client/effect/service" +import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { Model } from "@opencode-ai/schema/model" +import { open } from "node:fs/promises" +import path from "node:path" +import { readStdin } from "../util/io" +import { ServerConnection } from "../services/server-connection" +import { loadRunAgents, waitForCatalogReady } from "./catalog.shared" +import { runNonInteractivePrompt } from "./noninteractive" +import { toolInlineInfo } from "./tool" +import type { MiniToolPart } from "./types" +import { UI } from "./ui" + +export type RunCommandInput = { + server: ServerConnection.Resolved + message: string[] + continue?: boolean + session?: string + fork?: boolean + model?: string + agent?: string + format: "default" | "json" + file: string[] + title?: string + thinking?: boolean + auto?: boolean +} + +type FilePart = { + url: string + filename: string + mime: string +} + +type Prepared = { + directory?: string + message: string + files: FilePart[] +} + +const ATTACH_FILE_MAX_BYTES = 10 * 1024 * 1024 + +export function runNonInteractive(input: RunCommandInput) { + return run(input).catch((error) => reportError(input, error instanceof Error ? error.message : String(error))) +} + +async function run(input: RunCommandInput) { + if (input.fork && !input.continue && !input.session) fail("--fork requires --continue or --session") + const root = process.env.PWD ?? process.cwd() + const directory = localDirectory(root) + const message = mergeInput(formatMessage(input.message), process.stdin.isTTY ? undefined : await readStdin()) + if (!message?.trim()) fail("You must provide a message") + const files = await Promise.all(input.file.map((file) => prepareFile(file, root))) + const prepared = { directory, message, files } + return execute(input, prepared, input.server.endpoint) +} + +async function execute(input: RunCommandInput, prepared: Prepared, endpoint: Endpoint) { + const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }) + const requestedDirectory = prepared.directory ?? (await client.location.get()).directory + if (!requestedDirectory) fail("Failed to resolve server directory") + const session = await selectSession(client, requestedDirectory, input) + const cwd = session?.location.directory ?? requestedDirectory + const workspace = session?.location.workspaceID + const explicit = parseRunModel(input.model) + const explicitModel = explicit?.model + const variant = explicit?.variant + const sessionModel = session?.model ? { providerID: session.model.providerID, modelID: session.model.id } : undefined + const defaultModel = + !explicitModel && !sessionModel + ? await client.model + .default({ location: { directory: cwd, workspace } }) + .then((result) => (result.data ? { providerID: result.data.providerID, modelID: result.data.id } : undefined)) + : undefined + const model = pickRunModel(explicitModel, variant, sessionModel, defaultModel) + if (variant && !model) return reportError(input, "Cannot select a variant before selecting a model", session?.id) + if (model) { + await waitForCatalogReady({ sdk: client, directory: cwd, workspace, model }) + const available = await client.model.list({ location: { directory: cwd, workspace } }) + if (!available.data.some((item) => item.providerID === model.providerID && item.id === model.modelID)) + return reportError(input, `Model unavailable: ${model.providerID}/${model.modelID}`, session?.id) + } + const agent = await validateAgent(client, cwd, input.agent) + const selected = + session ?? + (await client.session.create({ + agent, + model: model ? { providerID: model.providerID, id: model.modelID, variant } : undefined, + location: { directory: cwd }, + })) + if (!session && input.title !== undefined) { + await client.session.rename({ + sessionID: selected.id, + title: input.title || prepared.message.slice(0, 50) + (prepared.message.length > 50 ? "..." : ""), + }) + } + + await runNonInteractivePrompt({ + client, + sessionID: selected.id, + message: prepared.message, + files: prepared.files, + agent, + model, + variant, + thinking: input.thinking ?? false, + format: input.format, + auto: input.auto ?? false, + attached: true, + renderTool, + renderToolError, + }).catch((error) => reportError(input, error instanceof Error ? error.message : String(error), selected.id)) +} + +export function mergeInput(message: string | undefined, piped: string | undefined) { + if (!message) return piped || undefined + if (!piped) return message + return message + "\n" + piped +} + +export function pickRunModel( + explicit: { providerID: string; modelID: string } | undefined, + variant: string | undefined, + session: { providerID: string; modelID: string } | undefined, + fallback: { providerID: string; modelID: string } | undefined, +) { + if (explicit) return explicit + if (!variant) return + return session ?? fallback +} + +function formatMessage(message: string[]) { + const value = message.map((part) => (part.includes(" ") ? `"${part.replace(/"/g, '\\"')}"` : part)).join(" ") + return value || undefined +} + +function localDirectory(root: string) { + try { + process.chdir(root) + return process.cwd() + } catch { + fail(`Failed to change directory to ${root}`) + } +} + +export function parseRunModel(value?: string) { + if (!value) return + const ref = Model.Ref.parse(value) + return { + model: { providerID: ref.providerID, modelID: ref.id }, + variant: ref.variant, + } +} + +async function validateAgent(client: OpenCodeClient, directory: string, name?: string) { + if (!name) return + const agents = await loadRunAgents(client, directory).catch(() => undefined) + if (!agents) { + warning("failed to list agents. Falling back to default agent") + return + } + const agent = agents.find((item) => item.id === name) + if (!agent) { + warning(`agent "${name}" not found. Falling back to default agent`) + return + } + if (agent.mode === "subagent") { + warning(`agent "${name}" is a subagent, not a primary agent. Falling back to default agent`) + return + } + return name +} + +async function selectSession(client: OpenCodeClient, directory: string, input: RunCommandInput) { + const selected = input.session + ? await client.session.get({ sessionID: input.session }).catch(() => undefined) + : input.continue + ? await client.session + .list({ directory, parentID: null, limit: 1, order: "desc" }) + .then((result) => result.data[0]) + : undefined + if (input.session && !selected) fail("Session not found") + if (!selected || !input.fork) return selected + return client.session.fork({ sessionID: selected.id }) +} + +async function prepareFile(input: string, directory: string): Promise { + const file = path.resolve(directory, input) + const handle = await open(file, "r").catch(() => fail(`File not found: ${input}`)) + try { + const stat = await handle.stat() + if (!stat.isFile() || stat.size > ATTACH_FILE_MAX_BYTES) + fail(`Cannot attach a directory, special file, or file larger than 10 MiB: ${input}`) + const content = Buffer.alloc(Number(stat.size)) + let offset = 0 + while (offset < content.length) { + const read = await handle.read(content, offset, content.length - offset, offset) + if (read.bytesRead === 0) break + offset += read.bytesRead + } + const bytes = content.subarray(0, offset) + const detected = FSUtil.mimeType(file) + const text = bytes.toString("utf8") + const mime = + detected.startsWith("image/") || detected === "application/pdf" + ? detected + : !isBinaryContent(bytes) && Buffer.from(text, "utf8").equals(bytes) + ? "text/plain" + : detected + return { + url: `data:${mime};base64,${bytes.toString("base64")}`, + filename: path.basename(file), + mime, + } + } finally { + await handle.close() + } +} + +function isBinaryContent(bytes: Uint8Array) { + if (bytes.length === 0) return false + if (bytes.includes(0)) return true + return bytes.reduce((count, byte) => count + Number(byte < 9 || (byte > 13 && byte < 32)), 0) / bytes.length > 0.3 +} + +async function renderTool(part: MiniToolPart) { + const info = toolInlineInfo(part) + if (info.mode === "block") { + UI.empty() + UI.println(UI.Style.TEXT_NORMAL + info.icon, UI.Style.TEXT_NORMAL + info.title) + if (info.body?.trim()) UI.println(info.body) + UI.empty() + return + } + UI.println( + UI.Style.TEXT_NORMAL + info.icon, + UI.Style.TEXT_NORMAL + info.title, + info.description ? UI.Style.TEXT_DIM + info.description + UI.Style.TEXT_NORMAL : "", + ) +} + +async function renderToolError(part: MiniToolPart) { + const info = toolInlineInfo(part) + UI.println(UI.Style.TEXT_NORMAL + "✗", UI.Style.TEXT_NORMAL + `${info.title} failed`) +} + +function warning(message: string) { + UI.println(UI.Style.TEXT_WARNING_BOLD + "!", UI.Style.TEXT_NORMAL, message) +} + +function reportError(input: RunCommandInput, message: string, sessionID?: string) { + process.exitCode = 1 + if (input.format === "json") { + process.stdout.write( + JSON.stringify({ + type: "error", + timestamp: Date.now(), + sessionID: sessionID ?? "", + error: { type: "unknown", message }, + }) + "\n", + ) + return + } + UI.error(message) +} + +function fail(message: string): never { + throw new Error(message) +} diff --git a/packages/opencode/src/cli/cmd/run/runtime.boot.ts b/packages/cli/src/mini/runtime.boot.ts similarity index 62% rename from packages/opencode/src/cli/cmd/run/runtime.boot.ts rename to packages/cli/src/mini/runtime.boot.ts index b1f6217846..815f1a2080 100644 --- a/packages/opencode/src/cli/cmd/run/runtime.boot.ts +++ b/packages/cli/src/mini/runtime.boot.ts @@ -6,11 +6,12 @@ // history ring. All are async because they read config or hit the SDK, but // none block each other. import { Context, Effect, Layer } from "effect" -import { resolve } from "@opencode-ai/tui/config" -import { TuiConfig } from "@/config/tui" -import { makeRuntime } from "@/effect/run-service" -import { reusePendingTask } from "./runtime.shared" -import { resolveSession, sessionHistory } from "./session.shared" +import { resolve } from "@opencode-ai/tui/config/v1" +import { makeGlobalNode } from "@opencode-ai/core/effect/app-node" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { makeRuntime } from "@opencode-ai/core/effect/runtime" +import { loadRunProviders } from "./catalog.shared" +import { resolveCurrentSession, sessionHistory } from "./session.shared" import type { RunDiffStyle, RunInput, RunPrompt, RunProvider, RunTuiConfig } from "./types" import { pickVariant } from "./variant.shared" @@ -23,10 +24,10 @@ export type ModelInfo = { export type SessionInfo = { first: boolean history: RunPrompt[] + model?: NonNullable variant: string | undefined } -type Config = Awaited> type BootService = { readonly resolveModelInfo: ( sdk: RunInput["sdk"], @@ -38,18 +39,10 @@ type BootService = { sessionID: string, model: RunInput["model"], ) => Effect.Effect - readonly resolveRunTuiConfig: () => Effect.Effect - readonly resolveDiffStyle: () => Effect.Effect } -const configTask: { current?: Promise } = {} - class Service extends Context.Service()("@opencode/RunBoot") {} -function loadConfig() { - return reusePendingTask(configTask, () => TuiConfig.get()) -} - function emptyModelInfo(): ModelInfo { return { providers: [], @@ -73,42 +66,15 @@ function defaultRunTuiConfig(): RunTuiConfig { } } -function runTuiConfig(config: Config | undefined): RunTuiConfig { - if (!config) { - return defaultRunTuiConfig() - } - - return { - keybinds: config.keybinds, - leader_timeout: config.leader_timeout, - diff_style: config.diff_style ?? "auto", - } -} - const layer = Layer.effect( Service, Effect.gen(function* () { - const config = Effect.fn("RunBoot.config")(() => Effect.promise(() => loadConfig().catch(() => undefined))) - const resolveModelInfo = Effect.fn("RunBoot.resolveModelInfo")(function* ( sdk: RunInput["sdk"], directory: string, model: RunInput["model"], ) { - const connected = yield* Effect.promise(() => - sdk.config - .providers({ directory }) - .then((item) => item.data?.providers) - .catch(() => undefined), - ) - const providers = yield* Effect.promise(() => - connected - ? Promise.resolve(connected) - : sdk.provider - .list() - .then((item) => item.data?.all ?? []) - .catch(() => []), - ) + const providers = yield* Effect.promise(() => loadRunProviders(sdk, directory)) const limits = Object.fromEntries( providers.flatMap((provider) => Object.entries(provider.models ?? {}).flatMap(([modelID, info]) => { @@ -143,7 +109,7 @@ const layer = Layer.effect( sessionID: string, model: RunInput["model"], ) { - const session = yield* Effect.promise(() => resolveSession(sdk, sessionID).catch(() => undefined)) + const session = yield* Effect.promise(() => resolveCurrentSession(sdk, sessionID).catch(() => undefined)) if (!session) { return emptySessionInfo() } @@ -151,28 +117,20 @@ const layer = Layer.effect( return { first: session.first, history: sessionHistory(session), - variant: pickVariant(model, session), + model: session.model, + variant: pickVariant(model ?? session.model, session), } }) - const resolveRunTuiConfig = Effect.fn("RunBoot.resolveRunTuiConfig")(function* () { - return runTuiConfig(yield* config()) - }) - - const resolveDiffStyle = Effect.fn("RunBoot.resolveDiffStyle")(function* () { - return runTuiConfig(yield* config()).diff_style ?? "auto" - }) - return Service.of({ resolveModelInfo, resolveSessionInfo, - resolveRunTuiConfig, - resolveDiffStyle, }) }), ) -const runtime = makeRuntime(Service, layer) +const node = makeGlobalNode({ service: Service, layer, deps: [] }) +const runtime = makeRuntime(Service, LayerNode.compile(node)) // Fetches available variants and context limits for every provider/model pair. export async function resolveModelInfo( @@ -183,6 +141,10 @@ export async function resolveModelInfo( return runtime.runPromise((svc) => svc.resolveModelInfo(sdk, directory, model)).catch(() => emptyModelInfo()) } +export function resolveModelInfoStrict(sdk: RunInput["sdk"], directory: string, model: RunInput["model"]) { + return runtime.runPromise((svc) => svc.resolveModelInfo(sdk, directory, model)) +} + // Fetches session messages to determine if this is the first turn and build prompt history. export async function resolveSessionInfo( sdk: RunInput["sdk"], @@ -193,10 +155,12 @@ export async function resolveSessionInfo( } // Reads TUI config once for direct mode keymap setup and display preferences. -export async function resolveRunTuiConfig(): Promise { - return runtime.runPromise((svc) => svc.resolveRunTuiConfig()).catch(() => defaultRunTuiConfig()) +export async function resolveRunTuiConfig( + config?: RunTuiConfig | Promise, +): Promise { + return Promise.resolve(config).then((value) => value ?? defaultRunTuiConfig()).catch(() => defaultRunTuiConfig()) } -export async function resolveDiffStyle(): Promise { - return runtime.runPromise((svc) => svc.resolveDiffStyle()).catch(() => "auto") +export async function resolveDiffStyle(config?: RunTuiConfig | Promise): Promise { + return resolveRunTuiConfig(config).then((value) => value.diff_style ?? "auto") } diff --git a/packages/opencode/src/cli/cmd/run/runtime.lifecycle.ts b/packages/cli/src/mini/runtime.lifecycle.ts similarity index 90% rename from packages/opencode/src/cli/cmd/run/runtime.lifecycle.ts rename to packages/cli/src/mini/runtime.lifecycle.ts index 4644d3d036..937acb307b 100644 --- a/packages/opencode/src/cli/cmd/run/runtime.lifecycle.ts +++ b/packages/cli/src/mini/runtime.lifecycle.ts @@ -10,12 +10,9 @@ // back to the usual two-press exit sequence through RunFooter.requestExit(). import path from "path" import { CliRenderEvents, createCliRenderer, type CliRenderer, type ScrollbackWriter } from "@opentui/core" -import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui" import { Global } from "@opencode-ai/core/global" -import { openEditor } from "@opencode-ai/tui/editor" -import { registerOpencodeKeymap } from "@opencode-ai/tui/keymap" -import { Session as SessionApi } from "@/session/session" -import * as Locale from "@/util/locale" +import { isDefaultTitle } from "@opencode-ai/tui/util/session" +import { Locale } from "@opencode-ai/tui/util/locale" import { resolveInteractiveStdin } from "./runtime.stdin" import { entrySplash, exitSplash, splashMeta } from "./splash" import { resolveRunTheme } from "./theme" @@ -27,7 +24,7 @@ import type { RunAgent, RunInput, RunPrompt, - RunResource, + RunReference, RunTuiConfig, } from "./types" import { formatModelLabel } from "./variant.shared" @@ -55,7 +52,7 @@ export type LifecycleInput = { directory: string findFiles: (query: string) => Promise agents: RunAgent[] - resources: RunResource[] + references: RunReference[] sessionID: string sessionTitle?: string getSessionID?: () => string | undefined @@ -64,8 +61,7 @@ export type LifecycleInput = { agent: string | undefined model: RunInput["model"] variant: string | undefined - tuiConfig: RunTuiConfig - backgroundSubagents: boolean + tuiConfig: RunTuiConfig | Promise onPermissionReply: (input: PermissionReply) => void | Promise onQuestionReply: (input: QuestionReply) => void | Promise onQuestionReject: (input: QuestionReject) => void | Promise @@ -75,6 +71,7 @@ export type LifecycleInput = { onInterrupt?: () => void onBackground?: () => void onSubagentSelect?: (sessionID: string | undefined) => void + onSubagentInterrupt?: (sessionID: string) => void } export type Lifecycle = { @@ -107,7 +104,7 @@ function shutdown(renderer: CliRenderer): void { } function splashInfo(title: string | undefined, history: RunPrompt[]) { - if (title && !SessionApi.isDefaultTitle(title)) { + if (title && !isDefaultTitle(title)) { return { title, showSession: true, @@ -123,17 +120,9 @@ function splashInfo(title: string | undefined, history: RunPrompt[]) { function footerLabels(input: Pick): FooterLabels { const agentLabel = Locale.titlecase(input.agent ?? "build") - - if (!input.model) { - return { - agentLabel, - modelLabel: "Model default", - } - } - return { agentLabel, - modelLabel: formatModelLabel(input.model, input.variant), + modelLabel: input.model ? formatModelLabel(input.model, input.variant) : "", } } @@ -175,8 +164,7 @@ function queueSplash( // the entry splash, RunFooter takes over the footer region. export async function createRuntimeLifecycle(input: LifecycleInput): Promise { const source = resolveInteractiveStdin() - let unregisterKeymap: (() => void) | undefined - + const footerTask = import("./footer") try { const renderer = await createCliRenderer({ stdin: source.stdin, @@ -193,10 +181,8 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise input.sessionID), ...labels, model: input.model, @@ -242,10 +227,8 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise {}) const ignore = () => {} detachSigint() @@ -276,6 +260,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise { @@ -341,7 +326,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise {}) footer.destroy() - unregisterKeymap?.() shutdown(renderer) if (!wroteExit) { process.stdout.write("\n") @@ -399,7 +383,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise { ? prompt : { ...prompt, - messageID: prompt.messageID ?? queued?.messageID ?? MessageID.ascending(), + messageID: prompt.messageID ?? queued?.messageID ?? SessionMessage.ID.create(), } state.active = sent @@ -285,8 +286,8 @@ export async function runPromptQueue(input: QueueInput): Promise { !isNewCommand(prompt.text) ) { const queued: FooterQueuedPrompt = { - messageID: MessageID.ascending(), - partID: PartID.ascending(), + messageID: SessionMessage.ID.create(), + partID: "prt_" + ascending(), prompt, } state.queued = [...state.queued, queued] diff --git a/packages/opencode/src/cli/cmd/run/runtime.shared.ts b/packages/cli/src/mini/runtime.shared.ts similarity index 100% rename from packages/opencode/src/cli/cmd/run/runtime.shared.ts rename to packages/cli/src/mini/runtime.shared.ts diff --git a/packages/opencode/src/cli/cmd/run/runtime.stdin.ts b/packages/cli/src/mini/runtime.stdin.ts similarity index 89% rename from packages/opencode/src/cli/cmd/run/runtime.stdin.ts rename to packages/cli/src/mini/runtime.stdin.ts index d236fb02c2..fcb1a40a8e 100644 --- a/packages/opencode/src/cli/cmd/run/runtime.stdin.ts +++ b/packages/cli/src/mini/runtime.stdin.ts @@ -1,7 +1,7 @@ import fs from "fs" import * as tty from "node:tty" -export const INTERACTIVE_INPUT_ERROR = "--mini requires a controlling terminal for input" +export const INTERACTIVE_INPUT_ERROR = "opencode mini requires a controlling terminal for input" type InteractiveStdin = { stdin: NodeJS.ReadStream diff --git a/packages/opencode/src/cli/cmd/run/runtime.ts b/packages/cli/src/mini/runtime.ts similarity index 64% rename from packages/opencode/src/cli/cmd/run/runtime.ts rename to packages/cli/src/mini/runtime.ts index 90cddffa22..881dd2950a 100644 --- a/packages/opencode/src/cli/cmd/run/runtime.ts +++ b/packages/cli/src/mini/runtime.ts @@ -1,10 +1,10 @@ -// Top-level orchestrator for `opencode --mini`. +// Top-level orchestrator for `opencode mini`. // // Wires the boot sequence, lifecycle (renderer + footer), stream transport, // and prompt queue together into a single session loop. Two entry points: // // runInteractiveMode -- used when an SDK client already exists (attach mode) -// runInteractiveLocalMode -- used for local in-process mode (no server) +// runInteractiveDeferredMode -- paints before resolving its session // // Both delegate to runInteractiveRuntime, which: // 1. resolves TUI config, model info, and session history, @@ -12,15 +12,22 @@ // 3. starts the stream transport (SDK event subscription), lazily for fresh // local sessions, // 4. runs the prompt queue until the footer closes. -import { createOpencodeClient } from "@opencode-ai/sdk/v2" import { Flag } from "@opencode-ai/core/flag/flag" -import { MessageID } from "@/session/schema" -import { createRunDemo } from "./demo" -import { resolveModelInfo, resolveRunTuiConfig, resolveSessionInfo } from "./runtime.boot" +import { SessionMessage } from "@opencode-ai/schema/session-message" +import { loadRunAgents, loadRunCommands, loadRunReferences, waitForDefaultModel } from "./catalog.shared" +import { resolveModelInfo, resolveModelInfoStrict, resolveRunTuiConfig, resolveSessionInfo } from "./runtime.boot" import { createRuntimeLifecycle } from "./runtime.lifecycle" import { trace } from "./trace" import { cycleVariant, formatModelLabel, resolveSavedVariant, resolveVariant, saveVariant } from "./variant.shared" -import type { LocalReplayAnchor, LocalReplayRow, RunInput, RunPrompt, RunProvider, StreamCommit } from "./types" +import type { + LocalReplayAnchor, + LocalReplayRow, + RunInput, + RunPrompt, + RunProvider, + RunTuiConfig, + StreamCommit, +} from "./types" /** @internal Exported for testing */ export { pickVariant, resolveVariant } from "./variant.shared" @@ -44,25 +51,22 @@ type CreateSession = (sdk: RunInput["sdk"], input: CreateSessionInput) => Promis type RunRuntimeInput = { boot: () => Promise afterPaint?: (ctx: BootContext) => Promise | void - resolveSession?: ( - ctx: BootContext, - ) => Promise<{ sessionID: string; sessionTitle?: string; agent?: string | undefined }> + resolveSession?: (ctx: BootContext) => Promise createSession?: (ctx: BootContext, input: CreateSessionInput) => Promise files: RunInput["files"] initialInput?: string thinking: boolean - backgroundSubagents: boolean replay?: boolean replayLimit?: number demo?: RunInput["demo"] + tuiConfig?: RunTuiConfig | Promise } -type RunLocalInput = { +type RunDeferredInput = { + sdk: RunInput["sdk"] directory: string - fetch: typeof globalThis.fetch resolveAgent: () => Promise - session: (sdk: RunInput["sdk"]) => Promise<{ id: string; title?: string } | undefined> - share: (sdk: RunInput["sdk"], sessionID: string) => Promise + session: (sdk: RunInput["sdk"]) => Promise<{ id: string; title?: string; resume?: boolean } | undefined> createSession?: CreateSession agent: RunInput["agent"] model: RunInput["model"] @@ -70,14 +74,14 @@ type RunLocalInput = { files: RunInput["files"] initialInput?: string thinking: boolean - backgroundSubagents: boolean replay?: boolean replayLimit?: number demo?: RunInput["demo"] + tuiConfig?: RunTuiConfig | Promise } type StreamTransportModule = Pick< - Awaited, + Awaited, "createSessionTransport" | "formatUnknownError" > @@ -91,10 +95,13 @@ type StreamState = { handle: Awaited> } +type RunDemo = ReturnType<(typeof import("./demo"))["createRunDemo"]> + type ResolvedSession = { sessionID: string sessionTitle?: string agent?: string | undefined + resume?: boolean } function createSessionResolver(fn?: CreateSession) { @@ -130,7 +137,7 @@ type RuntimeState = { sessionTitle?: string agent: string | undefined switching?: Promise - demo?: ReturnType + demo?: RunDemo selectSubagent?: (sessionID: string | undefined) => void session?: Promise stream?: Promise @@ -165,10 +172,8 @@ async function resolveExitTitle( } return ctx.sdk.session - .get({ - sessionID: state.sessionID, - }) - .then((x) => x.data?.title) + .get({ sessionID: state.sessionID }) + .then((session) => session.title) .catch(() => undefined) } @@ -181,23 +186,23 @@ async function resolveExitTitle( async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDeps = {}): Promise { const start = performance.now() const log = trace() - const tuiConfigTask = resolveRunTuiConfig() + const tuiConfigTask = resolveRunTuiConfig(input.tuiConfig) const ctx = await input.boot() - const modelTask = resolveModelInfo(ctx.sdk, ctx.directory, ctx.model) const sessionTask = ctx.resume === true ? resolveSessionInfo(ctx.sdk, ctx.sessionID, ctx.model) : Promise.resolve({ first: true, history: [], + model: undefined, variant: undefined, }) const savedTask = resolveSavedVariant(ctx.model) - const [tuiConfig, session, savedVariant] = await Promise.all([tuiConfigTask, sessionTask, savedTask]) + const [session, savedVariant] = await Promise.all([sessionTask, savedTask]) const state: RuntimeState = { shown: !session.first, aborting: false, - model: ctx.model, + model: ctx.model ?? session.model, providers: [], variants: [], limits: {}, @@ -208,63 +213,86 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep sessionTitle: ctx.sessionTitle, agent: ctx.agent, } - const ensureSession = () => { - if (!input.resolveSession || state.sessionID) { - return Promise.resolve() + const loadModel = async () => { + if (state.model) { + return { + model: state.model, + savedVariant, + boot: true, + info: await resolveModelInfo(ctx.sdk, ctx.directory, state.model), + } } - if (state.session) { - return state.session - } - - state.session = input.resolveSession(ctx).then((next) => { - state.sessionID = next.sessionID - state.sessionTitle = next.sessionTitle ?? state.sessionTitle - state.agent = next.agent + const model = await waitForDefaultModel({ + sdk: ctx.sdk, + directory: ctx.directory, + active: () => !footer.isClosed, }) - return state.session - } + if (footer.isClosed) return + const [fallbackSavedVariant, info] = await Promise.all([ + resolveSavedVariant(model), + resolveModelInfo(ctx.sdk, ctx.directory, model), + ]) + if (!model || state.model) { + return { + model: state.model, + savedVariant: undefined, + boot: false, + info, + } + } + state.model = model + return { + model, + savedVariant: fallbackSavedVariant, + boot: true, + info, + } + } const shell = await (deps.createRuntimeLifecycle ?? createRuntimeLifecycle)({ directory: ctx.directory, findFiles: (query) => - ctx.sdk.find - .files({ query, directory: ctx.directory }) - .then((x) => x.data ?? []) + ctx.sdk.file + .find({ query, type: "file", location: { directory: ctx.directory } }) + .then((result) => result.data.map((file) => file.path)) .catch(() => []), agents: [], - resources: [], + references: [], sessionID: state.sessionID, sessionTitle: state.sessionTitle, getSessionID: () => state.sessionID, first: session.first, - history: session.history, + history: state.history, agent: state.agent, model: state.model, variant: state.activeVariant, - tuiConfig, - backgroundSubagents: input.backgroundSubagents, + tuiConfig: tuiConfigTask, onPermissionReply: async (next) => { if (state.demo?.permission(next)) { return } log?.write("send.permission.reply", next) - await ctx.sdk.permission.reply(next) + await ctx.sdk.permission.reply({ sessionID: state.sessionID, ...next }) }, onQuestionReply: async (next) => { if (state.demo?.questionReply(next)) { return } - await ctx.sdk.question.reply(next) + await ctx.sdk.question.reply({ + sessionID: state.sessionID, + requestID: next.requestID, + answers: next.answers ?? [], + }) }, onQuestionReject: async (next) => { if (state.demo?.questionReject(next)) { return } - await ctx.sdk.question.reject(next) + await ctx.sdk.question.reject({ sessionID: state.sessionID, ...next }) }, onCycleVariant: () => { if (!state.model || state.variants.length === 0) { @@ -339,22 +367,32 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep }, onInterrupt: () => { if (!hasSession(input, state) || state.aborting) { - return + return false } state.aborting = true - void ctx.sdk.session - .abort({ - sessionID: state.sessionID, - }) + void ( + state.stream + ? state.stream.then((item) => item.handle.interruptActiveTurn()) + : ctx.sdk.session.interrupt({ sessionID: state.sessionID }) + ) .catch(() => {}) .finally(() => { state.aborting = false }) + return true }, onBackground: () => { - if (!hasSession(input, state)) return - void ctx.sdk.experimental.session.background({ sessionID: state.sessionID }).catch(() => {}) + if (!hasSession(input, state)) { + return + } + + log?.write("send.background", { sessionID: state.sessionID }) + void ctx.sdk.session.background({ sessionID: state.sessionID }).catch(() => {}) + }, + onSubagentInterrupt: (sessionID) => { + log?.write("send.subagent.interrupt", { sessionID }) + void ctx.sdk.session.interrupt({ sessionID }).catch(() => {}) }, onSubagentSelect: (sessionID) => { state.selectSubagent?.(sessionID) @@ -364,96 +402,183 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep }, }) const footer = shell.footer + const firstPaint = footer.idle().catch(() => {}) + const ensureSession = () => { + if (!input.resolveSession || state.sessionID) { + return Promise.resolve() + } + + if (state.session) { + return state.session + } + + state.session = input.resolveSession(ctx).then(async (next) => { + state.sessionID = next.sessionID + state.sessionTitle = next.sessionTitle ?? state.sessionTitle + state.agent = next.agent + if (!next.resume) return + const resumed = await resolveSessionInfo(ctx.sdk, next.sessionID, ctx.model) + session.first = resumed.first + session.history = resumed.history + session.model = resumed.model + session.variant = resumed.variant + state.shown = !resumed.first + state.history = [...resumed.history] + state.model = ctx.model ?? resumed.model + const resumedSavedVariant = state.model ? await resolveSavedVariant(state.model) : undefined + state.activeVariant = resolveVariant(ctx.variant, resumed.variant, resumedSavedVariant, []) + session.variant = state.activeVariant + footer.event({ type: "history", history: resumed.history }) + footer.event({ type: "first", first: resumed.first }) + }) + return state.session + } + const modelTask = firstPaint.then(async () => { + if (footer.isClosed) return + await ensureSession() + if (footer.isClosed) return + return loadModel() + }) const rememberLocal = (commit: StreamCommit, after?: LocalReplayAnchor) => { state.localRows = [...state.localRows, { commit, after }].slice(-LOCAL_REPLAY_ROW_LIMIT) } - const loadCatalog = async (): Promise => { + const applyCatalog = (catalog: { + agents: Awaited> + references: Awaited> + commands: Awaited> + }) => { if (footer.isClosed) { return } - - const [agents, resources, commands] = await Promise.all([ - ctx.sdk.app - .agents({ directory: ctx.directory }) - .then((x) => x.data ?? []) - .catch(() => []), - ctx.sdk.experimental.resource - .list({ directory: ctx.directory }) - .then((x) => Object.values(x.data ?? {})) - .catch(() => []), - ctx.sdk.command - .list({ directory: ctx.directory }) - .then((x) => x.data ?? []) - .catch(() => []), - ]) - if (footer.isClosed) { - return - } - footer.event({ type: "catalog", - agents, - resources, - commands, + agents: catalog.agents, + references: catalog.references, + commands: catalog.commands, }) } - void footer - .idle() - .then(loadCatalog) - .catch(() => {}) + const fetchCatalog = async () => { + const [agents, references, commands] = await Promise.all([ + loadRunAgents(ctx.sdk, ctx.directory), + loadRunReferences(ctx.sdk, ctx.directory), + loadRunCommands(ctx.sdk, ctx.directory), + ]) + return { agents, references, commands } + } + + const loadCatalog = async () => { + applyCatalog( + await Promise.all([ + loadRunAgents(ctx.sdk, ctx.directory).catch(() => []), + loadRunReferences(ctx.sdk, ctx.directory).catch(() => []), + loadRunCommands(ctx.sdk, ctx.directory).catch(() => []), + ]).then(([agents, references, commands]) => ({ agents, references, commands })), + ) + } + + const applyModelInfo = ( + info: Awaited>, + current: string | undefined, + boot = false, + saved = savedVariant, + ) => { + state.providers = info.providers + state.variants = variantsFor(state.providers, state.model) + state.limits = info.limits + state.activeVariant = boot + ? resolveVariant(ctx.variant, current, saved, state.variants) + : current && !state.variants.includes(current) + ? undefined + : current + if (footer.isClosed) return + footer.event({ type: "models", providers: info.providers }) + footer.event({ type: "variants", variants: state.variants, current: state.activeVariant }) + if (state.model) + footer.event({ + type: "model", + model: formatModelLabel(state.model, state.activeVariant, state.providers), + selection: state.model, + }) + } + + let catalogRefresh: Promise | undefined + let catalogRefreshQueued = false + const requestCatalogRefresh = () => { + catalogRefreshQueued = true + if (catalogRefresh || footer.isClosed) return + catalogRefresh = (async () => { + await Promise.all([modelTask, initialCatalog]) + while (catalogRefreshQueued && !footer.isClosed) { + catalogRefreshQueued = false + const [catalog, info] = await Promise.allSettled([ + fetchCatalog(), + resolveModelInfoStrict(ctx.sdk, ctx.directory, state.model), + ]) + if (catalog.status === "fulfilled") applyCatalog(catalog.value) + if (info.status === "fulfilled") applyModelInfo(info.value, state.activeVariant) + } + })().finally(() => { + catalogRefresh = undefined + if (catalogRefreshQueued) requestCatalogRefresh() + }) + void catalogRefresh.catch(() => {}) + } + + const initialCatalog = firstPaint.then(() => (footer.isClosed ? undefined : loadCatalog())).catch(() => {}) + void initialCatalog if (Flag.OPENCODE_SHOW_TTFD) { - footer.append({ - kind: "system", - text: `startup ${Math.max(0, Math.round(performance.now() - start))}ms`, - phase: "final", - source: "system", + void firstPaint.then(() => { + if (footer.isClosed) return + footer.append({ + kind: "system", + text: `startup ${Math.max(0, Math.round(performance.now() - start))}ms`, + phase: "final", + source: "system", + }) + }) + } + + const createDemo = async () => { + const { createRunDemo } = await import("./demo") + return createRunDemo({ + footer, + sessionID: state.sessionID, + thinking: input.thinking, }) } if (input.demo) { - await ensureSession() - state.demo = createRunDemo({ - footer, - sessionID: state.sessionID, - thinking: input.thinking, - limits: () => state.limits, - }) + await firstPaint + if (!footer.isClosed) { + await ensureSession() + state.demo = await createDemo() + } } if (input.afterPaint) { - void Promise.resolve(input.afterPaint(ctx)).catch(() => {}) + void firstPaint.then(() => (footer.isClosed ? undefined : input.afterPaint?.(ctx))).catch(() => {}) } - void modelTask.then((info) => { - state.providers = info.providers - state.variants = variantsFor(state.providers, state.model) - state.limits = info.limits - - const next = resolveVariant(ctx.variant, session.variant, savedVariant, state.variants) - if (next !== state.activeVariant) { - state.activeVariant = next - } - - if (footer.isClosed) { - return - } - - footer.event({ type: "models", providers: info.providers }) - footer.event({ type: "variants", variants: state.variants, current: state.activeVariant }) - if (!state.model) { - return - } - - footer.event({ - type: "model", - model: formatModelLabel(state.model, state.activeVariant, state.providers), - }) + void modelTask.then((result) => { + if (!result) return + const current = state.model + const boot = + result.boot && + !!current && + current.providerID === result.model?.providerID && + current.modelID === result.model.modelID + applyModelInfo(result.info, boot ? session.variant : state.activeVariant, boot, result.savedVariant) }) - const streamTask = deps.streamTransport ?? import("./stream.transport") + let streamTask = deps.streamTransport + const loadStreamTransport = () => { + if (streamTask) return streamTask + streamTask = import("./stream-v2.transport") + return streamTask + } const ensureStream = () => { if (state.stream) { return state.stream @@ -467,7 +592,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep throw new Error("runtime closed") } - const mod = await streamTask + const mod = await loadStreamTransport() if (footer.isClosed) { throw new Error("runtime closed") } @@ -483,6 +608,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep providers: () => state.providers, footer, trace: log, + onCatalogRefresh: requestCatalogRefresh, }) if (footer.isClosed) { await handle.close() @@ -535,6 +661,12 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep }) const runQueue = async () => { + await firstPaint + if (footer.isClosed) return + await ensureSession() + if (footer.isClosed) return + await modelTask + if (footer.isClosed) return let includeFiles = true if (state.demo) { await state.demo.start() @@ -580,14 +712,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep state.history = [] state.localRows = [] includeFiles = true - state.demo = input.demo - ? createRunDemo({ - footer, - sessionID: state.sessionID, - thinking: input.thinking, - limits: () => state.limits, - }) - : undefined + state.demo = input.demo ? await createDemo() : undefined log?.write("session.new", { sessionID: state.sessionID, }) @@ -630,7 +755,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep text: error instanceof Error ? error.message : String(error), phase: "start", source: "system", - messageID: MessageID.ascending(), + messageID: SessionMessage.ID.create(), } as const rememberLocal(commit) footer.append(commit) @@ -664,7 +789,9 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep (row) => row.commit.kind !== "user" || row.commit.messageID !== prompt.messageID, ) } - includeFiles = false + // Shell and skill turns never send CLI file attachments; keep them + // pending for the next prompt-shaped turn. + if (prompt.mode !== "shell" && prompt.command?.source !== "skill") includeFiles = false } catch (error) { if (signal.aborted || footer.isClosed) { return @@ -690,6 +817,8 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep try { const eager = eagerStream(input, ctx) if (eager) { + await firstPaint + if (footer.isClosed) return if (input.replay && state.shown) { // Replay commits immutable scrollback rows, so wait for provider names // before bootstrapping existing session history. @@ -700,13 +829,15 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep } if (!eager && input.resolveSession) { - queueMicrotask(() => { - if (footer.isClosed) { - return - } + void firstPaint + .then(() => { + if (footer.isClosed) { + return + } - void ensureStream().catch(() => {}) - }) + return ensureStream() + }) + .catch(() => {}) } try { @@ -730,62 +861,61 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep } } -// Local in-process mode. Creates an SDK client backed by a direct fetch to -// the in-process server, so no external HTTP server is needed. -export async function runInteractiveLocalMode(input: RunLocalInput): Promise { - const sdk = createOpencodeClient({ - baseUrl: "http://opencode.internal", - fetch: input.fetch, - directory: input.directory, - }) +// Deferred mode paints before session resolution. The caller may back the +// generated client with a transport that is still acquiring a daemon. +export async function runInteractiveDeferredMode(input: RunDeferredInput, deps?: RunRuntimeDeps): Promise { + const sdk = input.sdk let session: Promise | undefined - return runInteractiveRuntime({ - files: input.files, - initialInput: input.initialInput, - thinking: input.thinking, - backgroundSubagents: input.backgroundSubagents, - replay: input.replay, - replayLimit: input.replayLimit, - demo: input.demo, - resolveSession: () => { - if (session) { + return runInteractiveRuntime( + { + files: input.files, + initialInput: input.initialInput, + thinking: input.thinking, + replay: input.replay, + replayLimit: input.replayLimit, + demo: input.demo, + tuiConfig: input.tuiConfig, + resolveSession: () => { + if (session) { + return session + } + + session = Promise.all([input.resolveAgent(), input.session(sdk)]).then(([agent, next]) => { + if (!next?.id) { + throw new Error("Session not found") + } + + return { + sessionID: next.id, + sessionTitle: next.title, + agent, + resume: next.resume, + } + }) return session - } - - session = Promise.all([input.resolveAgent(), input.session(sdk)]).then(([agent, next]) => { - if (!next?.id) { - throw new Error("Session not found") - } - - void input.share(sdk, next.id).catch(() => {}) + }, + createSession: createSessionResolver(input.createSession), + boot: async () => { return { - sessionID: next.id, - sessionTitle: next.title, - agent, + sdk, + directory: input.directory, + sessionID: "", + sessionTitle: undefined, + resume: false, + agent: input.agent, + model: input.model, + variant: input.variant, } - }) - return session + }, }, - createSession: createSessionResolver(input.createSession), - boot: async () => { - return { - sdk, - directory: input.directory, - sessionID: "", - sessionTitle: undefined, - resume: false, - agent: input.agent, - model: input.model, - variant: input.variant, - } - }, - }) + deps, + ) } // Attach mode. Uses the caller-provided SDK client directly. export async function runInteractiveMode( - input: RunInput & { createSession?: CreateSession }, + input: RunInput & { createSession?: CreateSession; tuiConfig?: RunTuiConfig | Promise }, deps?: RunRuntimeDeps, ): Promise { return runInteractiveRuntime( @@ -793,10 +923,10 @@ export async function runInteractiveMode( files: input.files, initialInput: input.initialInput, thinking: input.thinking, - backgroundSubagents: input.backgroundSubagents, replay: input.replay, replayLimit: input.replayLimit, demo: input.demo, + tuiConfig: input.tuiConfig, boot: async () => ({ sdk: input.sdk, directory: input.directory, diff --git a/packages/opencode/src/cli/cmd/run/scrollback.shared.ts b/packages/cli/src/mini/scrollback.shared.ts similarity index 91% rename from packages/opencode/src/cli/cmd/run/scrollback.shared.ts rename to packages/cli/src/mini/scrollback.shared.ts index fc040536e9..447d260c09 100644 --- a/packages/opencode/src/cli/cmd/run/scrollback.shared.ts +++ b/packages/cli/src/mini/scrollback.shared.ts @@ -6,11 +6,7 @@ function syntax(style?: SyntaxStyle): SyntaxStyle { return style ?? SyntaxStyle.fromTheme([]) } -export function entrySyntax(commit: StreamCommit, theme: RunTheme): SyntaxStyle { - if (commit.kind === "reasoning") { - return syntax(theme.block.subtleSyntax ?? theme.block.syntax) - } - +export function entrySyntax(theme: RunTheme): SyntaxStyle { return syntax(theme.block.syntax) } diff --git a/packages/opencode/src/cli/cmd/run/scrollback.surface.ts b/packages/cli/src/mini/scrollback.surface.ts similarity index 96% rename from packages/opencode/src/cli/cmd/run/scrollback.surface.ts rename to packages/cli/src/mini/scrollback.surface.ts index f8516a054c..d9182219d2 100644 --- a/packages/opencode/src/cli/cmd/run/scrollback.surface.ts +++ b/packages/cli/src/mini/scrollback.surface.ts @@ -105,7 +105,7 @@ export class RunScrollbackStream { ) { this.diffStyle = options.diffStyle this.sessionID = options.sessionID - this.treeSitterClient = options.treeSitterClient ?? getTreeSitterClient() + this.treeSitterClient = options.treeSitterClient this.wrote = options.wrote ?? false this.onThemeRelease = options.onThemeRelease } @@ -143,7 +143,7 @@ export class RunScrollbackStream { } active.renderable.fg = entryColor(active.commit, theme) - active.renderable.syntaxStyle = entrySyntax(active.commit, theme) + active.renderable.syntaxStyle = entrySyntax(theme) } private createEntry(commit: StreamCommit, body: ActiveBody): ActiveEntry { @@ -151,6 +151,7 @@ export class RunScrollbackStream { startOnNewLine: entryFlags(commit).startOnNewLine, }) const style = entryLook(commit, this.theme.entry) + const treeSitterClient = body.type === "text" ? undefined : (this.treeSitterClient ??= getTreeSitterClient()) const renderable = body.type === "text" ? new TextRenderable(surface.renderContext, { @@ -164,23 +165,23 @@ export class RunScrollbackStream { ? new CodeRenderable(surface.renderContext, { content: "", filetype: body.filetype, - syntaxStyle: entrySyntax(commit, this.theme), + syntaxStyle: entrySyntax(this.theme), width: "100%", wrapMode: "word", drawUnstyledText: false, streaming: true, fg: entryColor(commit, this.theme), - treeSitterClient: this.treeSitterClient, + treeSitterClient, }) : new MarkdownRenderable(surface.renderContext, { content: "", - syntaxStyle: entrySyntax(commit, this.theme), + syntaxStyle: entrySyntax(this.theme), width: "100%", streaming: true, internalBlockMode: "top-level", tableOptions: { widthMode: "content" }, fg: entryColor(commit, this.theme), - treeSitterClient: this.treeSitterClient, + treeSitterClient, }) surface.root.add(renderable) diff --git a/packages/opencode/src/cli/cmd/run/scrollback.writer.tsx b/packages/cli/src/mini/scrollback.writer.tsx similarity index 87% rename from packages/opencode/src/cli/cmd/run/scrollback.writer.tsx rename to packages/cli/src/mini/scrollback.writer.tsx index 43176ded79..e6ab9bb408 100644 --- a/packages/opencode/src/cli/cmd/run/scrollback.writer.tsx +++ b/packages/cli/src/mini/scrollback.writer.tsx @@ -7,26 +7,6 @@ import { toolFiletype, toolStructuredFinal } from "./tool" import { RUN_THEME_FALLBACK, transparent, type RunTheme } from "./theme" import type { EntryLayout, RunEntryBody, ScrollbackOptions, StreamCommit } from "./types" -function todoText(item: { status: string; content: string }): string { - if (item.status === "completed") { - return `[✓] ${item.content}` - } - - if (item.status === "cancelled") { - return `~[ ] ${item.content}~` - } - - if (item.status === "in_progress") { - return `[•] ${item.content}` - } - - return `[ ] ${item.content}` -} - -function todoColor(theme: RunTheme, status: string) { - return status === "in_progress" ? theme.block.warning : theme.block.muted -} - export function entryGroupKey(commit: StreamCommit): string | undefined { if (!commit.partID) { return undefined @@ -104,7 +84,7 @@ export function RunEntryContent(props: { const theme = createMemo(() => props.theme ?? RUN_THEME_FALLBACK) const body = createMemo(() => props.body ?? entryBody(props.commit)) const style = createMemo(() => entryLook(props.commit, theme().entry)) - const syntax = createMemo(() => entrySyntax(props.commit, theme())) + const syntax = createMemo(() => entrySyntax(theme())) const color = createMemo(() => entryColor(props.commit, theme())) const suppressBackgrounds = createMemo(() => props.opts?.suppressBackgrounds === true) const diffBg = (color: ColorInput) => (suppressBackgrounds() ? transparent : color) @@ -137,10 +117,6 @@ export function RunEntryContent(props: { const next = structured() return next?.kind === "task" ? next : undefined }) - const todo_snapshot = createMemo(() => { - const next = structured() - return next?.kind === "todo" ? next : undefined - }) const question_snapshot = createMemo(() => { const next = structured() return next?.kind === "question" ? next : undefined @@ -242,25 +218,6 @@ export function RunEntryContent(props: { - - - - # Todos - - - {todo_snapshot()!.items.map((item) => ( - - {todoText(item)} - - ))} - {todo_snapshot()!.tail ? ( - - {todo_snapshot()!.tail} - - ) : null} - - - @@ -338,7 +295,6 @@ export function turnSummaryWriter(input: { agent: string; model: string; duratio () => ( - {input.agent} {" "} diff --git a/packages/cli/src/mini/session-data.ts b/packages/cli/src/mini/session-data.ts new file mode 100644 index 0000000000..30a7f6328e --- /dev/null +++ b/packages/cli/src/mini/session-data.ts @@ -0,0 +1,17 @@ +import type { PermissionV2Request, QuestionV2Request } from "@opencode-ai/client/promise" +import type { FooterView } from "./types" + +export function pickBlockerView(input: { + permission?: PermissionV2Request + question?: QuestionV2Request +}): FooterView { + if (input.permission) return { type: "permission", request: input.permission } + if (input.question) return { type: "question", request: input.question } + return { type: "prompt" } +} + +export function blockerStatus(view: FooterView) { + if (view.type === "permission") return "awaiting permission" + if (view.type === "question") return "awaiting answer" + return "" +} diff --git a/packages/cli/src/mini/session.shared.ts b/packages/cli/src/mini/session.shared.ts new file mode 100644 index 0000000000..2b0a99f716 --- /dev/null +++ b/packages/cli/src/mini/session.shared.ts @@ -0,0 +1,101 @@ +import type { SessionMessageInfo, SessionMessageUser } from "@opencode-ai/client/promise" +import { promptCopy, promptSame } from "./prompt.shared" +import type { RunInput, RunPrompt } from "./types" + +const LIMIT = 200 + +export type SessionMessages = SessionMessageInfo[] + +type Turn = { + prompt: RunPrompt + provider: string | undefined + model: string | undefined + variant: string | undefined +} + +export type RunSession = { + first: boolean + turns: Turn[] + model?: NonNullable + variant?: string +} + +function messagePrompt(message: SessionMessageUser): RunPrompt { + return { + text: message.text, + parts: [ + ...(message.files ?? []).map((file) => ({ + type: "file" as const, + url: file.source.type === "uri" ? file.source.uri : `data:${file.mime};base64,${file.data}`, + mime: file.mime, + filename: file.name, + source: file.mention + ? { + type: "file", + path: file.name ?? (file.source.type === "uri" ? file.source.uri : "inline attachment"), + text: { start: file.mention.start, end: file.mention.end, value: file.mention.text }, + } + : undefined, + })), + ...(message.agents ?? []).map((agent) => ({ + type: "agent" as const, + name: agent.name, + source: agent.mention + ? { start: agent.mention.start, end: agent.mention.end, value: agent.mention.text } + : undefined, + })), + ], + } +} + +export function createSession(messages: SessionMessages): RunSession { + return { + first: messages.length === 0, + turns: messages.flatMap((message) => + message.type === "user" + ? [{ prompt: messagePrompt(message), provider: undefined, model: undefined, variant: undefined }] + : [], + ), + } +} + +export async function resolveCurrentSession( + sdk: RunInput["sdk"], + sessionID: string, + limit = LIMIT, +): Promise { + const [response, session] = await Promise.all([ + sdk.message.list({ sessionID, limit, order: "desc" }), + sdk.session.get({ sessionID }), + ]) + const current = createSession(response.data.toReversed()) + return { + ...current, + turns: current.turns.map((turn) => ({ + ...turn, + provider: session.model?.providerID, + model: session.model?.id, + variant: session.model?.variant, + })), + ...(session.model && { + model: { providerID: session.model.providerID, modelID: session.model.id }, + variant: session.model.variant, + }), + } +} + +export function sessionHistory(session: RunSession, limit = LIMIT): RunPrompt[] { + return session.turns + .map((turn) => turn.prompt) + .filter((prompt) => prompt.text.trim()) + .filter((prompt, index, prompts) => index === 0 || !promptSame(prompts[index - 1], prompt)) + .map(promptCopy) + .slice(-limit) +} + +export function sessionVariant(session: RunSession, model: RunInput["model"]): string | undefined { + if (!model) return + if (session.model?.providerID === model.providerID && session.model.modelID === model.modelID) return session.variant + + return session.turns.findLast((turn) => turn.provider === model.providerID && turn.model === model.modelID)?.variant +} diff --git a/packages/opencode/src/cli/cmd/run/splash.ts b/packages/cli/src/mini/splash.ts similarity index 97% rename from packages/opencode/src/cli/cmd/run/splash.ts rename to packages/cli/src/mini/splash.ts index 141ff6fc55..e39adba376 100644 --- a/packages/opencode/src/cli/cmd/run/splash.ts +++ b/packages/cli/src/mini/splash.ts @@ -17,8 +17,8 @@ import { type ScrollbackSnapshot, type ScrollbackWriter, } from "@opentui/core" -import * as Locale from "@/util/locale" -import { go } from "@/cli/logo" +import { Locale } from "@opencode-ai/tui/util/locale" +import { go } from "@opencode-ai/tui/logo" import type { RunSplashTheme } from "./theme" export const SPLASH_TITLE_LIMIT = 50 @@ -234,7 +234,7 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback lines, body_left + label.length, top + 1, - `opencode --mini -s ${meta.session_id}`, + `opencode mini -s ${meta.session_id}`, right, undefined, TextAttributes.BOLD, diff --git a/packages/cli/src/mini/stream-v2.subagent.ts b/packages/cli/src/mini/stream-v2.subagent.ts new file mode 100644 index 0000000000..fd716f4f75 --- /dev/null +++ b/packages/cli/src/mini/stream-v2.subagent.ts @@ -0,0 +1,786 @@ +// Current-native subagent (child Session) tracking for the mini transport. +// +// Discovers child Sessions of the active parent from four current sources: +// 1. projected subagent tool output (`structured.sessionID`) during hydration +// 2. the current session list filtered by `parentID` during hydration +// 3. the process-local active-session map during hydration +// 4. live events from unknown sessions whose `parentID` matches the parent +// +// Tracks one footer tab per child and a detail transcript for the selected +// child, reduced from the same current live event stream the parent uses. +// Detail transcripts rebuild from projected messages on discovery, selection, +// and reconnect, then continue from live deltas using the same +// projected-prefix dedup the parent transport uses. +// +// Per-child interruption uses `v2.session.interrupt(childID)`. Per-child +// backgrounding is intentionally absent: subagent jobs block the parent +// session, so only whole-session `v2.session.background(parentID)` exists. +import type { + EventSubscribeOutput, + OpenCodeClient, + SessionMessageAssistantTool, + SessionMessageInfo, +} from "@opencode-ai/client/promise" +import { Locale } from "@opencode-ai/tui/util/locale" +import type { FooterSubagentDetail, FooterSubagentState, FooterSubagentTab, MiniToolPart, StreamCommit } from "./types" + +const CHILD_MESSAGE_LIMIT = 80 +const CHILD_FRAME_LIMIT = 80 +const CHILD_EVENT_BUFFER_LIMIT = 64 +const FAMILY_LIST_LIMIT = 100 +const FALLBACK_LABEL = "Subagent" + +type V2Event = EventSubscribeOutput + +export function outputText(content: ReadonlyArray<{ type: string; text?: string }>) { + return content.flatMap((item) => (item.type === "text" && item.text ? [item.text] : [])).join("\n") +} + +export function miniTool(input: { + sessionID: string + messageID: string + tool: SessionMessageAssistantTool +}): MiniToolPart { + const tool = input.tool + const providerCall = + tool.executed === undefined && tool.providerState === undefined + ? undefined + : { executed: tool.executed, state: tool.providerState } + const providerResult = + tool.executed === undefined && tool.providerResultState === undefined + ? undefined + : { executed: tool.executed, state: tool.providerResultState } + const base = { + id: `prt_${tool.id}`, + sessionID: input.sessionID, + messageID: input.messageID, + type: "tool" as const, + callID: tool.id, + tool: tool.name, + } + if (tool.state.status === "streaming") { + return { + ...base, + state: { status: "pending", input: {}, raw: tool.state.input }, + } + } + if (tool.state.status === "running") { + return { + ...base, + state: { + status: "running", + input: tool.state.input, + title: tool.name, + metadata: { structured: tool.state.structured, content: tool.state.content, providerCall }, + time: { start: tool.time.ran ?? tool.time.created }, + }, + } + } + if (tool.state.status === "completed") { + return { + ...base, + state: { + status: "completed", + input: tool.state.input, + output: outputText(tool.state.content), + title: tool.name, + metadata: { + structured: tool.state.structured, + content: tool.state.content, + result: tool.state.result, + providerCall, + providerResult, + }, + time: { start: tool.time.ran ?? tool.time.created, end: tool.time.completed ?? tool.time.created }, + }, + } + } + return { + ...base, + state: { + status: "error", + input: tool.state.input, + error: tool.state.error.message, + metadata: { + structured: tool.state.structured, + content: tool.state.content, + result: tool.state.result, + providerCall, + providerResult, + }, + time: { start: tool.time.ran ?? tool.time.created, end: tool.time.completed ?? tool.time.created }, + }, + } +} + +export function toolCommit(part: MiniToolPart, phase: "start" | "progress" | "final"): StreamCommit { + const status = part.state.status + const text = + status === "running" + ? part.tool === "task" + ? "running task" + : `running ${part.tool}` + : status === "completed" + ? part.state.output + : status === "error" + ? part.state.error + : "" + return { + kind: "tool", + source: "tool", + text, + phase, + messageID: part.messageID, + partID: part.id, + tool: part.tool, + part, + toolState: status === "error" ? "error" : status === "completed" ? "completed" : "running", + toolError: status === "error" ? part.state.error : undefined, + } +} + +type Frame = { + key: string + commit: StreamCommit +} + +type ToolTrack = { + name: string + input: Record + started: number + providerState?: Record +} + +type ChildState = { + sessionID: string + label: string + description: string + status: FooterSubagentTab["status"] + background: boolean + title?: string + callIDs: Set + lastUpdatedAt: number + frames: Frame[] + text: Map + projectedText: Map + reasoning: Map + projectedReasoning: Map + tools: Map + finishedTools: Set + messageIDs: Set + prompts: Map + hydrated: boolean +} + +export type SubagentTrackerInput = { + sdk: OpenCodeClient + sessionID: string + thinking: boolean + emit: () => void +} + +export type SubagentTracker = { + main(event: V2Event): void + foreign(sessionID: string, event: V2Event): void + hydrate(next: { messages: SessionMessageInfo[]; active: Record }): Promise + select(sessionID: string | undefined): void + snapshot(): FooterSubagentState +} + +function record(value: unknown): Record | undefined { + if (typeof value === "object" && value !== null && !Array.isArray(value)) return value as Record + return undefined +} + +function text(value: unknown): string | undefined { + if (typeof value !== "string") return undefined + const next = value.trim() + return next || undefined +} + +function childSessionID(structured: Record | undefined) { + const sessionID = text(structured?.sessionID) + if (!sessionID || !sessionID.startsWith("ses")) return undefined + const status = structured?.status + if (status !== "running" && status !== "completed") return undefined + return { sessionID, running: status === "running" } +} + +function tab(child: ChildState): FooterSubagentTab { + return { + sessionID: child.sessionID, + partID: `subagent:${child.sessionID}`, + callID: `subagent:${child.sessionID}`, + label: child.label, + description: child.description || child.title || "", + status: child.status, + background: child.background ? true : undefined, + title: child.title, + toolCalls: child.callIDs.size > 0 ? child.callIDs.size : undefined, + lastUpdatedAt: child.lastUpdatedAt, + } +} + +export function createSubagentTracker(input: SubagentTrackerInput): SubagentTracker { + const children = new Map() + // Live subagent tool calls in the parent, so tool.success structured output + // can be joined with the call's input metadata. + const pendingCalls = new Map>() + // Foreign sessions already resolved through session.get. Non-children stay + // cached so unrelated concurrent sessions are checked at most once. + const checked = new Set() + // Foreign events buffered while a session.get discovery is in flight, so a + // fast child (including its settled event) is not lost mid-discovery. + const pendingEvents = new Map() + const hydrationEvents = new Map() + const hydrationOverflow = new Set() + const hydrations = new Map>() + let selected: string | undefined + const fragmentKey = (messageID: string, partID: string) => `${messageID}\u0000${partID}` + + const ensureChild = (sessionID: string): ChildState => { + const existing = children.get(sessionID) + const child: ChildState = existing ?? { + sessionID, + label: FALLBACK_LABEL, + description: "", + status: "running", + background: false, + callIDs: new Set(), + lastUpdatedAt: Date.now(), + frames: [], + text: new Map(), + projectedText: new Map(), + reasoning: new Map(), + projectedReasoning: new Map(), + tools: new Map(), + finishedTools: new Set(), + messageIDs: new Set(), + prompts: new Map(), + hydrated: false, + } + if (!existing) children.set(sessionID, child) + // Adopting a child while its session.get discovery is still in flight: + // drain the buffered events now. They arrived before whatever the caller + // applies next, so replaying them first preserves bus order, and the + // resolved discovery can no longer replay stale events (e.g. step.started) + // after a terminal settled event was applied directly. + const buffered = pendingEvents.get(sessionID) + if (buffered) { + pendingEvents.delete(sessionID) + for (const event of buffered) reduce(child, event) + } + return child + } + + const touch = (child: ChildState, timestamp?: number) => { + child.lastUpdatedAt = Math.max(child.lastUpdatedAt, timestamp ?? Date.now()) + } + + const notifyDetail = (child: ChildState) => { + if (child.sessionID === selected) input.emit() + } + + const setFrame = (child: ChildState, key: string, commit: StreamCommit) => { + const index = child.frames.findIndex((item) => item.key === key) + if (index === -1) { + child.frames.push({ key, commit }) + if (child.frames.length > CHILD_FRAME_LIMIT) child.frames.splice(0, child.frames.length - CHILD_FRAME_LIMIT) + return + } + child.frames[index] = { key, commit } + } + + const applyMeta = (child: ChildState, meta: Record | undefined) => { + if (!meta) return + const agent = text(meta.agent) + if (agent) child.label = Locale.titlecase(agent) + const description = text(meta.description) + if (description) child.description = description + if (meta.background === true) child.background = true + } + + const userFrame = (child: ChildState, messageID: string, value: string) => { + if (child.messageIDs.has(messageID)) return false + child.messageIDs.add(messageID) + setFrame(child, `user:${messageID}`, { + kind: "user", + source: "system", + text: value, + phase: "start", + messageID, + }) + return true + } + + const childTool = (child: ChildState, item: SessionMessageAssistantTool, messageID: string) => { + const part = miniTool({ + sessionID: child.sessionID, + messageID, + tool: item, + }) + if (item.state.status === "streaming") return + child.callIDs.add(item.id) + if (item.state.status === "running") { + setFrame(child, `tool:${item.id}`, toolCommit(part, "start")) + return + } + child.finishedTools.add(item.id) + child.tools.delete(item.id) + setFrame(child, `tool:${item.id}`, toolCommit(part, "final")) + } + + const rebuild = (child: ChildState, messages: SessionMessageInfo[]) => { + child.frames = [] + child.text.clear() + child.projectedText.clear() + child.reasoning.clear() + child.projectedReasoning.clear() + child.finishedTools.clear() + child.messageIDs.clear() + child.callIDs.clear() + for (const message of messages) { + if (message.type === "user") { + child.prompts.delete(message.id) + userFrame(child, message.id, message.text) + continue + } + if (message.type !== "assistant") continue + child.messageIDs.add(message.id) + let textOrdinal = 0 + let reasoningOrdinal = 0 + for (const item of message.content) { + if (item.type === "text") { + const id = `text:${textOrdinal++}` + const key = fragmentKey(message.id, id) + child.text.set(key, item.text) + child.projectedText.set(key, item.text) + setFrame(child, key, { + kind: "assistant", + source: "assistant", + text: item.text, + phase: "progress", + messageID: message.id, + partID: id, + }) + continue + } + if (item.type === "reasoning") { + const id = `reasoning:${reasoningOrdinal++}` + const key = fragmentKey(message.id, id) + child.reasoning.set(key, item.text) + child.projectedReasoning.set(key, item.text) + if (input.thinking) + setFrame(child, key, { + kind: "reasoning", + source: "reasoning", + text: `Thinking: ${item.text}`, + phase: "progress", + messageID: message.id, + partID: id, + }) + continue + } + childTool(child, item, message.id) + } + if (message.error) { + setFrame(child, `error:${message.id}`, { + kind: "error", + source: "system", + text: message.error.message, + phase: "start", + messageID: message.id, + }) + } + } + } + + const hydrateChild = (child: ChildState): Promise => { + const existing = hydrations.get(child.sessionID) + if (existing) return existing + const pendingPrompts = new Map(child.prompts) + const pendingTools = new Map(child.tools) + let retry = false + const task = input.sdk.message + .list({ sessionID: child.sessionID, limit: CHILD_MESSAGE_LIMIT, order: "desc" }) + .then((response) => { + const buffered = hydrationEvents.get(child.sessionID) ?? [] + hydrationEvents.delete(child.sessionID) + if (hydrationOverflow.delete(child.sessionID)) { + child.hydrated = false + retry = true + notifyDetail(child) + return + } + for (const [id, prompt] of pendingPrompts) { + if (!child.prompts.has(id)) child.prompts.set(id, prompt) + } + rebuild(child, structuredClone(response.data).toReversed() as SessionMessageInfo[]) + for (const [id, tool] of pendingTools) { + if (!child.finishedTools.has(id) && !child.tools.has(id)) child.tools.set(id, tool) + } + for (const event of buffered) reduce(child, event) + child.hydrated = true + notifyDetail(child) + }) + .catch(() => { + hydrationEvents.delete(child.sessionID) + hydrationOverflow.delete(child.sessionID) + }) + .finally(() => { + hydrations.delete(child.sessionID) + if (retry) queueMicrotask(() => void hydrateChild(child)) + }) + hydrations.set(child.sessionID, task) + return task + } + + const discover = (sessionID: string) => { + if (checked.has(sessionID) || children.has(sessionID) || sessionID === input.sessionID) return + checked.add(sessionID) + if (!pendingEvents.has(sessionID)) pendingEvents.set(sessionID, []) + void input.sdk.session + .get({ sessionID }) + .then((session) => { + const buffered = pendingEvents.get(sessionID) ?? [] + pendingEvents.delete(sessionID) + if (session.parentID !== input.sessionID) return + const child = ensureChild(sessionID) + if (session.agent) child.label = Locale.titlecase(session.agent) + child.title = session.title + for (const event of buffered) reduce(child, event) + touch(child) + input.emit() + void hydrateChild(child) + }) + .catch(() => { + // Allow a later event to retry discovery after transient failures. + pendingEvents.delete(sessionID) + checked.delete(sessionID) + }) + } + + const reduce = (child: ChildState, event: V2Event) => { + if (event.type === "session.input.admitted") { + if (event.data.input.type === "user") child.prompts.set(event.data.inputID, event.data.input.data.text) + return + } + if (event.type === "session.input.promoted") { + const prompt = child.prompts.get(event.data.inputID) + if (prompt === undefined) return + child.prompts.delete(event.data.inputID) + if (userFrame(child, event.data.inputID, prompt)) { + touch(child, event.created) + notifyDetail(child) + } + return + } + if (event.type === "session.step.started") { + touch(child, event.created) + if (child.label === FALLBACK_LABEL && event.data.agent) child.label = Locale.titlecase(event.data.agent) + if (child.status !== "running") child.status = "running" + input.emit() + return + } + if (event.type === "session.text.started") { + return + } + if (event.type === "session.text.delta") { + const id = `text:${event.data.ordinal}` + const key = fragmentKey(event.data.assistantMessageID, id) + const projected = child.projectedText.get(key) + const covered = projected?.indexOf(event.data.delta) ?? -1 + if (projected && covered >= 0) { + child.projectedText.set(key, projected.slice(covered + event.data.delta.length)) + return + } + const next = (child.text.get(key) ?? "") + event.data.delta + child.text.set(key, next) + setFrame(child, key, { + kind: "assistant", + source: "assistant", + text: next, + phase: "progress", + messageID: event.data.assistantMessageID, + partID: id, + }) + touch(child, event.created) + notifyDetail(child) + return + } + if (event.type === "session.text.ended") { + const id = `text:${event.data.ordinal}` + const key = fragmentKey(event.data.assistantMessageID, id) + child.text.set(key, event.data.text) + child.projectedText.delete(key) + setFrame(child, key, { + kind: "assistant", + source: "assistant", + text: event.data.text, + phase: "progress", + messageID: event.data.assistantMessageID, + partID: id, + }) + touch(child, event.created) + notifyDetail(child) + return + } + if (event.type === "session.reasoning.started") { + return + } + if (event.type === "session.reasoning.delta") { + const id = `reasoning:${event.data.ordinal}` + const key = fragmentKey(event.data.assistantMessageID, id) + const projected = child.projectedReasoning.get(key) + const covered = projected?.indexOf(event.data.delta) ?? -1 + if (projected && covered >= 0) { + child.projectedReasoning.set(key, projected.slice(covered + event.data.delta.length)) + return + } + const next = (child.reasoning.get(key) ?? "") + event.data.delta + child.reasoning.set(key, next) + if (!input.thinking) return + setFrame(child, key, { + kind: "reasoning", + source: "reasoning", + text: `Thinking: ${next}`, + phase: "progress", + messageID: event.data.assistantMessageID, + partID: id, + }) + notifyDetail(child) + return + } + if (event.type === "session.reasoning.ended") { + const id = `reasoning:${event.data.ordinal}` + const key = fragmentKey(event.data.assistantMessageID, id) + child.reasoning.set(key, event.data.text) + child.projectedReasoning.delete(key) + if (!input.thinking) return + setFrame(child, key, { + kind: "reasoning", + source: "reasoning", + text: `Thinking: ${event.data.text}`, + phase: "progress", + messageID: event.data.assistantMessageID, + partID: id, + }) + notifyDetail(child) + return + } + if (event.type === "session.tool.input.started") { + if (child.finishedTools.has(event.data.callID)) return + child.tools.set(event.data.callID, { name: event.data.name, input: {}, started: event.created }) + return + } + if (event.type === "session.tool.called") { + if (child.finishedTools.has(event.data.callID)) return + const current = child.tools.get(event.data.callID) + child.tools.set(event.data.callID, { + name: current?.name ?? "tool", + input: event.data.input, + started: current?.started ?? event.created, + providerState: event.data.state, + }) + childTool( + child, + structuredClone({ + type: "tool", + id: event.data.callID, + name: current?.name ?? "tool", + executed: event.data.executed, + providerState: event.data.state, + state: { status: "running", input: event.data.input, structured: {}, content: [] }, + time: { created: current?.started ?? event.created, ran: event.created }, + }) as SessionMessageAssistantTool, + event.data.assistantMessageID, + ) + touch(child, event.created) + notifyDetail(child) + return + } + if (event.type === "session.tool.success" || event.type === "session.tool.failed") { + if (child.finishedTools.has(event.data.callID)) return + const current = child.tools.get(event.data.callID) + const failed = event.type === "session.tool.failed" + childTool( + child, + structuredClone({ + type: "tool", + id: event.data.callID, + name: current?.name ?? "tool", + executed: event.data.executed, + providerState: current?.providerState, + providerResultState: event.data.resultState, + state: failed + ? { + status: "error", + input: current?.input ?? {}, + structured: {}, + content: [], + error: event.data.error, + result: event.data.result, + } + : { + status: "completed", + input: current?.input ?? {}, + structured: event.data.structured, + content: event.data.content, + result: event.data.result, + }, + time: { + created: current?.started ?? event.created, + ran: current?.started, + completed: event.created, + }, + }) as SessionMessageAssistantTool, + event.data.assistantMessageID, + ) + touch(child, event.created) + notifyDetail(child) + return + } + if (event.type === "session.step.ended") return + if (event.type === "session.step.failed") { + setFrame(child, `error:step:${event.data.assistantMessageID}`, { + kind: "error", + source: "system", + text: event.data.error.message, + phase: "start", + messageID: event.data.assistantMessageID, + }) + touch(child, event.created) + notifyDetail(child) + return + } + if (event.type === "session.execution.started") { + child.status = "running" + touch(child, event.created) + input.emit() + return + } + if ( + event.type === "session.execution.succeeded" || + event.type === "session.execution.failed" || + event.type === "session.execution.interrupted" + ) { + child.status = + event.type === "session.execution.succeeded" + ? "completed" + : event.type === "session.execution.interrupted" + ? "cancelled" + : "error" + touch(child, event.created) + input.emit() + } + } + + const mainTool = (item: SessionMessageAssistantTool, active?: Record) => { + if (item.name !== "subagent" || item.state.status !== "completed") return + const found = childSessionID(record(item.state.structured)) + if (!found) return + const child = ensureChild(found.sessionID) + applyMeta(child, record(item.state.input)) + if (found.running) child.background = true + if (child.status === "running") { + const running = found.running && (!active || found.sessionID in active) + child.status = running ? "running" : "completed" + } + touch(child, item.time.completed ?? item.time.created) + } + + return { + main(event) { + if (event.type === "session.tool.input.started") { + if (event.data.name === "subagent") pendingCalls.set(event.data.callID, {}) + return + } + if (event.type === "session.tool.called") { + if (pendingCalls.has(event.data.callID)) pendingCalls.set(event.data.callID, event.data.input) + return + } + if (event.type === "session.tool.failed") { + pendingCalls.delete(event.data.callID) + return + } + if (event.type !== "session.tool.success") return + const pending = pendingCalls.get(event.data.callID) + pendingCalls.delete(event.data.callID) + const found = childSessionID(record(event.data.structured)) + if (!found) return + const child = ensureChild(found.sessionID) + applyMeta(child, pending) + if (found.running) { + child.background = true + child.status = "running" + } + if (!found.running && child.status === "running") child.status = "completed" + touch(child, event.created) + input.emit() + if (!child.hydrated) void hydrateChild(child) + }, + foreign(sessionID, event) { + const child = children.get(sessionID) + if (child) { + if (hydrations.has(sessionID)) { + const buffered = hydrationEvents.get(sessionID) ?? [] + if (buffered.length < CHILD_EVENT_BUFFER_LIMIT) buffered.push(event) + else hydrationOverflow.add(sessionID) + hydrationEvents.set(sessionID, buffered) + } + reduce(child, event) + return + } + discover(sessionID) + const buffered = pendingEvents.get(sessionID) + if (buffered && buffered.length < CHILD_EVENT_BUFFER_LIMIT) buffered.push(event) + }, + async hydrate(next) { + for (const message of next.messages) { + if (message.type !== "assistant") continue + for (const item of message.content) { + if (item.type === "tool") mainTool(item, next.active) + } + } + // Family index: adopt children directly from the current session list so + // historical subagents beyond the projected message window still get tabs. + const family = await input.sdk.session + .list({ parentID: input.sessionID, limit: FAMILY_LIST_LIMIT, order: "desc" }) + .then((response) => response.data) + .catch(() => []) + for (const session of family) { + const child = ensureChild(session.id) + if (session.agent && child.label === FALLBACK_LABEL) child.label = Locale.titlecase(session.agent) + if (!child.title) child.title = session.title + touch(child, session.time.updated) + } + for (const sessionID of Object.keys(next.active)) discover(sessionID) + for (const child of children.values()) { + // Reconnect can miss a child's settled event; the active map is the + // authoritative live signal for still-running children. + if (child.status === "running" && !(child.sessionID in next.active)) child.status = "completed" + } + const current = selected ? children.get(selected) : undefined + if (current) await hydrateChild(current) + if (children.size > 0) input.emit() + }, + select(sessionID) { + selected = sessionID + const child = sessionID ? children.get(sessionID) : undefined + if (child && !child.hydrated) void hydrateChild(child) + input.emit() + }, + snapshot() { + const tabs = [...children.values()].map(tab).toSorted((a, b) => { + const active = Number(b.status === "running") - Number(a.status === "running") + if (active !== 0) return active + return b.lastUpdatedAt - a.lastUpdatedAt + }) + const child = selected ? children.get(selected) : undefined + const details: Record = child + ? { [child.sessionID]: { sessionID: child.sessionID, commits: child.frames.map((item) => item.commit) } } + : {} + return { tabs, details, permissions: [], questions: [] } + }, + } +} diff --git a/packages/cli/src/mini/stream-v2.transport.ts b/packages/cli/src/mini/stream-v2.transport.ts new file mode 100644 index 0000000000..c595aab88a --- /dev/null +++ b/packages/cli/src/mini/stream-v2.transport.ts @@ -0,0 +1,1094 @@ +import { readFile } from "node:fs/promises" +import type { + EventSubscribeOutput, + OpenCodeClient, + PermissionV2Request, + QuestionV2Request, + SessionMessageAssistantTool, + SessionMessageInfo, +} from "@opencode-ai/client/promise" +import { Event } from "@opencode-ai/schema/event" +import { blockerStatus, pickBlockerView } from "./session-data" +import { writeSessionOutput } from "./stream" +import { createSubagentTracker, miniTool, toolCommit } from "./stream-v2.subagent" +import type { + FooterApi, + FooterView, + LocalReplayAnchor, + LocalReplayRow, + RunFilePart, + RunInput, + RunPrompt, + RunPromptPart, + RunProvider, + StreamCommit, +} from "./types" + +type Trace = { + write(type: string, data?: unknown): void +} + +type StreamInput = { + sdk: OpenCodeClient + directory?: string + sessionID: string + thinking: boolean + replay?: boolean + replayLimit?: number + limits: () => Record + providers?: () => RunProvider[] + footer: FooterApi + trace?: Trace + signal?: AbortSignal + onCatalogRefresh?: () => void +} + +export type SessionTurnInput = { + agent: string | undefined + model: RunInput["model"] + variant: string | undefined + prompt: RunPrompt + files: RunFilePart[] + includeFiles: boolean + onVisibleOutput?: (anchor: LocalReplayAnchor) => void + signal?: AbortSignal +} + +export type SessionResizeReplayInput = { + localRows: () => LocalReplayRow[] + reset: () => Promise +} + +export type SessionTransport = { + runPromptTurn(input: SessionTurnInput): Promise + interruptActiveTurn(): Promise + selectSubagent(sessionID: string | undefined): void + replayOnResize(input: SessionResizeReplayInput): Promise + close(): Promise +} + +type Wait = { + messageID: string + promoted: boolean + interrupted: boolean + failureRendered: boolean + resolve: () => void + reject: (error: unknown) => void + onVisibleOutput?: (anchor: LocalReplayAnchor) => void +} + +// One active session.shell call. The HTTP response is the completion signal; +// callID correlates the live shell events once shell.started is observed, and +// abort cancels the blocking request when the user interrupts the turn. +type ShellWait = { + eventID: string + messageID: string + callID?: string + resolve: () => void + abort: () => void +} + +type RunV2Event = EventSubscribeOutput +type PromptFilePart = Extract + +type ToolState = { + messageID: string + name: string + input: Record + started: number + running: boolean + providerState?: Record +} + +type State = { + permissions: PermissionV2Request[] + questions: QuestionV2Request[] + view: FooterView + messageIDs: Set + text: Map + projectedText: Map + reasoning: Map + projectedReasoning: Map + tools: Map + finishedTools: Set + skillMessages: Set + shellCommands: Map + shellStarted: Set + shellEnded: Set + shellWait?: ShellWait + wait?: Wait + connected: boolean + closed: boolean + initial: boolean + buffered?: RunV2Event[] + errors: Set +} + +const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }) + +export function formatUnknownError(error: unknown): string { + if (typeof error === "string") return error + if (error instanceof Error) return error.message || error.name + if (error && typeof error === "object") { + const message = Reflect.get(error, "message") + if (typeof message === "string" && message.trim()) return message + const tag = Reflect.get(error, "_tag") + if (typeof tag === "string" && tag.trim()) return tag + } + return "unknown error" +} + +function sessionID(event: RunV2Event) { + return "sessionID" in event.data && typeof event.data.sessionID === "string" ? event.data.sessionID : undefined +} + +function errorMessage(error: { message?: string; _tag?: string }) { + return error.message || error._tag || "Session execution failed" +} + +function wait(delay: number, signal: AbortSignal) { + return new Promise((resolve) => { + const timer = setTimeout(done, delay) + signal.addEventListener("abort", done, { once: true }) + function done() { + clearTimeout(timer) + signal.removeEventListener("abort", done) + resolve() + } + }) +} + +async function prepareFile(file: RunFilePart) { + if (file.mime !== "text/plain") return { attachment: { uri: file.url, name: file.filename } } + const content = file.url.startsWith("data:") + ? Buffer.from(file.url.slice(file.url.indexOf(",") + 1), "base64").toString("utf8") + : await readFile(new URL(file.url), "utf8") + return { text: `\n${content}\n` } +} + +function promptFileMention(part: PromptFilePart) { + if (!part.source?.text) return + return { + start: part.source.text.start, + end: part.source.text.end, + text: part.source.text.value, + } +} + +function promptFiles(next: SessionTurnInput) { + return next.prompt.parts.flatMap((part) => + part.type === "file" + ? [ + { + uri: part.url, + name: part.filename, + mention: promptFileMention(part), + }, + ] + : [], + ) +} + +function promptAgents(next: SessionTurnInput) { + return next.prompt.parts.flatMap((part) => + part.type === "agent" + ? [ + { + name: part.name, + mention: part.source + ? { start: part.source.start, end: part.source.end, text: part.source.value } + : undefined, + }, + ] + : [], + ) +} + +function streamPartKey(messageID: string, partID: string) { + return `${messageID}\u0000${partID}` +} + +// Direct shell calls use one "start" commit rendering `$ command` and one "progress" +// commit rendering the merged output (see toolEntryBody in tool.ts). +function shellCommit( + callID: string, + command: string, + next: Pick, +): StreamCommit { + return { + kind: "tool", + source: "tool", + partID: `shell:${callID}`, + tool: "shell", + shell: { callID, command }, + ...next, + } +} + +function shellTerminal( + callID: string, + command: string, + shell: { status: string; exit?: number | string }, + output: { output: string; cursor: number; size: number; truncated: boolean }, +) { + const incomplete = output.truncated || output.cursor < output.size + const text = `${output.output}${incomplete ? `${output.output.endsWith("\n") || !output.output ? "" : "\n"}[output truncated]` : ""}` + const error = + shell.status === "exited" && shell.exit === 0 + ? undefined + : shell.status === "exited" + ? `Shell exited with code ${shell.exit ?? "unknown"}` + : `Shell ${shell.status}` + if (!error) return [shellCommit(callID, command, { text, phase: "progress", toolState: "completed" })] + return [ + ...(text ? [shellCommit(callID, command, { text, phase: "progress", toolState: "running" })] : []), + shellCommit(callID, command, { text: error, phase: "final", toolState: "error", toolError: error }), + ] +} + +function messageIDFromEvent(id: string) { + return id.replace(/^evt_/, "msg_") +} + +const catalogEvents = new Set([ + "catalog.updated", + "integration.updated", + "agent.updated", + "command.updated", + "skill.updated", + "reference.updated", +]) + +// session.shell resolves after the command settled server-side; the matching +// live shell.ended event usually lands within the same tick, but hold the turn +// briefly so the output commit renders inside it. +const SHELL_OUTPUT_GRACE_MS = 1500 + +function skillCommit(messageID: string, name: string): StreamCommit { + return { + kind: "system", + source: "system", + messageID, + partID: `skill:${messageID}`, + text: `→ Skill "${name}"`, + phase: "start", + } +} + +async function resolveSelectedModel(input: StreamInput, next: Pick) { + if (next.model) return { providerID: next.model.providerID, id: next.model.modelID, variant: next.variant } + if (!next.variant) return + + const session = await input.sdk.session + .get({ sessionID: input.sessionID }, { signal: next.signal }) + .then((response) => response.model) + if (session) return { ...session, variant: next.variant } + + const fallback = await input.sdk.model.default(undefined, { signal: next.signal }).then((response) => response.data) + if (!fallback) return + return { providerID: fallback.providerID, id: fallback.id, variant: next.variant } +} + +export async function createSessionTransport(input: StreamInput): Promise { + const controller = new AbortController() + input.signal?.addEventListener("abort", () => controller.abort(), { once: true }) + const state: State = { + permissions: [], + questions: [], + view: { type: "prompt" }, + messageIDs: new Set(), + text: new Map(), + projectedText: new Map(), + reasoning: new Map(), + projectedReasoning: new Map(), + tools: new Map(), + finishedTools: new Set(), + skillMessages: new Set(), + shellCommands: new Map(), + shellStarted: new Set(), + shellEnded: new Set(), + connected: false, + closed: false, + initial: true, + errors: new Set(), + } + let readyResolve!: () => void + let readyReject!: (error: unknown) => void + const ready = new Promise((resolve, reject) => { + readyResolve = resolve + readyReject = reject + }) + const abortReady = () => readyReject(new Error("Mini closed before the event stream connected")) + controller.signal.addEventListener("abort", abortReady, { once: true }) + const offFooterClose = input.footer.onClose(() => controller.abort()) + + const subagents = createSubagentTracker({ + sdk: input.sdk, + sessionID: input.sessionID, + thinking: input.thinking, + emit: () => { + if (state.closed || input.footer.isClosed) return + writeSessionOutput( + { footer: input.footer, trace: input.trace }, + { commits: [], footer: { subagent: subagents.snapshot() } }, + ) + }, + }) + + const write = (commits: StreamCommit[], patch?: { phase?: "idle" | "running"; status?: string; usage?: string }) => { + const visible = commits.at(-1) + if (visible) { + state.wait?.onVisibleOutput?.({ + kind: visible.kind, + text: visible.text, + phase: visible.phase, + messageID: visible.messageID, + partID: visible.partID, + toolState: visible.toolState, + }) + } + writeSessionOutput({ footer: input.footer, trace: input.trace }, { commits, footer: patch ? { patch } : undefined }) + } + + const syncBlockers = () => { + const next = pickBlockerView({ permission: state.permissions[0], question: state.questions[0] }) + if (next.type === "prompt" && state.view.type === "prompt") return + if (next.type !== "prompt" && state.view.type === next.type && next.request.id === state.view.request.id) return + state.view = next + writeSessionOutput( + { footer: input.footer, trace: input.trace }, + { commits: [], footer: { view: next, patch: { status: blockerStatus(next) } } }, + ) + } + + const renderTool = (messageID: string, item: SessionMessageAssistantTool) => { + const part = miniTool({ + sessionID: input.sessionID, + messageID, + tool: item, + }) + if (item.state.status === "streaming") return + if (item.state.status === "running") { + if (state.tools.get(item.id)?.running) return + state.tools.set(item.id, { + messageID, + name: item.name, + input: item.state.input, + started: item.time.ran ?? item.time.created, + running: true, + providerState: item.providerState, + }) + write([toolCommit(part, "start")], { phase: "running", status: `running ${item.name}` }) + return + } + if (state.finishedTools.has(item.id)) return + if (!state.tools.get(item.id)?.running) write([toolCommit(part, "start")]) + state.finishedTools.add(item.id) + state.tools.delete(item.id) + write([ + toolCommit( + part, + item.state.status === "completed" && part.state.status === "completed" && part.state.output + ? "progress" + : "final", + ), + ]) + } + + const renderMessage = (message: SessionMessageInfo, render: boolean, reuseVisibleWait: boolean) => { + if (message.type === "user") { + const waiting = state.wait?.messageID === message.id + if (waiting && state.wait) state.wait.promoted = true + if (!render || state.messageIDs.has(message.id)) return + state.messageIDs.add(message.id) + if (reuseVisibleWait && waiting) return + write([{ kind: "user", source: "system", text: message.text, phase: "start", messageID: message.id }]) + return + } + if (message.type === "skill") { + if (state.wait?.messageID === message.id) state.wait.promoted = true + if (!render || state.skillMessages.has(message.id)) { + state.skillMessages.add(message.id) + return + } + state.skillMessages.add(message.id) + write([skillCommit(message.id, message.name)]) + return + } + if (message.type === "shell") { + state.shellCommands.set(message.shellID, message.command) + if (state.shellWait?.messageID === message.id) state.shellWait.callID = message.shellID + const completed = message.time.completed !== undefined + if (!render) { + // Suppressed history: mark settled shells rendered so live redelivery + // stays silent. A still-running shell stays unmarked and renders in + // full when its live shell.ended event arrives. + if (completed) { + state.shellStarted.add(message.shellID) + state.shellEnded.add(message.shellID) + } + return + } + if (!state.shellStarted.has(message.shellID)) { + state.shellStarted.add(message.shellID) + write([ + shellCommit(message.shellID, message.command, { + text: "running shell", + phase: "start", + toolState: "running", + }), + ]) + } + if (completed && message.output && !state.shellEnded.has(message.shellID)) { + state.shellEnded.add(message.shellID) + write(shellTerminal(message.shellID, message.command, message, message.output)) + } + if (completed && state.shellWait?.callID === message.shellID) state.shellWait.resolve() + return + } + if (message.type !== "assistant") return + state.messageIDs.add(message.id) + let textOrdinal = 0 + let reasoningOrdinal = 0 + for (const item of message.content) { + if (item.type === "text") { + const id = `text:${textOrdinal++}` + const key = streamPartKey(message.id, id) + const sent = state.text.get(key)?.length ?? 0 + state.text.set(key, item.text) + if (render) state.projectedText.set(key, item.text) + if (render && item.text.length > sent) + write([ + { + kind: "assistant", + source: "assistant", + text: item.text.slice(sent), + phase: "progress", + messageID: message.id, + partID: id, + }, + ]) + continue + } + if (item.type === "reasoning") { + const id = `reasoning:${reasoningOrdinal++}` + const key = streamPartKey(message.id, id) + const sent = state.reasoning.get(key)?.length ?? 0 + state.reasoning.set(key, item.text) + if (render) state.projectedReasoning.set(key, item.text) + if (render && input.thinking && item.text.length > sent) + write([ + { + kind: "reasoning", + source: "reasoning", + text: sent === 0 ? `Thinking: ${item.text}` : item.text.slice(sent), + phase: "progress", + messageID: message.id, + partID: id, + }, + ]) + continue + } + if (render) renderTool(message.id, item) + } + if (render && message.error && !state.errors.has(message.id)) { + state.errors.add(message.id) + write([ + { + kind: "error", + source: "system", + text: errorMessage(message.error), + phase: "start", + messageID: message.id, + }, + ]) + } + } + + const hydrate = async (next: { render: boolean; reuseVisibleWait: boolean }) => { + const [messages, permissions, questions, active] = await Promise.all([ + input.sdk.message.list({ sessionID: input.sessionID, limit: input.replayLimit ?? 200, order: "desc" }), + input.sdk.permission.list({ sessionID: input.sessionID }), + input.sdk.question.list({ sessionID: input.sessionID }), + input.sdk.session.active(), + ]) + const projected = structuredClone(messages.data).toReversed() as SessionMessageInfo[] + for (const message of projected) renderMessage(message, next.render, next.reuseVisibleWait) + state.permissions = permissions + state.questions = questions + syncBlockers() + await subagents.hydrate({ messages: [...projected], active }) + const running = input.sessionID in active + write([], { phase: running ? "running" : "idle", status: running ? "assistant responding" : "" }) + if (!running && state.wait && (state.wait.promoted || state.wait.interrupted)) { + const current = state.wait + state.wait = undefined + current.resolve() + } + } + + const apply = (event: RunV2Event) => { + if (catalogEvents.has(event.type)) { + if (input.directory && event.location?.directory && event.location.directory !== input.directory) return + input.onCatalogRefresh?.() + return + } + const source = sessionID(event) + if (source !== input.sessionID) { + if (source) subagents.foreign(source, event) + return + } + input.trace?.write("recv.event", event) + subagents.main(event) + if (event.type === "session.input.promoted") { + if (state.wait?.messageID === event.data.inputID) state.wait.promoted = true + state.messageIDs.add(event.data.inputID) + write([], { phase: "running", status: "waiting for assistant" }) + return + } + if (event.type === "session.step.started") { + write([], { phase: "running", status: "assistant responding" }) + return + } + if (event.type === "session.skill.activated") { + const messageID = messageIDFromEvent(event.id) + if (state.wait?.messageID === messageID) state.wait.promoted = true + if (state.skillMessages.has(messageID)) return + state.skillMessages.add(messageID) + write([skillCommit(messageID, event.data.name)]) + return + } + if (event.type === "session.shell.started") { + state.shellCommands.set(event.data.shell.id, event.data.shell.command) + const wait = state.shellWait + if (wait?.eventID === event.id) wait.callID = event.data.shell.id + if (state.shellStarted.has(event.data.shell.id)) return + state.shellStarted.add(event.data.shell.id) + write( + [ + shellCommit(event.data.shell.id, event.data.shell.command, { + text: "running shell", + phase: "start", + toolState: "running", + }), + ], + { + phase: "running", + status: "running shell", + }, + ) + return + } + if (event.type === "session.shell.ended") { + const command = state.shellCommands.get(event.data.shell.id) ?? event.data.shell.command + const commits: StreamCommit[] = [] + if (!state.shellStarted.has(event.data.shell.id)) { + state.shellStarted.add(event.data.shell.id) + if (command) + commits.push( + shellCommit(event.data.shell.id, command, { text: "running shell", phase: "start", toolState: "running" }), + ) + } + if (!state.shellEnded.has(event.data.shell.id)) { + state.shellEnded.add(event.data.shell.id) + commits.push(...shellTerminal(event.data.shell.id, command, event.data.shell, event.data.output)) + } + const wait = state.shellWait + const owned = wait?.callID === event.data.shell.id + write(commits, owned || state.wait || state.shellWait ? undefined : { phase: "idle", status: "" }) + if (owned) wait.resolve() + return + } + if (event.type === "session.text.started") { + return + } + if (event.type === "session.text.delta") { + const id = `text:${event.data.ordinal}` + const key = streamPartKey(event.data.assistantMessageID, id) + const projected = state.projectedText.get(key) + const covered = projected?.indexOf(event.data.delta) ?? -1 + if (projected && covered >= 0) { + state.projectedText.set(key, projected.slice(covered + event.data.delta.length)) + return + } + const previous = state.text.get(key) ?? "" + state.text.set(key, previous + event.data.delta) + write([ + { + kind: "assistant", + source: "assistant", + text: event.data.delta, + phase: "progress", + messageID: event.data.assistantMessageID, + partID: id, + }, + ]) + return + } + if (event.type === "session.text.ended") { + const id = `text:${event.data.ordinal}` + const key = streamPartKey(event.data.assistantMessageID, id) + const previous = state.text.get(key) ?? "" + if (event.data.text.length > previous.length) + write([ + { + kind: "assistant", + source: "assistant", + text: event.data.text.slice(previous.length), + phase: "progress", + messageID: event.data.assistantMessageID, + partID: id, + }, + ]) + state.text.set(key, event.data.text) + state.projectedText.delete(key) + return + } + if (event.type === "session.reasoning.started") { + return + } + if (event.type === "session.reasoning.delta") { + const id = `reasoning:${event.data.ordinal}` + const key = streamPartKey(event.data.assistantMessageID, id) + const projected = state.projectedReasoning.get(key) + const covered = projected?.indexOf(event.data.delta) ?? -1 + if (projected && covered >= 0) { + state.projectedReasoning.set(key, projected.slice(covered + event.data.delta.length)) + return + } + const previous = state.reasoning.get(key) ?? "" + state.reasoning.set(key, previous + event.data.delta) + if (input.thinking) + write([ + { + kind: "reasoning", + source: "reasoning", + text: previous ? event.data.delta : `Thinking: ${event.data.delta}`, + phase: "progress", + messageID: event.data.assistantMessageID, + partID: id, + }, + ]) + return + } + if (event.type === "session.reasoning.ended") { + const id = `reasoning:${event.data.ordinal}` + const key = streamPartKey(event.data.assistantMessageID, id) + const previous = state.reasoning.get(key) ?? "" + if (input.thinking && event.data.text.length > previous.length) + write([ + { + kind: "reasoning", + source: "reasoning", + text: previous ? event.data.text.slice(previous.length) : `Thinking: ${event.data.text}`, + phase: "progress", + messageID: event.data.assistantMessageID, + partID: id, + }, + ]) + state.reasoning.set(key, event.data.text) + state.projectedReasoning.delete(key) + return + } + if (event.type === "session.tool.input.started") { + state.tools.set(event.data.callID, { + messageID: event.data.assistantMessageID, + name: event.data.name, + input: {}, + started: event.created, + running: false, + }) + return + } + if (event.type === "session.tool.called") { + if (state.finishedTools.has(event.data.callID)) return + const current = state.tools.get(event.data.callID) + const item = structuredClone({ + type: "tool", + id: event.data.callID, + name: current?.name ?? "tool", + executed: event.data.executed, + providerState: event.data.state, + state: { status: "running", input: event.data.input, structured: {}, content: [] }, + time: { created: current?.started ?? event.created, ran: event.created }, + }) as SessionMessageAssistantTool + renderTool(event.data.assistantMessageID, item) + return + } + if (event.type === "session.tool.progress") return + if (event.type === "session.tool.success" || event.type === "session.tool.failed") { + const current = state.tools.get(event.data.callID) + const failed = event.type === "session.tool.failed" + const item = structuredClone({ + type: "tool", + id: event.data.callID, + name: current?.name ?? "tool", + executed: event.data.executed, + providerState: current?.providerState, + providerResultState: event.data.resultState, + state: failed + ? { + status: "error", + input: current?.input ?? {}, + structured: {}, + content: [], + error: event.data.error, + result: event.data.result, + } + : { + status: "completed", + input: current?.input ?? {}, + structured: event.data.structured, + content: event.data.content, + result: event.data.result, + }, + time: { created: current?.started ?? event.created, ran: current?.started, completed: event.created }, + }) as SessionMessageAssistantTool + renderTool(event.data.assistantMessageID, item) + return + } + if (event.type === "permission.v2.asked") { + if (!state.permissions.some((item) => item.id === event.data.id)) state.permissions.push(event.data) + syncBlockers() + return + } + if (event.type === "permission.v2.replied") { + state.permissions = state.permissions.filter((item) => item.id !== event.data.requestID) + syncBlockers() + return + } + if (event.type === "question.v2.asked") { + if (!state.questions.some((item) => item.id === event.data.id)) state.questions.push(event.data) + syncBlockers() + return + } + if (event.type === "question.v2.replied" || event.type === "question.v2.rejected") { + state.questions = state.questions.filter((item) => item.id !== event.data.requestID) + syncBlockers() + return + } + if (event.type === "session.step.ended") { + const total = + event.data.tokens.input + + event.data.tokens.output + + event.data.tokens.reasoning + + event.data.tokens.cache.read + + event.data.tokens.cache.write + const usage = total > 0 ? total.toLocaleString() : "" + write([], { + usage: event.data.cost ? `${usage} · ${money.format(event.data.cost)}` : usage, + }) + return + } + if (event.type === "session.step.failed") { + state.errors.add(event.data.assistantMessageID) + if (state.wait) state.wait.failureRendered = true + write([{ kind: "error", source: "system", text: errorMessage(event.data.error), phase: "start" }]) + return + } + if (event.type === "session.execution.started") { + write([], { phase: "running" }) + return + } + if ( + event.type === "session.execution.succeeded" || + event.type === "session.execution.failed" || + event.type === "session.execution.interrupted" + ) { + write([], { phase: "idle", status: "" }) + const current = state.wait + if (!current || (!current.promoted && !current.interrupted)) return + state.wait = undefined + if (current.interrupted && event.type === "session.execution.interrupted" && event.data.reason === "user") { + current.resolve() + return + } + if (event.type === "session.execution.failed") { + if (current.failureRendered) { + current.resolve() + return + } + current.reject(new Error(errorMessage(event.data.error))) + return + } + if (event.type === "session.execution.interrupted") { + current.reject(new Error(`Session interrupted: ${event.data.reason}`)) + return + } + current.resolve() + } + } + + const receive = (event: RunV2Event) => { + if (state.buffered) { + state.buffered.push(event) + return + } + apply(event) + } + + const connect = async () => { + while (!controller.signal.aborted && !input.footer.isClosed) { + const error = await (async () => { + const connection = new AbortController() + const abortConnection = () => connection.abort() + controller.signal.addEventListener("abort", abortConnection, { once: true }) + const stream = input.sdk.event.subscribe({ signal: connection.signal })[Symbol.asyncIterator]() + try { + const first = await stream.next() + if (first.done || first.value.type !== "server.connected") throw new Error("Event stream disconnected") + const buffered: RunV2Event[] = [] + let booting = true + const consume = (async () => { + while (!connection.signal.aborted) { + const next = await stream.next() + if (next.done) throw new Error("Event stream disconnected") + if (booting) buffered.push(next.value) + else receive(next.value) + } + })() + void consume.catch(() => {}) + await hydrate({ render: state.initial ? input.replay === true : true, reuseVisibleWait: !state.initial }) + input.onCatalogRefresh?.() + state.initial = false + booting = false + for (const event of buffered.splice(0)) apply(event) + state.connected = true + readyResolve() + await consume + } finally { + controller.signal.removeEventListener("abort", abortConnection) + connection.abort() + void stream.return?.(undefined).catch(() => {}) + } + })().catch((error) => error) + state.connected = false + if (controller.signal.aborted || input.footer.isClosed) return + input.trace?.write("recv.reconnect", { error: formatUnknownError(error) }) + write([], { phase: "running", status: "reconnecting" }) + await wait(250, controller.signal) + } + } + const connection = connect() + try { + await ready + } catch (error) { + offFooterClose() + throw error + } finally { + controller.signal.removeEventListener("abort", abortReady) + } + + const runShellTurn = async (next: SessionTurnInput) => { + if (state.wait || state.shellWait) throw new Error("prompt already running") + if (!state.connected) throw new Error("Event stream is reconnecting") + const abort = new AbortController() + const onAbort = () => abort.abort() + next.signal?.addEventListener("abort", onAbort, { once: true }) + let rendered!: () => void + const output = new Promise((resolve) => { + rendered = resolve + }) + const eventID = Event.ID.create() + const active: ShellWait = { + eventID, + messageID: messageIDFromEvent(eventID), + resolve: rendered, + abort: () => abort.abort(), + } + state.shellWait = active + input.trace?.write("send.shell", { sessionID: input.sessionID, id: eventID, command: next.prompt.text }) + write([], { phase: "running", status: "running shell" }) + try { + await input.sdk.session.shell( + { sessionID: input.sessionID, id: eventID, command: next.prompt.text }, + { signal: abort.signal }, + ) + await Promise.race([output, wait(SHELL_OUTPUT_GRACE_MS, abort.signal)]) + } catch (error) { + if (abort.signal.aborted) return + throw error + } finally { + next.signal?.removeEventListener("abort", onAbort) + if (state.shellWait === active) state.shellWait = undefined + } + } + + // Shared settlement scaffolding for prompt-shaped turns: registers the wait, + // wires interruption, sends, then blocks until the live settled event (or a + // hydration pass over an idle session) resolves it. + const runTurnWait = async ( + next: SessionTurnInput, + messageID: string, + turn: { promoted?: boolean; send: () => Promise }, + ) => { + let resolve!: () => void + let reject!: (error: unknown) => void + const done = new Promise((ok, fail) => { + resolve = ok + reject = fail + }) + const active: Wait = { + messageID, + promoted: turn.promoted === true, + interrupted: false, + failureRendered: false, + resolve, + reject, + onVisibleOutput: next.onVisibleOutput, + } + state.wait = active + const interrupt = () => { + active.interrupted = true + void input.sdk.session.interrupt({ sessionID: input.sessionID }).catch(() => {}) + } + next.signal?.addEventListener("abort", interrupt, { once: true }) + try { + await turn.send() + await done + } catch (error) { + if (state.wait === active) state.wait = undefined + if (next.signal?.aborted) return + throw error + } finally { + next.signal?.removeEventListener("abort", interrupt) + } + } + + return { + async runPromptTurn(next) { + if (next.prompt.mode === "shell") { + await runShellTurn(next) + return + } + if (state.wait || state.shellWait) throw new Error("prompt already running") + if (!state.connected) throw new Error("Event stream is reconnecting") + const messageID = next.prompt.messageID + if (!messageID) throw new Error("Prompt message ID is required") + + const command = next.prompt.command + if (command?.source === "skill") { + input.trace?.write("send.skill", { sessionID: input.sessionID, messageID, skill: command.name }) + await runTurnWait(next, messageID, { + send: () => + input.sdk.session.skill( + { sessionID: input.sessionID, id: messageID, skill: command.name }, + { signal: next.signal }, + ), + }) + return + } + if (command) { + const selected = await resolveSelectedModel(input, next) + if (next.variant && !selected) throw new Error("Cannot select a variant before selecting a model") + // Agent and model ride the command payload; the server switches only + // when the command itself does not pin them. + const files = [ + ...(next.includeFiles ? next.files : []).map((file) => ({ uri: file.url, name: file.filename })), + ...promptFiles(next), + ] + const agents = promptAgents(next) + input.trace?.write("send.command", { sessionID: input.sessionID, messageID, command: command.name }) + await runTurnWait(next, messageID, { + send: () => + input.sdk.session.command( + { + sessionID: input.sessionID, + id: messageID, + command: command.name, + arguments: command.arguments, + agent: next.agent, + model: selected, + files: files.length ? files : undefined, + agents: agents.length ? agents : undefined, + delivery: "steer", + }, + { signal: next.signal }, + ), + }) + return + } + + if (next.agent) { + await input.sdk.session.switchAgent({ sessionID: input.sessionID, agent: next.agent }, { signal: next.signal }) + } + const selected = await resolveSelectedModel(input, next) + if (next.variant && !selected) throw new Error("Cannot select a variant before selecting a model") + if (selected) + await input.sdk.session.switchModel({ sessionID: input.sessionID, model: selected }, { signal: next.signal }) + + const prepared = await Promise.all((next.includeFiles ? next.files : []).map(prepareFile)) + const attachments = [ + ...prepared.flatMap((file) => (file.attachment ? [file.attachment] : [])), + ...promptFiles(next), + ] + const agents = promptAgents(next) + input.trace?.write("send.prompt", { sessionID: input.sessionID, messageID }) + await runTurnWait(next, messageID, { + send: () => + input.sdk.session.prompt( + { + sessionID: input.sessionID, + id: messageID, + text: [next.prompt.text, ...prepared.flatMap((file) => (file.text ? [file.text] : []))].join("\n\n"), + files: attachments.length ? attachments : undefined, + agents: agents.length ? agents : undefined, + delivery: "steer", + }, + { signal: next.signal }, + ), + }) + }, + async interruptActiveTurn() { + // A running shell holds no drain, so session.interrupt cannot reach it; + // abort the blocking request instead. The server-side command keeps its + // own lifecycle and simply loses its waiter. + const shell = state.shellWait + if (shell) { + shell.abort() + return + } + if (state.wait) state.wait.interrupted = true + await input.sdk.session.interrupt({ sessionID: input.sessionID }).catch(() => {}) + }, + selectSubagent(sessionID) { + subagents.select(sessionID) + }, + async replayOnResize(next) { + if (!input.replay || state.closed || input.footer.isClosed) return false + const buffered: RunV2Event[] = [] + state.buffered = buffered + try { + await input.footer.idle() + await next.reset() + state.messageIDs.clear() + state.text.clear() + state.projectedText.clear() + state.reasoning.clear() + state.projectedReasoning.clear() + state.tools.clear() + state.finishedTools.clear() + state.skillMessages.clear() + state.shellCommands.clear() + state.shellStarted.clear() + state.shellEnded.clear() + state.errors.clear() + await hydrate({ render: true, reuseVisibleWait: false }) + } finally { + state.buffered = undefined + } + for (const event of buffered) apply(event) + for (const row of next.localRows()) { + if (row.commit.messageID && state.messageIDs.has(row.commit.messageID)) continue + input.footer.append(row.commit) + } + return true + }, + async close() { + state.closed = true + offFooterClose() + controller.abort() + void connection.catch(() => {}) + }, + } +} diff --git a/packages/opencode/src/cli/cmd/run/stream.ts b/packages/cli/src/mini/stream.ts similarity index 91% rename from packages/opencode/src/cli/cmd/run/stream.ts rename to packages/cli/src/mini/stream.ts index 3efd864604..ce50a2ae21 100644 --- a/packages/opencode/src/cli/cmd/run/stream.ts +++ b/packages/cli/src/mini/stream.ts @@ -1,10 +1,10 @@ -// Thin bridge between reducer output and the footer API. +// Thin bridge between transport output and the footer API. // -// The reducers produce StreamCommit[] and an optional FooterOutput (patch + +// Transports produce StreamCommit[] and an optional FooterOutput (patch + // view + subagent state). This module forwards them to footer.append() and // footer.event() respectively, adding trace writes along the way. It also // defaults status updates to phase "running" if the caller didn't set a -// phase -- a convenience so reducer code doesn't have to repeat that. +// phase -- a convenience so transport code doesn't have to repeat that. import type { FooterApi, FooterOutput, FooterPatch, FooterSubagentState, StreamCommit } from "./types" type Trace = { @@ -103,9 +103,9 @@ export function traceSubagentState(state: FooterSubagentState) { permissions: state.permissions.map((item) => ({ id: item.id, sessionID: item.sessionID, - permission: item.permission, - patterns: item.patterns, - tool: item.tool, + action: item.action, + resources: item.resources, + source: item.source, metadata: item.metadata ? { keys: Object.keys(item.metadata), @@ -137,7 +137,7 @@ export function traceFooterOutput(footer?: FooterOutput) { } } -// Forwards reducer output to the footer: commits go to scrollback, patches update the status bar. +// Forwards transport output to the footer: commits go to scrollback, patches update the status bar. export function writeSessionOutput(input: OutputInput, out: StreamOutput): void { for (const commit of out.commits) { input.trace?.write("ui.commit", commit) diff --git a/packages/opencode/src/cli/cmd/run/theme.ts b/packages/cli/src/mini/theme.ts similarity index 94% rename from packages/opencode/src/cli/cmd/run/theme.ts rename to packages/cli/src/mini/theme.ts index e4fb315426..6fe36883d1 100644 --- a/packages/opencode/src/cli/cmd/run/theme.ts +++ b/packages/cli/src/mini/theme.ts @@ -47,7 +47,6 @@ export type RunBlockTheme = { text: ColorInput muted: ColorInput syntax?: SyntaxStyle - subtleSyntax?: SyntaxStyle diffAdded: ColorInput diffRemoved: ColorInput diffAddedBg: ColorInput @@ -172,42 +171,10 @@ function tint(base: RGBA, overlay: RGBA, value: number): RGBA { ) } -function blend(color: RGBA, bg: RGBA): RGBA { - if (color.a >= 1) { - return color - } - - return RGBA.fromValues( - bg.r + (color.r - bg.r) * color.a, - bg.g + (color.g - bg.g) * color.a, - bg.b + (color.b - bg.b) * color.a, - 1, - ) -} - function chroma(color: RGBA) { return Math.max(color.r, color.g, color.b) - Math.min(color.r, color.g, color.b) } -function opaqueSyntaxStyle(style: SyntaxStyle | undefined, bg: RGBA): SyntaxStyle | undefined { - if (!style) { - return undefined - } - - return SyntaxStyle.fromStyles( - Object.fromEntries( - [...style.getAllStyles()].map(([name, value]) => [ - name, - { - ...value, - fg: value.fg ? blend(value.fg, bg) : value.fg, - bg: value.bg ? blend(value.bg, bg) : value.bg, - }, - ]), - ), - ) -} - function indexedPalette(colors: TerminalColors, size: number = Math.max(colors.palette.length, 16)): RGBA[] { return Array.from({ length: size }, (_, index) => { const value = colors.palette[index] @@ -502,10 +469,7 @@ function map( scrollbackTheme: TuiThemeCurrent, splash: RunSplashTheme, syntax?: SyntaxStyle, - subtleSyntax?: SyntaxStyle, ): RunTheme { - const opaqueSubtleSyntax = opaqueSyntaxStyle(subtleSyntax, scrollbackTheme.background) - subtleSyntax?.destroy() const footerBackground = alpha(footerTheme.background, 1) const footerMode = mode(footerBackground) const shade = fade(footerTheme.backgroundMenu, footerTheme.background, 0.12, 0.56, 0.72) @@ -566,7 +530,6 @@ function map( text: scrollbackTheme.text, muted: scrollbackTheme.textMuted, syntax, - subtleSyntax: opaqueSubtleSyntax, diffAdded: scrollbackTheme.diffAdded, diffRemoved: scrollbackTheme.diffRemoved, diffAddedBg: transparent, @@ -677,13 +640,7 @@ export async function resolveRunTheme(renderer: CliRenderer): Promise _hasSelectedListItemText: true, } const syntax = shared.generateSyntax(syntaxTheme) - return map( - footerTheme, - scrollbackTheme, - splashTheme(scrollbackTheme, indexed), - syntax, - shared.generateSubtleSyntax(syntaxTheme), - ) + return map(footerTheme, scrollbackTheme, splashTheme(scrollbackTheme, indexed), syntax) } catch { return RUN_THEME_FALLBACK } diff --git a/packages/opencode/src/cli/cmd/run/tool.ts b/packages/cli/src/mini/tool.ts similarity index 75% rename from packages/opencode/src/cli/cmd/run/tool.ts rename to packages/cli/src/mini/tool.ts index 9a717ba4e6..fc30c565ec 100644 --- a/packages/opencode/src/cli/cmd/run/tool.ts +++ b/packages/cli/src/mini/tool.ts @@ -1,6 +1,6 @@ // Per-tool display rules shared across `opencode run` output paths. // -// Each known tool (bash, edit, write, task, etc.) has a ToolRule that controls +// Each known tool (shell, edit, write, task, etc.) has a ToolRule that controls // five display hooks: // // view → visibility policy for progress/final scrollback entries and @@ -15,27 +15,9 @@ import os from "os" import path from "path" import stripAnsi from "strip-ansi" -import type { ToolPart } from "@opencode-ai/sdk/v2" -import type * as Tool from "@/tool/tool" -import type { ApplyPatchTool } from "@/tool/apply_patch" -import type { ShellTool as BashTool } from "@/tool/shell" -import type { EditTool } from "@/tool/edit" -import type { GlobTool } from "@/tool/glob" -import type { GrepTool } from "@/tool/grep" -import type { InvalidTool } from "@/tool/invalid" -import type { LspTool } from "@/tool/lsp" -import type { PlanExitTool } from "@/tool/plan" -import type { QuestionTool } from "@/tool/question" -import type { ReadTool } from "@/tool/read" -import type { SkillTool } from "@/tool/skill" -import type { TaskTool } from "@/tool/task" -import type { TodoWriteTool } from "@/tool/todo" -import type { WebFetchTool } from "@/tool/webfetch" -import { webSearchProviderLabel, type WebSearchTool } from "@/tool/websearch" -import type { WriteTool } from "@/tool/write" -import { LANGUAGE_EXTENSIONS } from "@/lsp/language" -import * as Locale from "@/util/locale" -import type { RunEntryBody, StreamCommit, ToolSnapshot } from "./types" +import { LANGUAGE_EXTENSIONS } from "@opencode-ai/tui/util/filetype" +import { Locale } from "@opencode-ai/tui/util/locale" +import type { MiniToolPart, RunEntryBody, StreamCommit, ToolSnapshot } from "./types" export type ToolView = { output: boolean @@ -47,6 +29,45 @@ export type ToolPhase = "start" | "progress" | "final" export type ToolDict = Record +type PatchFile = { + type?: string + relativePath?: string + filePath?: string + movePath?: string + patch?: string + deletions?: number +} + +type ToolInput = ToolDict & { + path?: string + pattern?: string + filePath?: string + filepath?: string + url?: string + query?: string + subagent_type?: string + description?: string + name?: string + operation?: string + line?: number + character?: number + content?: string + command?: string + workdir?: string + questions?: Array<{ question?: string }> + diff?: string +} + +type ToolMetadata = ToolDict & { + count?: number + matches?: number + diff?: string + provider?: unknown + files?: PatchFile[] + answers?: string[][] + exit?: number +} + export type ToolFrame = { raw: string name: string @@ -73,15 +94,15 @@ export type ToolPermissionInfo = { file?: string } -export type ToolProps = { - input: Partial> - metadata: Partial> +export type ToolProps = { + input: ToolInput + metadata: ToolMetadata frame: ToolFrame } -type ToolPermissionProps = { - input: Partial> - metadata: Partial> +type ToolPermissionProps = { + input: ToolInput + metadata: ToolMetadata patterns: string[] } @@ -91,40 +112,34 @@ type ToolPermissionCtx = { patterns: string[] } -type ToolDefs = { - invalid: typeof InvalidTool - bash: typeof BashTool - write: typeof WriteTool - edit: typeof EditTool - apply_patch: typeof ApplyPatchTool - batch: Tool.Info - task: typeof TaskTool - todowrite: typeof TodoWriteTool - question: typeof QuestionTool - read: typeof ReadTool - glob: typeof GlobTool - grep: typeof GrepTool - list: Tool.Info - lsp: typeof LspTool - webfetch: typeof WebFetchTool - websearch: typeof WebSearchTool - skill: typeof SkillTool - plan_exit: typeof PlanExitTool -} +type ToolName = + | "invalid" + | "shell" + | "write" + | "edit" + | "patch" + | "batch" + | "task" + | "question" + | "read" + | "glob" + | "grep" + | "list" + | "lsp" + | "webfetch" + | "websearch" + | "skill" + | "plan_exit" -type ToolName = keyof ToolDefs - -type ToolRule = { +type ToolRule = { view: ToolView - run: (props: ToolProps) => ToolInline - scroll?: Partial) => string>> - permission?: (props: ToolPermissionProps) => ToolPermissionInfo - snap?: (props: ToolProps) => ToolSnapshot | undefined + run: (props: ToolProps) => ToolInline + scroll?: Partial string>> + permission?: (props: ToolPermissionProps) => ToolPermissionInfo + snap?: (props: ToolProps) => ToolSnapshot | undefined } -type ToolRegistry = { - [K in ToolName]: ToolRule -} +type ToolRegistry = Record type AnyToolRule = ToolRule @@ -136,22 +151,28 @@ function dict(v: unknown): ToolDict { return { ...v } } -function props(frame: ToolFrame): ToolProps { +function props(frame: ToolFrame): ToolProps { return { - input: Object.assign(Object.create(null), frame.input), - metadata: Object.assign(Object.create(null), frame.meta), + input: frame.input, + metadata: frame.meta, frame, } } -function permission(ctx: ToolPermissionCtx): ToolPermissionProps { +function permission(ctx: ToolPermissionCtx): ToolPermissionProps { return { - input: Object.assign(Object.create(null), ctx.input), - metadata: Object.assign(Object.create(null), ctx.meta), + input: ctx.input, + metadata: ctx.meta, patterns: ctx.patterns, } } +function webSearchProviderLabel(provider: unknown) { + if (provider === "parallel") return "Parallel Web Search" + if (provider === "exa") return "Exa Web Search" + return "Web Search" +} + function text(v: unknown): string { return typeof v === "string" ? v : "" } @@ -285,7 +306,7 @@ function count(n: number, label: string): string { return `${n} ${label}${n === 1 ? "" : "es"}` } -function runGlob(p: ToolProps): ToolInline { +function runGlob(p: ToolProps): ToolInline { const root = p.input.path ?? "" const title = `Glob "${p.input.pattern ?? ""}"` const suffix = root ? `in ${toolPath(root)}` : "" @@ -298,7 +319,7 @@ function runGlob(p: ToolProps): ToolInline { } } -function runGrep(p: ToolProps): ToolInline { +function runGrep(p: ToolProps): ToolInline { const root = p.input.path ?? "" const title = `Grep "${p.input.pattern ?? ""}"` const suffix = root ? `in ${toolPath(root)}` : "" @@ -319,7 +340,7 @@ function runList(p: ToolProps): ToolInline { } } -function runRead(p: ToolProps): ToolInline { +function runRead(p: ToolProps): ToolInline { const file = toolPath(p.input.filePath) const description = info(p.frame.input, ["filePath"]) || undefined return { @@ -329,7 +350,7 @@ function runRead(p: ToolProps): ToolInline { } } -function runWrite(p: ToolProps): ToolInline { +function runWrite(p: ToolProps): ToolInline { return { icon: "←", title: `Write ${toolPath(p.input.filePath)}`, @@ -338,7 +359,7 @@ function runWrite(p: ToolProps): ToolInline { } } -function runWebfetch(p: ToolProps): ToolInline { +function runWebfetch(p: ToolProps): ToolInline { const url = p.input.url ?? "" return { icon: "%", @@ -346,7 +367,7 @@ function runWebfetch(p: ToolProps): ToolInline { } } -function runEdit(p: ToolProps): ToolInline { +function runEdit(p: ToolProps): ToolInline { return { icon: "←", title: `Edit ${toolPath(p.input.filePath)}`, @@ -355,7 +376,7 @@ function runEdit(p: ToolProps): ToolInline { } } -function runWebSearch(p: ToolProps): ToolInline { +function runWebSearch(p: ToolProps): ToolInline { const title = webSearchProviderLabel(p.metadata.provider) return { icon: "◈", @@ -363,7 +384,7 @@ function runWebSearch(p: ToolProps): ToolInline { } } -function runTask(p: ToolProps): ToolInline { +function runTask(p: ToolProps): ToolInline { const kind = Locale.titlecase(p.input.subagent_type || "unknown") const desc = p.input.description const icon = p.frame.status === "error" ? "✗" : p.frame.status === "running" ? "•" : "✓" @@ -374,33 +395,14 @@ function runTask(p: ToolProps): ToolInline { } } -function runTodo(p: ToolProps): ToolInline { - return { - icon: "#", - title: "Todos", - mode: "block", - body: list<{ status?: string; content?: string }>(p.frame.input.todos) - .flatMap((item) => { - const body = typeof item?.content === "string" ? item.content : "" - if (!body) { - return [] - } - - const mark = item.status === "completed" ? "[✓]" : item.status === "in_progress" ? "[•]" : "[ ]" - return [`${mark} ${body}`] - }) - .join("\n"), - } -} - -function runSkill(p: ToolProps): ToolInline { +function runSkill(p: ToolProps): ToolInline { return { icon: "→", title: `Skill "${p.input.name ?? ""}"`, } } -function runPatch(p: ToolProps): ToolInline { +function runPatch(p: ToolProps): ToolInline { const files = p.metadata.files?.length ?? 0 if (files === 0) { return { @@ -415,7 +417,7 @@ function runPatch(p: ToolProps): ToolInline { } } -function runQuestion(p: ToolProps): ToolInline { +function runQuestion(p: ToolProps): ToolInline { const total = list(p.frame.input.questions).length return { icon: "→", @@ -423,7 +425,7 @@ function runQuestion(p: ToolProps): ToolInline { } } -function runInvalid(p: ToolProps): ToolInline { +function runInvalid(p: ToolProps): ToolInline { return { icon: "✗", title: text(p.frame.state.title) || "Invalid Tool", @@ -463,14 +465,14 @@ function lspTitle( return `LSP ${op} ${file}${pos}` } -function runLsp(p: ToolProps): ToolInline { +function runLsp(p: ToolProps): ToolInline { return { icon: "→", title: text(p.frame.state.title) || lspTitle(p.input), } } -function runPlanExit(p: ToolProps): ToolInline { +function runPlanExit(p: ToolProps): ToolInline { return { icon: "→", title: text(p.frame.state.title) || "Switching to build agent", @@ -479,8 +481,6 @@ function runPlanExit(p: ToolProps): ToolInline { } } -type PatchFile = Tool.InferMetadata["files"][number] - function patchTitle(file: PatchFile): string { const rel = file.relativePath const from = file.filePath @@ -497,7 +497,7 @@ function patchTitle(file: PatchFile): string { return `# Patched ${rel || toolPath(from)}` } -function snapWrite(p: ToolProps): ToolSnapshot | undefined { +function snapWrite(p: ToolProps): ToolSnapshot | undefined { const file = p.input.filePath || "" const content = p.input.content || "" if (!file && !content) { @@ -512,7 +512,7 @@ function snapWrite(p: ToolProps): ToolSnapshot | undefined { } } -function snapEdit(p: ToolProps): ToolSnapshot | undefined { +function snapEdit(p: ToolProps): ToolSnapshot | undefined { const file = p.input.filePath || "" const diff = p.metadata.diff || "" if (!file || !diff.trim()) { @@ -531,7 +531,7 @@ function snapEdit(p: ToolProps): ToolSnapshot | undefined { } } -function snapPatch(p: ToolProps): ToolSnapshot | undefined { +function snapPatch(p: ToolProps): ToolSnapshot | undefined { const files = list(p.frame.meta.files) if (files.length === 0) { return undefined @@ -568,7 +568,7 @@ function snapPatch(p: ToolProps): ToolSnapshot | undefine } } -function snapTask(p: ToolProps): ToolSnapshot { +function snapTask(p: ToolProps): ToolSnapshot { const kind = Locale.titlecase(p.input.subagent_type || "general") const desc = p.input.description const title = text(p.frame.state.title) @@ -582,29 +582,7 @@ function snapTask(p: ToolProps): ToolSnapshot { } } -function snapTodo(p: ToolProps): ToolSnapshot { - const items = list<{ status?: string; content?: string }>(p.frame.input.todos).flatMap((item) => { - const content = typeof item?.content === "string" ? item.content : "" - if (!content) { - return [] - } - - return [ - { - status: typeof item.status === "string" ? item.status : "", - content, - }, - ] - }) - - return { - kind: "todo", - items, - tail: "", - } -} - -function snapQuestion(p: ToolProps): ToolSnapshot { +function snapQuestion(p: ToolProps): ToolSnapshot { const answers = list(p.frame.meta.answers) const items = list<{ question?: string }>(p.frame.input.questions).map((item, i) => { const answer = list(answers[i]).filter((entry) => typeof entry === "string") @@ -621,7 +599,7 @@ function snapQuestion(p: ToolProps): ToolSnapshot { } } -function scrollBashStart(p: ToolProps): string { +function scrollBashStart(p: ToolProps): string { const cmd = p.input.command ?? "" const wd = p.input.workdir ?? "" const formatted = wd && wd !== "." ? toolPath(wd) : "" @@ -637,7 +615,7 @@ function scrollBashStart(p: ToolProps): string { return `# Running in ${dir}\n$ ${cmd}` } -function scrollBashProgress(p: ToolProps): string { +function scrollBashProgress(p: ToolProps): string { const out = stripAnsi(p.frame.raw) const cmd = (p.input.command ?? "").trim() const fmt = (text: string) => { @@ -670,36 +648,40 @@ function scrollBashProgress(p: ToolProps): string { return fmt(out) } -function scrollBashFinal(p: ToolProps): string { +function scrollShellFinal(p: ToolProps): string { + if (p.frame.status === "error") { + return fail(p.frame) + } + const code = p.metadata.exit ?? num(p.frame.meta.exitCode) ?? num(p.frame.meta.exit_code) const time = span(p.frame.state) if (code === undefined) { if (!time) { - return "bash completed" + return "shell completed" } - return `bash completed · ${time}` + return `shell completed · ${time}` } - return `bash completed (exit ${code})${time ? ` · ${time}` : ""}` + return `shell completed (exit ${code})${time ? ` · ${time}` : ""}` } -function scrollReadStart(p: ToolProps): string { +function scrollReadStart(p: ToolProps): string { const file = toolPath(p.input.filePath) const extra = info(p.frame.input, ["filePath"]) const tail = extra ? ` ${extra}` : "" return `→ Read ${file}${tail}`.trim() } -function scrollWriteStart(_: ToolProps): string { +function scrollWriteStart(_: ToolProps): string { return "" } -function scrollEditStart(_: ToolProps): string { +function scrollEditStart(_: ToolProps): string { return "" } -function scrollPatchStart(_: ToolProps): string { +function scrollPatchStart(_: ToolProps): string { return "" } @@ -723,7 +705,7 @@ function patchLine(file: PatchFile): string { return `~ Patched ${rel || toolPath(from)}` } -function scrollPatchFinal(p: ToolProps): string { +function scrollPatchFinal(p: ToolProps): string { if (p.frame.status === "error") { return fail(p.frame) } @@ -752,7 +734,7 @@ function scrollPatchFinal(p: ToolProps): string { return patchLine(files[0]!) } -function scrollTaskStart(_: ToolProps): string { +function scrollTaskStart(_: ToolProps): string { return "" } @@ -774,7 +756,7 @@ function taskResult(output: string): string | undefined { return next || undefined } -function scrollTaskFinal(p: ToolProps): string { +function scrollTaskFinal(p: ToolProps): string { if (p.frame.status === "error") { return fail(p.frame) } @@ -788,47 +770,11 @@ function scrollTaskFinal(p: ToolProps): string { return `# ${kind} Task\n${row}` } -function scrollTodoStart(_: ToolProps): string { +function scrollQuestionStart(_: ToolProps): string { return "" } -function scrollTodoFinal(p: ToolProps): string { - const items = list<{ status?: string }>(p.input.todos) - const time = span(p.frame.state) - if (items.length === 0) { - if (!time) { - return "0 todos" - } - - return `0 todos · ${time}` - } - - const doneN = items.filter((item) => item.status === "completed").length - const runN = items.filter((item) => item.status === "in_progress").length - const left = items.length - doneN - runN - const tail = [`${items.length} total`] - if (doneN > 0) { - tail.push(`${doneN} done`) - } - if (runN > 0) { - tail.push(`${runN} active`) - } - if (left > 0) { - tail.push(`${left} pending`) - } - - if (time) { - tail.push(time) - } - - return tail.join(" · ") -} - -function scrollQuestionStart(_: ToolProps): string { - return "" -} - -function scrollQuestionFinal(p: ToolProps): string { +function scrollQuestionFinal(p: ToolProps): string { const q = p.input.questions ?? [] const a = p.metadata.answers ?? [] const time = span(p.frame.state) @@ -855,15 +801,15 @@ function scrollQuestionFinal(p: ToolProps): string { return rows.join("\n") } -function scrollLspStart(p: ToolProps): string { +function scrollLspStart(p: ToolProps): string { return `→ ${lspTitle(p.input)}` } -function scrollSkillStart(p: ToolProps): string { +function scrollSkillStart(p: ToolProps): string { return `→ Skill "${p.input.name ?? ""}"` } -function scrollGlobStart(p: ToolProps): string { +function scrollGlobStart(p: ToolProps): string { const pattern = p.input.pattern ?? "" const head = pattern ? `✱ Glob "${pattern}"` : "✱ Glob" const dir = p.input.path ?? "" @@ -874,11 +820,11 @@ function scrollGlobStart(p: ToolProps): string { return `${head} in ${toolPath(dir)}` } -function scrollGlobFinal(p: ToolProps): string { +function scrollGlobFinal(p: ToolProps): string { return toolError(p.frame) || fail(p.frame) } -function scrollGrepStart(p: ToolProps): string { +function scrollGrepStart(p: ToolProps): string { const pattern = p.input.pattern ?? "" const head = pattern ? `✱ Grep "${pattern}"` : "✱ Grep" const dir = p.input.path ?? "" @@ -898,7 +844,7 @@ function scrollListStart(p: ToolProps): string { return `→ List ${toolPath(dir)}` } -function scrollWebfetchStart(p: ToolProps): string { +function scrollWebfetchStart(p: ToolProps): string { const url = p.input.url ?? "" if (!url) { return "% WebFetch" @@ -907,7 +853,7 @@ function scrollWebfetchStart(p: ToolProps): string { return `% WebFetch ${url}` } -function scrollWebSearchStart(p: ToolProps): string { +function scrollWebSearchStart(p: ToolProps): string { const title = webSearchProviderLabel(p.metadata.provider) const query = p.input.query ?? "" if (!query) { @@ -917,7 +863,7 @@ function scrollWebSearchStart(p: ToolProps): string { return `◈ ${title} "${query}"` } -function permEdit(p: ToolPermissionProps): ToolPermissionInfo { +function permEdit(p: ToolPermissionProps): ToolPermissionInfo { const input = p.input as { filePath?: string; filepath?: string; diff?: string } const file = input.filePath || input.filepath || p.patterns[0] || "" return { @@ -929,7 +875,7 @@ function permEdit(p: ToolPermissionProps): ToolPermissionInfo { } } -function permRead(p: ToolPermissionProps): ToolPermissionInfo { +function permRead(p: ToolPermissionProps): ToolPermissionInfo { const file = p.input.filePath || p.patterns[0] || "" return { icon: "→", @@ -938,7 +884,7 @@ function permRead(p: ToolPermissionProps): ToolPermissionInfo { } } -function permGlob(p: ToolPermissionProps): ToolPermissionInfo { +function permGlob(p: ToolPermissionProps): ToolPermissionInfo { const pattern = p.input.pattern || p.patterns[0] || "" return { icon: "✱", @@ -947,7 +893,7 @@ function permGlob(p: ToolPermissionProps): ToolPermissionInfo { } } -function permGrep(p: ToolPermissionProps): ToolPermissionInfo { +function permGrep(p: ToolPermissionProps): ToolPermissionInfo { const pattern = p.input.pattern || p.patterns[0] || "" return { icon: "✱", @@ -965,7 +911,7 @@ function permList(p: ToolPermissionProps): ToolPermissionInfo { } } -function permBash(p: ToolPermissionProps): ToolPermissionInfo { +function permBash(p: ToolPermissionProps): ToolPermissionInfo { const cmd = p.input.command || "" return { icon: "#", @@ -974,7 +920,7 @@ function permBash(p: ToolPermissionProps): ToolPermissionInfo { } } -function permTask(p: ToolPermissionProps): ToolPermissionInfo { +function permTask(p: ToolPermissionProps): ToolPermissionInfo { const type = p.input.subagent_type || "general" const desc = p.input.description return { @@ -984,7 +930,7 @@ function permTask(p: ToolPermissionProps): ToolPermissionInfo { } } -function permWebfetch(p: ToolPermissionProps): ToolPermissionInfo { +function permWebfetch(p: ToolPermissionProps): ToolPermissionInfo { const url = p.input.url || "" return { icon: "%", @@ -993,7 +939,7 @@ function permWebfetch(p: ToolPermissionProps): ToolPermissi } } -function permWebSearch(p: ToolPermissionProps): ToolPermissionInfo { +function permWebSearch(p: ToolPermissionProps): ToolPermissionInfo { const query = p.input.query || "" const title = webSearchProviderLabel(p.metadata.provider) return { @@ -1003,7 +949,7 @@ function permWebSearch(p: ToolPermissionProps): ToolPermis } } -function permLsp(p: ToolPermissionProps): ToolPermissionInfo { +function permLsp(p: ToolPermissionProps): ToolPermissionInfo { const file = p.input.filePath || "" const line = typeof p.input.line === "number" ? p.input.line : undefined const char = typeof p.input.character === "number" ? p.input.character : undefined @@ -1030,16 +976,16 @@ const TOOL_RULES = { start: () => "", }, }, - bash: { + shell: { view: { output: true, final: false, }, - run: runBash, + run: runShell, scroll: { start: scrollBashStart, progress: scrollBashProgress, - final: scrollBashFinal, + final: scrollShellFinal, }, permission: permBash, }, @@ -1068,7 +1014,7 @@ const TOOL_RULES = { }, permission: permEdit, }, - apply_patch: { + patch: { view: { output: false, final: true, @@ -1105,19 +1051,6 @@ const TOOL_RULES = { }, permission: permTask, }, - todowrite: { - view: { - output: false, - final: true, - snap: "structured", - }, - run: runTodo, - snap: snapTodo, - scroll: { - start: scrollTodoStart, - final: scrollTodoFinal, - }, - }, question: { view: { output: false, @@ -1243,7 +1176,7 @@ function rule(name?: string): AnyToolRule | undefined { return TOOL_RULES[name] } -function frame(part: ToolPart): ToolFrame { +function frame(part: MiniToolPart): ToolFrame { const state = dict(part.state) return { raw: "", @@ -1269,7 +1202,7 @@ export function toolFrame(commit: StreamCommit, raw: string): ToolFrame { } } -function runBash(p: ToolProps): ToolInline { +function runShell(p: ToolProps): ToolInline { return { icon: "$", title: p.input.command || "", @@ -1297,7 +1230,7 @@ export function toolStructuredFinal(commit: StreamCommit): boolean { ) } -export function toolInlineInfo(part: ToolPart): ToolInline { +export function toolInlineInfo(part: MiniToolPart): ToolInline { const ctx = frame(part) const draw = rule(ctx.name)?.run try { @@ -1427,6 +1360,11 @@ export function toolEntryBody(commit: StreamCommit, raw: string): RunEntryBody | return textBody(shellOutput(commit.shell.command, raw) ?? "") } + if (commit.toolState === "error") { + const ctx = toolFrame(commit, raw) + return textBody(toolScroll("final", ctx)) + } + return undefined } diff --git a/packages/opencode/src/cli/cmd/run/trace.ts b/packages/cli/src/mini/trace.ts similarity index 100% rename from packages/opencode/src/cli/cmd/run/trace.ts rename to packages/cli/src/mini/trace.ts diff --git a/packages/cli/src/mini/turn-summary.ts b/packages/cli/src/mini/turn-summary.ts new file mode 100644 index 0000000000..aa63a8ea0a --- /dev/null +++ b/packages/cli/src/mini/turn-summary.ts @@ -0,0 +1,21 @@ +import type { StreamCommit } from "./types" + +export function turnSummaryCommit(input: { + agent: string + model: string + duration: string + messageID?: string +}): StreamCommit { + return { + kind: "system", + text: `${input.agent} · ${input.model} · ${input.duration}`, + phase: "final", + source: "system", + summary: { + agent: input.agent, + model: input.model, + duration: input.duration, + }, + messageID: input.messageID, + } +} diff --git a/packages/opencode/src/cli/cmd/run/types.ts b/packages/cli/src/mini/types.ts similarity index 64% rename from packages/opencode/src/cli/cmd/run/types.ts rename to packages/cli/src/mini/types.ts index a914922e48..c34a25bfa6 100644 --- a/packages/opencode/src/cli/cmd/run/types.ts +++ b/packages/cli/src/mini/types.ts @@ -1,4 +1,4 @@ -// Shared type vocabulary for the direct interactive mode (`opencode --mini`). +// Shared type vocabulary for the direct interactive mode (`opencode mini`). // // Direct mode uses a split-footer terminal layout: immutable scrollback for the // session transcript, and a mutable footer for prompt input, status, and @@ -7,12 +7,17 @@ // // Data flow through the system: // -// SDK events → session-data reducer → StreamCommit[] + FooterOutput +// V2 events / demo actions → StreamCommit[] + FooterOutput // → stream.ts bridges to footer API // → footer.ts queues commits and patches the footer view // → OpenTUI split-footer renderer writes to terminal -import type { OpencodeClient, PermissionRequest, QuestionRequest, ToolPart } from "@opencode-ai/sdk/v2" -import type { TuiConfig } from "@opencode-ai/tui/config" +import type { + OpenCodeClient, + PermissionV2Request, + QuestionV2Request, + ReferenceListOutput, +} from "@opencode-ai/client/promise" +import type { TuiConfig } from "@opencode-ai/tui/config/v1" export type RunFilePart = { type: "file" @@ -21,14 +26,79 @@ export type RunFilePart = { mime: string } -type PromptModel = Parameters[0]["model"] -type PromptInput = Parameters[0] +type PromptModel = { providerID: string; modelID: string } -export type RunPromptPart = NonNullable[number] +export type RunPromptPart = + | { + type: "file" + url: string + filename?: string + mime?: string + source?: { + type: string + text: { start: number; end: number; value: string } + [key: string]: unknown + } + } + | { type: "agent"; name: string; source?: { start: number; end: number; value: string } } -export type RunCommand = NonNullable>["data"]>[number] +export type RunCommand = { + name: string + description?: string + source?: string + template?: string + hints?: unknown[] + agent?: string + model?: { + [key: string]: unknown + } + subtask?: boolean +} -export type RunProvider = NonNullable>["data"]>["all"][number] +export type RunProviderModel = { + id: string + providerID: string + api?: { + [key: string]: unknown + } + name?: string + capabilities?: { + [key: string]: unknown + } + cost?: { + input: number + output?: number + cache?: { + read: number + write: number + } + } + limit?: { + context: number + input?: number + output?: number + } + status?: string + options?: { + [key: string]: unknown + } + headers?: { + [key: string]: string + } + release_date?: string + variants?: Record +} + +export type RunProvider = { + id: string + name: string + source?: string + env?: string[] + options?: { + [key: string]: unknown + } + models: Record +} export type RunPrompt = { messageID?: string @@ -39,6 +109,8 @@ export type RunPrompt = { command?: { name: string arguments: string + // Catalog source of the matched slash entry ("skill" routes to session.skill). + source?: string } } @@ -48,14 +120,18 @@ export type FooterQueuedPrompt = { prompt: RunPrompt } -export type RunAgent = NonNullable>["data"]>[number] +export type RunAgent = { + id: string + name: string + description?: string + mode: "subagent" | "primary" | "all" + hidden: boolean +} -type RunResourceMap = NonNullable>["data"]> - -export type RunResource = RunResourceMap[string] +export type RunReference = ReferenceListOutput["data"][number] export type RunInput = { - sdk: OpencodeClient + sdk: OpenCodeClient directory: string sessionID: string sessionTitle?: string @@ -68,7 +144,6 @@ export type RunInput = { files: RunFilePart[] initialInput?: string thinking: boolean - backgroundSubagents: boolean demo?: boolean } @@ -132,15 +207,6 @@ export type ToolTaskSnapshot = { tail: string } -export type ToolTodoSnapshot = { - kind: "todo" - items: Array<{ - status: string - content: string - }> - tail: string -} - export type ToolQuestionSnapshot = { kind: "question" items: Array<{ @@ -150,12 +216,42 @@ export type ToolQuestionSnapshot = { tail: string } -export type ToolSnapshot = - | ToolCodeSnapshot - | ToolDiffSnapshot - | ToolTaskSnapshot - | ToolTodoSnapshot - | ToolQuestionSnapshot +export type ToolSnapshot = ToolCodeSnapshot | ToolDiffSnapshot | ToolTaskSnapshot | ToolQuestionSnapshot + +export type MiniToolState = + | { status: "pending"; input: Record; raw?: string } + | { + status: "running" + input: Record + title?: string + metadata?: Record + time: { start: number } + } + | { + status: "completed" + input: Record + output: string + title?: string + metadata?: Record + time: { start: number; end: number } + } + | { + status: "error" + input: Record + error: string + metadata?: Record + time: { start: number; end: number } + } + +export type MiniToolPart = { + id: string + sessionID: string + messageID: string + type?: "tool" + callID: string + tool: string + state: MiniToolState +} export type EntryLayout = "inline" | "block" @@ -167,13 +263,13 @@ export type RunEntryBody = | { type: "structured"; snapshot: ToolSnapshot } // Which interactive surface the footer is showing. Only one view is active at -// a time. The reducer drives transitions: when a permission arrives the view +// a time. The transport drives transitions: when a permission arrives the view // switches to "permission", and when the permission resolves it falls back to // "prompt". export type FooterView = | { type: "prompt" } - | { type: "permission"; request: PermissionRequest } - | { type: "question"; request: QuestionRequest } + | { type: "permission"; request: PermissionV2Request } + | { type: "question"; request: QuestionV2Request } export type FooterPromptRoute = | { type: "composer" } @@ -206,11 +302,11 @@ export type FooterSubagentDetail = { export type FooterSubagentState = { tabs: FooterSubagentTab[] details: Record - permissions: PermissionRequest[] - questions: QuestionRequest[] + permissions: PermissionV2Request[] + questions: QuestionV2Request[] } -// The reducer emits this alongside scrollback commits so the footer can update in the same frame. +// The transport emits this alongside scrollback commits so the footer can update in the same frame. export type FooterOutput = { patch?: FooterPatch view?: FooterView @@ -221,10 +317,14 @@ export type FooterOutput = { // transport both emit these to update footer state without reaching into // internal signals directly. export type FooterEvent = + | { + type: "history" + history: RunPrompt[] + } | { type: "catalog" agents: RunAgent[] - resources: RunResource[] + references: RunReference[] commands?: RunCommand[] } | { @@ -251,6 +351,7 @@ export type FooterEvent = | { type: "model" model: string + selection: NonNullable } | { type: "turn.send" @@ -280,11 +381,14 @@ export type FooterEvent = state: FooterSubagentState } -export type PermissionReply = Parameters[0] +export type PermissionReply = Omit[0], "sessionID"> -export type QuestionReply = Parameters[0] +export type QuestionReply = { + requestID: string + answers: string[][] +} -export type QuestionReject = Parameters[0] +export type QuestionReject = Omit[0], "sessionID"> export type RunTuiConfig = Pick @@ -296,8 +400,8 @@ export type StreamSource = "assistant" | "reasoning" | "tool" | "system" export type StreamToolState = "running" | "completed" | "error" -// A single append-only commit to scrollback. The session-data reducer produces -// these from SDK events, and RunFooter.append() queues them for the next +// A single append-only commit to scrollback. The transport produces these from +// V2 events, and RunFooter.append() queues them for the next // microtask flush. Once flushed, they become immutable terminal scrollback // rows -- they cannot be rewritten. export type StreamCommit = { @@ -309,7 +413,7 @@ export type StreamCommit = { messageID?: string partID?: string tool?: string - part?: ToolPart + part?: MiniToolPart interrupted?: boolean toolState?: StreamToolState toolError?: string diff --git a/packages/cli/src/mini/ui.ts b/packages/cli/src/mini/ui.ts new file mode 100644 index 0000000000..1caf2674fc --- /dev/null +++ b/packages/cli/src/mini/ui.ts @@ -0,0 +1,27 @@ +import { EOL } from "node:os" + +export const Style = { + TEXT_DIM: "\x1b[90m", + TEXT_NORMAL: "\x1b[0m", + TEXT_WARNING_BOLD: "\x1b[93m\x1b[1m", + TEXT_DANGER_BOLD: "\x1b[91m\x1b[1m", +} + +export function println(...message: string[]) { + process.stderr.write(message.join(" ") + EOL) +} + +let blank = false + +export function empty() { + if (blank) return + println(Style.TEXT_NORMAL) + blank = true +} + +export function error(message: string) { + if (message.startsWith("Error: ")) message = message.slice("Error: ".length) + println(Style.TEXT_DANGER_BOLD + "Error: " + Style.TEXT_NORMAL + message) +} + +export * as UI from "./ui" diff --git a/packages/opencode/src/cli/cmd/run/variant.shared.ts b/packages/cli/src/mini/variant.shared.ts similarity index 70% rename from packages/opencode/src/cli/cmd/run/variant.shared.ts rename to packages/cli/src/mini/variant.shared.ts index e685ceb028..50adb1c979 100644 --- a/packages/opencode/src/cli/cmd/run/variant.shared.ts +++ b/packages/cli/src/mini/variant.shared.ts @@ -8,11 +8,11 @@ // variant and the persisted file. import path from "path" import { FSUtil } from "@opencode-ai/core/fs-util" -import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Context, Effect, Layer } from "effect" -import { makeRuntime } from "@/effect/run-service" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { makeGlobalNode } from "@opencode-ai/core/effect/app-node" +import { makeRuntime } from "@opencode-ai/core/effect/runtime" import { Global } from "@opencode-ai/core/global" -import { isRecord } from "@/util/record" import { createSession, sessionVariant, type RunSession, type SessionMessages } from "./session.shared" import type { RunInput, RunProvider } from "./types" @@ -30,6 +30,10 @@ type VariantRuntime = { saveVariant(model: RunInput["model"], variant: string | undefined): Promise } +function isRecord(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value) +} + class Service extends Context.Service()("@opencode/RunVariant") {} function modelKey(provider: string, model: string): string { @@ -136,69 +140,69 @@ function state(value: unknown): ModelState { } } -function createLayer(fs = AppNodeBuilder.build(FSUtil.node)) { - return Layer.fresh( - Layer.effect( - Service, - Effect.gen(function* () { - const file = yield* FSUtil.Service +const layer = Layer.fresh( + Layer.effect( + Service, + Effect.gen(function* () { + const file = yield* FSUtil.Service - const read = Effect.fn("RunVariant.read")(function* () { - return yield* file.readJson(MODEL_FILE).pipe( - Effect.map(state), - Effect.catchCause(() => Effect.succeed(state(undefined))), - ) - }) + const read = Effect.fn("RunVariant.read")(function* () { + return yield* file.readJson(MODEL_FILE).pipe( + Effect.map(state), + Effect.catchCause(() => Effect.succeed(state(undefined))), + ) + }) - const resolveSavedVariant = Effect.fn("RunVariant.resolveSavedVariant")(function* (model: RunInput["model"]) { - if (!model) { - return undefined - } + const resolveSavedVariant = Effect.fn("RunVariant.resolveSavedVariant")(function* (model: RunInput["model"]) { + if (!model) { + return undefined + } - return (yield* read()).variant?.[variantKey(model)] - }) + return (yield* read()).variant?.[variantKey(model)] + }) - const saveVariant = Effect.fn("RunVariant.saveVariant")(function* ( - model: RunInput["model"], - variant: string | undefined, - ) { - if (!model) { - return - } + const saveVariant = Effect.fn("RunVariant.saveVariant")(function* ( + model: RunInput["model"], + variant: string | undefined, + ) { + if (!model) { + return + } - const current = yield* read() - const next = { - ...current.variant, - } - const key = variantKey(model) - if (variant) { - next[key] = variant - } + const current = yield* read() + const next = { + ...current.variant, + } + const key = variantKey(model) + if (variant) { + next[key] = variant + } - if (!variant) { - delete next[key] - } + if (!variant) { + delete next[key] + } - yield* file - .writeJson(MODEL_FILE, { - ...current, - variant: next, - }) - .pipe(Effect.orElseSucceed(() => undefined)) - }) + yield* file + .writeJson(MODEL_FILE, { + ...current, + variant: next, + }) + .pipe(Effect.orElseSucceed(() => undefined)) + }) - return Service.of({ - resolveSavedVariant, - saveVariant, - }) - }), - ).pipe(Layer.provide(fs)), - ) -} + return Service.of({ + resolveSavedVariant, + saveVariant, + }) + }), + ), +) + +const node = makeGlobalNode({ service: Service, layer, deps: [FSUtil.node] }) /** @internal Exported for testing. */ -export function createVariantRuntime(fs = AppNodeBuilder.build(FSUtil.node)): VariantRuntime { - const runtime = makeRuntime(Service, createLayer(fs)) +export function createVariantRuntime(replacements?: readonly LayerNode.Replacement[]): VariantRuntime { + const runtime = makeRuntime(Service, LayerNode.compile(node, replacements)) return { resolveSavedVariant: (model) => runtime.runPromise((svc) => svc.resolveSavedVariant(model)).catch(() => undefined), saveVariant: (model, variant) => runtime.runPromise((svc) => svc.saveVariant(model, variant)).catch(() => {}), diff --git a/packages/cli/src/node/index.ts b/packages/cli/src/node/index.ts new file mode 100644 index 0000000000..3559a614ca --- /dev/null +++ b/packages/cli/src/node/index.ts @@ -0,0 +1,9 @@ +import "./plugin-runtime.promise" +import "./plugin-runtime.effect" + +process.stdout.on("error", (error) => { + if ("code" in error && error.code === "EPIPE") return + throw error +}) + +await import("../index") diff --git a/packages/cli/src/node/plugin-runtime.effect.ts b/packages/cli/src/node/plugin-runtime.effect.ts new file mode 100644 index 0000000000..5e38b00d4c --- /dev/null +++ b/packages/cli/src/node/plugin-runtime.effect.ts @@ -0,0 +1,28 @@ +import { + Agent, + Command, + Connection, + Credential, + Integration, + Model, + Plugin, + Provider, + Reference, + Skill, +} from "@opencode-ai/plugin/v2/effect" +import { Tool } from "@opencode-ai/plugin/v2/effect/tool" + +const key = Symbol.for("opencode.plugin.v2.effect") +;(globalThis as typeof globalThis & { [key]?: unknown })[key] = { + Agent, + Command, + Connection, + Credential, + Integration, + Model, + Plugin, + Provider, + Reference, + Skill, + Tool, +} diff --git a/packages/cli/src/node/plugin-runtime.promise.ts b/packages/cli/src/node/plugin-runtime.promise.ts new file mode 100644 index 0000000000..93e20b87bb --- /dev/null +++ b/packages/cli/src/node/plugin-runtime.promise.ts @@ -0,0 +1,26 @@ +import { + Agent, + Command, + Connection, + Credential, + Integration, + Model, + Plugin, + Provider, + Reference, + Skill, +} from "@opencode-ai/plugin/v2" + +const key = Symbol.for("opencode.plugin.v2.promise") +;(globalThis as typeof globalThis & { [key]?: unknown })[key] = { + Agent, + Command, + Connection, + Credential, + Integration, + Model, + Plugin, + Provider, + Reference, + Skill, +} diff --git a/packages/cli/src/node/target.ts b/packages/cli/src/node/target.ts new file mode 100644 index 0000000000..097d74974f --- /dev/null +++ b/packages/cli/src/node/target.ts @@ -0,0 +1,34 @@ +const platforms = ["darwin", "linux", "win32"] as const + +export type NodeTarget = ReturnType + +export function nodeTarget(platform: string, arch: string) { + if (!platforms.includes(platform as (typeof platforms)[number]) || (arch !== "arm64" && arch !== "x64")) { + throw new Error(`Unsupported Node executable target: ${platform}-${arch}`) + } + + const targetPlatform = platform as (typeof platforms)[number] + const targetArch = arch as "arm64" | "x64" + const nodePtyPackage = `@lydell/node-pty-${targetPlatform}-${targetArch}` + const parcelWatcherPackage = `@parcel/watcher-${targetPlatform}-${targetArch}${targetPlatform === "linux" ? "-glibc" : ""}` + + return { + platform: targetPlatform, + arch: targetArch, + nodePtyPackage, + nodePtyEntryAsset: `${nodePtyPackage}/lib/index.js`, + parcelWatcherPackage, + parcelWatcherAsset: `${parcelWatcherPackage}/watcher.node`, + } +} + +export const photonWasmAsset = "@silvia-odwyer/photon-node/photon_rs_bg.wasm" +export const nodeExecArgv = ["--experimental-ffi", "--use-system-ca", "--disable-warning=ExperimentalWarning"] as const + +export const attentionSoundAssets = [ + "@opencode-ai/ui/audio/bip-bop-01.mp3", + "@opencode-ai/ui/audio/bip-bop-03.mp3", + "@opencode-ai/ui/audio/staplebops-06.mp3", + "@opencode-ai/ui/audio/nope-03.mp3", + "@opencode-ai/ui/audio/yup-01.mp3", +] as const diff --git a/packages/cli/src/server-process.ts b/packages/cli/src/server-process.ts new file mode 100644 index 0000000000..6cc4b7e7dd --- /dev/null +++ b/packages/cli/src/server-process.ts @@ -0,0 +1,190 @@ +export * as ServerProcess from "./server-process" + +import { NodeServices } from "@effect/platform-node" +import { Service, type DiscoverOptions, type Info } from "@opencode-ai/client/effect/service" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { Global } from "@opencode-ai/core/global" +import { InstallationVersion } from "@opencode-ai/core/installation/version" +import { AppProcess } from "@opencode-ai/core/process" +import { randomBytes, randomUUID } from "node:crypto" +import path from "node:path" +import { Effect, FileSystem, Logger, Option, Redacted, Schedule, Schema } from "effect" +import { HttpServer } from "effect/unstable/http" +import { Env } from "./env" +import { ServiceConfig } from "./services/service-config" +import { Updater } from "./services/updater" + +export type Mode = "default" | "service" | "stdio" + +export type Options = { + readonly mode: Mode + readonly hostname?: string + readonly port?: number +} + +// The process effect lives until server shutdown; tracing it would parent every request to one process-lifetime trace. +export const run = Effect.fnUntraced(function* (options: Options) { + return yield* processEffect(options).pipe( + Effect.provide(Updater.layer), + Effect.provide(LayerNode.compile(LayerNode.group([Global.node, AppProcess.node]))), + Effect.provide(NodeServices.layer), + ) +}) + +const processEffect = Effect.fnUntraced(function* (options: Options) { + if (options.mode === "service") yield* Effect.sync(() => process.chdir(Global.Path.home)) + return yield* Effect.scoped( + Effect.gen(function* () { + const serviceOptions = options.mode === "service" ? yield* ServiceConfig.options() : undefined + const config = options.mode === "service" ? yield* ServiceConfig.read() : {} + const hostname = options.hostname ?? config.hostname ?? "127.0.0.1" + const port = options.port ?? config.port ?? (options.mode === "service" ? ServiceConfig.defaultPort() : undefined) + if ( + serviceOptions !== undefined && + port !== undefined && + (yield* Service.incumbent({ ...serviceOptions, url: serviceURL(hostname, port) })) !== undefined + ) + return + const { start } = yield* Effect.promise(() => import("@opencode-ai/server/process")) + const environmentPassword = yield* Env.password + // Keep the lease credential out of the environment inherited by tools. + if (options.mode === "stdio") { + delete process.env.OPENCODE_PASSWORD + delete process.env.OPENCODE_SERVER_PASSWORD + } + const password = + options.mode === "service" + ? config.password || randomBytes(32).toString("base64url") + : environmentPassword + ? Redacted.value(environmentPassword) + : randomBytes(32).toString("base64url") + if (!password) return yield* Effect.fail(new Error("Missing server password")) + const instanceID = randomUUID() + const server = yield* start({ + hostname, + port: Option.fromNullishOr(port), + password, + instanceID, + service: + serviceOptions === undefined + ? undefined + : { + onListen: (address, shutdown) => + Effect.gen(function* () { + if (!config.password) yield* ServiceConfig.password(password) + return yield* register(address, password, instanceID, serviceOptions.file, shutdown) + }), + }, + }).pipe( + Effect.provide(Logger.layer([], { mergeWithExisting: false })), + Effect.catch((error) => { + if (serviceOptions === undefined || port === undefined || !addressInUse(error)) return Effect.fail(error) + return recognizeIncumbent(serviceOptions, hostname, port).pipe( + Effect.flatMap((found) => + found + ? Effect.void + : Effect.fail( + new Error( + `Managed service port ${port} on ${hostname} is already in use by another process. ` + + "Configure another port with `opencode service set port ` and start the service again.", + { cause: error }, + ), + ), + ), + ) + }), + ) + if (server === undefined) return + const url = HttpServer.formatAddress(server.address) + console.log(options.mode === "stdio" ? JSON.stringify({ url }) : `server listening on ${url}`) + if (options.mode === "default" && !environmentPassword) console.log(`server password ${password}`) + const updater = yield* Updater.Service + yield* updater.check().pipe(Effect.schedule(Schedule.spaced("10 minutes")), Effect.forkScoped) + return yield* options.mode === "service" + ? server.shutdown + : options.mode === "stdio" + ? waitForStdinClose() + : Effect.never + }).pipe(Effect.annotateLogs({ role: "server" })), + ) +}) + +const infoJson = Schema.fromJsonString(Service.Info) +const encodeInfo = Schema.encodeEffect(infoJson) +const decodeInfo = Schema.decodeUnknownEffect(infoJson) + +const register = Effect.fnUntraced(function* ( + address: HttpServer.Address, + password: string, + id: string, + file: string, + shutdown: Effect.Effect, +) { + const fs = yield* FileSystem.FileSystem + const temp = file + "." + id + ".tmp" + yield* fs.makeDirectory(path.dirname(file), { recursive: true }) + const info = { + id, + version: InstallationVersion, + url: HttpServer.formatAddress(address), + pid: process.pid, + password, + } + const encoded = yield* encodeInfo(info) + const current = fs.readFileString(file).pipe( + Effect.flatMap(decodeInfo), + Effect.orElseSucceed(() => undefined), + ) + const owns = (found: Info | undefined) => + found?.id === info.id && + found.version === info.version && + found.url === info.url && + found.pid === info.pid && + found.password === info.password + yield* fs.writeFileString(temp, encoded, { mode: 0o600 }).pipe(Effect.andThen(fs.rename(temp, file))) + yield* current.pipe( + Effect.filterOrFail(owns), + Effect.repeat(Schedule.spaced("5 seconds")), + Effect.ignore, + Effect.andThen(shutdown), + Effect.forkScoped, + ) + return current.pipe( + Effect.flatMap((found) => (owns(found) ? fs.remove(file) : Effect.void)), + Effect.ignore, + ) +}) + +const recognizeIncumbent = Effect.fnUntraced(function* (options: DiscoverOptions, hostname: string, port: number) { + const found = yield* Service.incumbent({ ...options, url: serviceURL(hostname, port) }).pipe( + Effect.filterOrFail((value) => value !== undefined), + Effect.retry(Schedule.spaced("100 millis")), + Effect.timeoutOption("15 seconds"), + ) + return Option.isSome(found) +}) + +function serviceURL(hostname: string, port: number) { + return `http://${hostname.includes(":") ? `[${hostname}]` : hostname}:${port}` +} + +function addressInUse(error: unknown): boolean { + if (typeof error !== "object" || error === null) return false + if ("code" in error && error.code === "EADDRINUSE") return true + return "cause" in error && addressInUse(error.cause) +} + +function waitForStdinClose() { + return Effect.callback((resume) => { + const close = () => resume(Effect.void) + process.stdin.once("end", close) + process.stdin.once("close", close) + process.stdin.resume() + if (process.stdin.readableEnded || process.stdin.destroyed) close() + return Effect.sync(() => { + process.stdin.off("end", close) + process.stdin.off("close", close) + process.stdin.pause() + }) + }) +} diff --git a/packages/cli/src/services/daemon.ts b/packages/cli/src/services/daemon.ts deleted file mode 100644 index bd30656f55..0000000000 --- a/packages/cli/src/services/daemon.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { Global } from "@opencode-ai/core/global" -import { InstallationVersion } from "@opencode-ai/core/installation/version" -import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" -import { ServerAuth } from "@opencode-ai/server/auth" -import { Context, Effect, FileSystem, Layer, Option, Schedule, Schema, Scope } from "effect" -import { HttpServer } from "effect/unstable/http" -import { randomBytes, randomUUID } from "crypto" -import { spawn } from "node:child_process" -import path from "path" - -export interface Interface { - readonly client: () => Effect.Effect, unknown> - readonly transport: () => Effect.Effect<{ url: string; headers: RequestInit["headers"] }, unknown> - readonly start: () => Effect.Effect - readonly status: () => Effect.Effect - readonly stop: () => Effect.Effect - readonly password: (value?: string) => Effect.Effect - readonly register: (address: HttpServer.Address) => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/cli/Daemon") {} - -const Registration = Schema.Struct({ - id: Schema.optional(Schema.String), - version: Schema.optional(Schema.String), - url: Schema.String, - pid: Schema.Int.check(Schema.isGreaterThan(0)), -}) -type Registration = typeof Registration.Type - -function sameRegistration(left: Registration, right: Registration) { - return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid -} - -export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem - const directory = Global.Path.state - const file = path.join(directory, "server.json") - const passwordFile = path.join(directory, "password") - const decodeRegistration = Schema.decodeUnknownEffect(Schema.fromJsonString(Registration)) - - const password = Effect.fn("cli.daemon.password")(function* (value?: string) { - const existing = yield* fs.readFileString(passwordFile).pipe(Effect.catch(() => Effect.succeed(undefined))) - if (value === undefined && existing) return existing - - // Keep one private credential across server restarts so discovered clients - // can reconnect without exposing a password flag or environment variable. - const generated = value ?? randomBytes(32).toString("base64url") - const temp = passwordFile + ".tmp" - yield* fs.makeDirectory(directory, { recursive: true }) - yield* fs.writeFileString(temp, generated, { mode: 0o600 }) - yield* fs.rename(temp, passwordFile) - return generated - }) - - const registration = Effect.fnUntraced(function* () { - return yield* fs.readFileString(file).pipe(Effect.flatMap(decodeRegistration)) - }) - - const createClient = Effect.fnUntraced(function* (url: string) { - return createOpencodeClient({ baseUrl: url, headers: ServerAuth.headers({ password: yield* password() }) }) - }) - - const healthy = Effect.fnUntraced(function* () { - const info = yield* registration() - const client = yield* createClient(info.url) - const response = yield* Effect.tryPromise(() => client.v2.health.get({ signal: AbortSignal.timeout(2_000) })) - if (response.data?.healthy === true) return info - return yield* Effect.fail(new Error("Registered server is not healthy")) - }) - - const compatible = Effect.fnUntraced(function* () { - const info = yield* healthy() - if (info.version === InstallationVersion) return info - return yield* Effect.fail(new Error("Registered server version does not match the client")) - }) - - const signal = (pid: number, signal: NodeJS.Signals) => - Effect.try({ try: () => process.kill(pid, signal), catch: (cause) => cause }).pipe(Effect.ignore) - - const awaitStopped = Effect.fnUntraced(function* (pid: number) { - const running = yield* Effect.try({ try: () => process.kill(pid, 0), catch: () => false }).pipe( - Effect.orElseSucceed(() => false), - ) - if (!running) return true - return yield* Effect.fail(new Error(`Server process ${pid} is still running`)) - }) - - const stopProcess = Effect.fnUntraced(function* (info: Registration) { - const current = yield* healthy().pipe(Effect.option) - if (Option.isNone(current) || !sameRegistration(current.value, info)) return - - yield* signal(info.pid, "SIGTERM") - const stopped = yield* awaitStopped(info.pid).pipe( - Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))), - Effect.option, - ) - if (Option.isSome(stopped)) return - - const latest = yield* healthy().pipe(Effect.option) - if (Option.isNone(latest) || !sameRegistration(latest.value, info)) return - yield* signal(info.pid, "SIGKILL") - yield* awaitStopped(info.pid).pipe( - Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))), - ) - }) - - const start = Effect.fn("cli.daemon.start")(function* () { - const existing = yield* healthy().pipe(Effect.option) - const found = Option.getOrUndefined(existing) - const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun" - if (found?.version === InstallationVersion && compiled) return found.url - if (found) yield* stopProcess(found).pipe(Effect.ignore) - - const entrypoint = compiled ? undefined : process.argv[1] - if (!compiled && entrypoint === undefined) - return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint")) - yield* Effect.try({ - try: () => { - spawn(process.execPath, [...(entrypoint ? [entrypoint] : []), "serve", "--register"], { - detached: true, - stdio: "ignore", - }).unref() - }, - catch: (cause) => new Error("Failed to start server", { cause }), - }) - - return yield* compatible().pipe( - Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))), - Effect.map((info) => info.url), - Effect.mapError(() => new Error("Failed to start server")), - ) - }) - - const transport = Effect.fn("cli.daemon.transport")(function* () { - return { url: yield* start(), headers: ServerAuth.headers({ password: yield* password() }) } - }) - - const client = Effect.fn("cli.daemon.client")(function* () { - const connection = yield* transport() - return createOpencodeClient({ baseUrl: connection.url, headers: connection.headers }) - }) - - const status = Effect.fn("cli.daemon.status")(function* () { - const existing = yield* healthy().pipe(Effect.option) - const found = Option.getOrUndefined(existing) - if (found?.version === InstallationVersion) return found.url - if (found) return undefined - yield* fs.remove(file).pipe(Effect.ignore) - return undefined - }) - - const stop = Effect.fn("cli.daemon.stop")(function* () { - const existing = yield* healthy().pipe(Effect.option) - // A stale registration may point at a PID that has since been reused by - // another process. Only signal the PID after authenticating the server. - if (Option.isNone(existing)) return yield* fs.remove(file).pipe(Effect.ignore) - yield* stopProcess(existing.value) - yield* fs.remove(file).pipe(Effect.ignore) - }) - - const register = Effect.fn("cli.daemon.register")(function* (address: HttpServer.Address) { - const id = randomUUID() - const temp = file + "." + id + ".tmp" - yield* fs.makeDirectory(directory, { recursive: true }) - yield* fs.writeFileString( - temp, - JSON.stringify({ id, version: InstallationVersion, url: HttpServer.formatAddress(address), pid: process.pid }), - { mode: 0o600 }, - ) - yield* fs.rename(temp, file) - yield* registration().pipe( - Effect.flatMap((info) => (info.id === id ? Effect.void : signal(process.pid, "SIGTERM"))), - Effect.catch(() => signal(process.pid, "SIGTERM")), - Effect.repeat(Schedule.spaced("10 seconds")), - Effect.forkScoped, - ) - yield* Effect.addFinalizer(() => - registration().pipe( - Effect.flatMap((info) => (info.id === id ? fs.remove(file) : Effect.void)), - Effect.ignore, - ), - ) - }) - - return Service.of({ client, transport, start, status, stop, password, register }) - }), -) - -export * as Daemon from "./daemon" diff --git a/packages/cli/src/services/server-connection.ts b/packages/cli/src/services/server-connection.ts new file mode 100644 index 0000000000..4eccc95bc9 --- /dev/null +++ b/packages/cli/src/services/server-connection.ts @@ -0,0 +1,93 @@ +import { Service, type Endpoint, type EnsureOptions } from "@opencode-ai/client/effect/service" +import { ClientError, isUnauthorizedError, OpenCode } from "@opencode-ai/client/promise" +import { InstallationVersion } from "@opencode-ai/core/installation/version" +import { Effect, Redacted } from "effect" +import { Env } from "../env" +import { ServiceConfig } from "./service-config" +import { Standalone } from "./standalone" + +export type Args = { + readonly server?: string + readonly standalone?: boolean + readonly mismatch?: "replace" | "ignore" | "error" + readonly onStart?: EnsureOptions["onStart"] +} + +export type Resolved = { + readonly endpoint: Endpoint + readonly service?: ReturnType +} + +export const resolve = Effect.fn("cli.server-connection.resolve")(function* (args: Args) { + if (args.server !== undefined && args.standalone) + return yield* Effect.fail(new Error("--server and --standalone cannot be combined")) + if (args.server !== undefined) { + const password = yield* Env.password + const endpoint = { + url: args.server, + auth: password ? { type: "basic" as const, username: "opencode", password: Redacted.value(password) } : undefined, + } satisfies Endpoint + const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }) + const health = yield* Effect.tryPromise({ + try: () => client.health.get({ signal: AbortSignal.timeout(5_000) }), + catch: (cause) => connectError(endpoint, cause), + }) + if (health.version !== InstallationVersion) + process.stderr.write( + `Warning: Server at ${endpoint.url} has version ${health.version}; this client is ${InstallationVersion}. Continuing anyway.\n`, + ) + return { endpoint } satisfies Resolved + } + if (args.standalone) { + return { endpoint: yield* Standalone.start() } satisfies Resolved + } + + const options = yield* ServiceConfig.options() + return { + endpoint: yield* resolveManaged({ ...options, onStart: args.onStart }, args.mismatch ?? "replace"), + service: managedService(options), + } satisfies Resolved +}) + +function managedService(options: EnsureOptions) { + const reconnectOptions = { ...options, version: undefined } + return { + reconnect: () => Service.ensure(reconnectOptions), + restart: () => + Effect.gen(function* () { + yield* Service.stop(options) + yield* Service.ensure(options) + }), + } +} + +const resolveManaged = Effect.fnUntraced(function* ( + options: EnsureOptions, + mismatch: NonNullable, +) { + if (mismatch === "replace") return yield* Service.ensure(options) + if (mismatch === "ignore") return yield* Service.ensure({ ...options, version: undefined }) + + const compatible = yield* Service.discover(options) + if (compatible !== undefined) return compatible + const existing = yield* Service.discover({ ...options, version: undefined }) + if (existing !== undefined) + return yield* Effect.fail(new Error("Background server version does not match this client")) + return yield* Service.ensure(options) +}) + +function connectError(endpoint: Endpoint, cause: unknown) { + if (isUnauthorizedError(cause)) { + return new Error( + endpoint.auth === undefined + ? `Server at ${endpoint.url} requires a password; set OPENCODE_PASSWORD` + : `Server at ${endpoint.url} rejected the password`, + { cause }, + ) + } + if (cause instanceof ClientError && cause.reason === "Transport") + return new Error(`Could not reach server at ${endpoint.url}`, { cause }) + return new Error(`Server at ${endpoint.url} did not provide a compatible V2 health response`, { cause }) +} + +export * as ServerConnection from "./server-connection" diff --git a/packages/cli/src/services/service-config.ts b/packages/cli/src/services/service-config.ts new file mode 100644 index 0000000000..58c7c76935 --- /dev/null +++ b/packages/cli/src/services/service-config.ts @@ -0,0 +1,187 @@ +import { Global } from "@opencode-ai/core/global" +import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version" +import { Hash } from "@opencode-ai/core/util/hash" +import { Service } from "@opencode-ai/client/effect/service" +import { Effect, FileSystem, Option, Schema } from "effect" +import { randomBytes } from "crypto" +import path from "path" +import { selfCommand } from "../util/process" + +// The CLI's service configuration file, plus the Service.EnsureOptions binding that +// points the client package's service operations at this CLI: which +// registration file (by channel), which version, and how to spawn opencode. + +export const Info = Schema.Struct({ + hostname: Schema.optional(Schema.String), + port: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(65_535))), + password: Schema.optional(Schema.String), +}) +export type Info = typeof Info.Type + +const keys = ["hostname", "port", "password"] as const +type Key = (typeof keys)[number] + +const decodeInfo = Schema.decodeUnknownEffect(Schema.fromJsonString(Info)) +const decodeRegistration = Schema.decodeUnknownEffect(Schema.fromJsonString(Service.Info)) + +export function filename(channel = InstallationChannel) { + if (channel === "latest") return "service.json" + if (channel === "local") return "service-local.json" + return `service-${Hash.fast(channel)}.json` +} + +export function defaultPort(channel = InstallationChannel) { + if (channel === "latest") return 0xc0de + if (channel === "local") return 0xc0df + return 10_000 + (Number.parseInt(Hash.fast(channel).slice(0, 8), 16) % 50_000) +} + +export function versionBelongsToChannel( + version: string | undefined, + channel = InstallationChannel, + installedVersion = InstallationVersion, +) { + if (version === undefined) return false + if (version === installedVersion) return true + const prefix = `0.0.0-${channel}-` + if (!version.startsWith(prefix)) return false + return /^\d+(?:\.\d+)?$/.test(version.slice(prefix.length)) +} + +export const migrateRegistration = Effect.fnUntraced(function* ( + legacy: string, + file: string, + channel = InstallationChannel, + installedVersion = InstallationVersion, +) { + if (channel === "latest" || channel === "local") return + const fs = yield* FileSystem.FileSystem + const text = yield* fs.readFileString(legacy).pipe(Effect.option) + if (Option.isNone(text)) return + const registration = yield* decodeRegistration(text.value).pipe(Effect.option) + if (Option.isNone(registration)) return + if (!versionBelongsToChannel(registration.value.version, channel, installedVersion)) return + yield* fs.writeFileString(file, text.value, { flag: "wx", mode: 0o600 }).pipe(Effect.ignore) +}) + +function configKey(key: string): Key { + if (key === "hostname" || key === "port" || key === "password") return key + throw new Error(`Unknown service config key: ${key}`) +} + +const paths = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + const global = yield* Global.Service + const name = filename() + const file = path.join(global.state, name) + return { + fs, + file, + legacyFile: path.join(global.state, "service.json"), + configFile: path.join(global.config, name), + } +}) + +export const options = Effect.fnUntraced(function* () { + const { file, legacyFile } = yield* paths + yield* migrateRegistration(legacyFile, file) + return { + file, + version: InstallationVersion, + command: [...selfCommand(), "serve", "--service"], + } +}) + +export const read = Effect.fn("cli.service-config.read")(function* () { + const { fs, configFile } = yield* paths + return yield* fs.readFileString(configFile).pipe( + Effect.flatMap(decodeInfo), + Effect.catch(() => Effect.succeed({} as Info)), + ) +}) + +const write = Effect.fn("cli.service-config.write")(function* (value: Info) { + const { fs, configFile } = yield* paths + const temp = configFile + ".tmp" + yield* fs.makeDirectory(path.dirname(configFile), { recursive: true }) + yield* fs.writeFileString(temp, JSON.stringify(value, null, 2) + "\n", { mode: 0o600 }) + yield* fs.rename(temp, configFile) +}) + +export const password = Effect.fn("cli.service-config.password")(function* (value?: string) { + const existing = yield* read() + if (value === undefined && existing.password) return existing.password + const next = value ?? randomBytes(32).toString("base64url") + + // Keep one private credential across server restarts so discovered clients + // can reconnect without exposing a password flag or environment variable. + yield* write({ ...existing, password: next }) + return next +}) + +export const get = Effect.fn("cli.service-config.get")(function* (key?: string) { + if (key === undefined) { + const { password: _password, ...safe } = yield* read() + return JSON.stringify(safe, null, 2) + } + switch (configKey(key)) { + case "hostname": { + return (yield* read()).hostname ?? "" + } + case "port": { + const port = (yield* read()).port + return port === undefined ? "" : String(port) + } + case "password": { + return yield* password() + } + } + throw new Error(`Unknown service config key: ${key}`) +}) + +export const set = Effect.fn("cli.service-config.set")(function* (key: string, value: string) { + switch (configKey(key)) { + case "hostname": { + yield* Service.stop(yield* options()) + yield* write({ ...(yield* read()), hostname: value }) + return + } + case "port": { + const port = Number(value) + if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error("Port must be between 1 and 65535") + yield* Service.stop(yield* options()) + yield* write({ ...(yield* read()), port }) + return + } + case "password": { + yield* Service.stop(yield* options()) + yield* password(value) + return + } + } +}) + +export const unset = Effect.fn("cli.service-config.unset")(function* (key: string) { + switch (configKey(key)) { + case "hostname": { + yield* Service.stop(yield* options()) + const { hostname: _hostname, ...next } = yield* read() + yield* write(next) + return + } + case "port": { + yield* Service.stop(yield* options()) + const { port: _port, ...next } = yield* read() + yield* write(next) + return + } + case "password": { + yield* Service.stop(yield* options()) + const { password: _password, ...next } = yield* read() + yield* write(next) + return + } + } +}) + +export * as ServiceConfig from "./service-config" diff --git a/packages/cli/src/services/standalone.ts b/packages/cli/src/services/standalone.ts new file mode 100644 index 0000000000..899af8153f --- /dev/null +++ b/packages/cli/src/services/standalone.ts @@ -0,0 +1,63 @@ +import { Service, type Endpoint } from "@opencode-ai/client/effect/service" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { Deferred, Effect, Schema, Stream } from "effect" +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import { randomBytes } from "node:crypto" +import { selfCommand } from "../util/process" + +const Ready = Schema.Struct({ url: Schema.String }) +const decodeReady = Schema.decodeUnknownPromise(Schema.fromJsonString(Ready)) + +type Options = { + readonly command?: ReadonlyArray +} + +function command(password: string, options: Options) { + const [executable, ...args] = options.command ?? [...selfCommand(), "serve"] + if (!executable) throw new Error("Failed to resolve standalone server command") + return ChildProcess.make(executable, [...args, "--stdio", "--port", "0"], { + cwd: process.cwd(), + // Explicit entry wins over anything inherited, so a user-exported + // OPENCODE_PASSWORD cannot shadow the child's lease credential. + env: { OPENCODE_PASSWORD: password }, + extendEnv: true, + // The server treats EOF on this pipe as the end of its ownership lease. + // The OS closes it even when the TUI is killed before Effect finalizers run. + stdin: "pipe", + stderr: "ignore", + killSignal: "SIGTERM", + forceKillAfter: "3 seconds", + }) +} + +const makeEndpoint = Effect.fn("cli.standalone.endpoint")( + function* (options: Options) { + const password = randomBytes(32).toString("base64url") + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const proc = yield* spawner.spawn(command(password, options)) + const readyLine = yield* Deferred.make() + // Keep draining stdout after readiness so later server writes cannot hit EPIPE. + yield* proc.stdout.pipe( + Stream.decodeText(), + Stream.splitLines, + Stream.runForEach((line) => Deferred.succeed(readyLine, line)), + Effect.ensuring(Deferred.fail(readyLine, new Error("Standalone server exited before reporting readiness"))), + Effect.forkScoped, + ) + const output = yield* Deferred.await(readyLine) + const ready = yield* Effect.tryPromise(() => decodeReady(output)) + return { + url: ready.url, + auth: { type: "basic" as const, username: "opencode", password }, + pid: proc.pid, + } satisfies Endpoint & { readonly pid: number } + }, + Effect.provide(LayerNode.compile(CrossSpawnSpawner.node)), +) + +export function start(options: Options = {}) { + return makeEndpoint(options) +} + +export * as Standalone from "./standalone" diff --git a/packages/cli/src/services/update-preflight.tsx b/packages/cli/src/services/update-preflight.tsx new file mode 100644 index 0000000000..f1dc57c460 --- /dev/null +++ b/packages/cli/src/services/update-preflight.tsx @@ -0,0 +1,495 @@ +/** @jsxImportSource @opentui/solid */ +// Split-footer status shown while a freshly launched CLI replaces a +// version-mismatched background service before the TUI attaches. +import { createCliRenderer, RGBA, TextAttributes, type CliRenderer, type ThemeMode } from "@opentui/core" +import { render, useTerminalDimensions } from "@opentui/solid" +import { InstallationVersion } from "@opencode-ai/core/installation/version" +import { registerOpencodeSpinner } from "@opencode-ai/tui/component/register-spinner" +import { SPINNER_FRAMES } from "@opencode-ai/tui/component/spinner" +import { go } from "@opencode-ai/tui/logo" +import { setTimeout } from "node:timers/promises" +import { + batch, + createEffect, + createMemo, + createSignal, + For, + Index, + on, + onCleanup, + onMount, + Show, + untrack, +} from "solid-js" + +const stages = ["Keeping your session safe", "Starting the new background service", "Loading OpenCode"] as const +const stageFloor = 480 +const transitionDuration = 420 +const completionHold = 650 + +export type Handle = { + readonly begin: (from?: string) => boolean + readonly loading: () => void + readonly finish: () => Promise + readonly fail: (message: string) => Promise + readonly close: () => Promise +} + +export type Handoff = { + readonly renderer: CliRenderer + readonly mode: ThemeMode | null + readonly complete: () => void +} + +export const make = (): Handle => { + let session: Promise | undefined + return { + begin: (from) => { + if (!process.stdout.isTTY || !process.stdin.isTTY) return false + session ??= open(from).catch(() => { + process.stderr.write("Restarting background server (version mismatch)...\n") + return undefined + }) + return true + }, + loading: () => { + void session?.then((active) => active?.loading()) + }, + finish: async () => { + const active = await session + return active?.finish() + }, + fail: async (message) => { + const active = await session + await active?.fail(message) + }, + close: async () => { + const active = await session + await active?.close() + }, + } +} + +type Session = { + readonly loading: () => Promise + readonly finish: () => Promise + readonly fail: (message: string) => Promise + readonly close: () => Promise +} + +async function open(from?: string): Promise { + registerOpencodeSpinner() + const [active, setActive] = createSignal(0) + const [outcome, setOutcome] = createSignal<"running" | "success" | "failure">("running") + const [failure, setFailure] = createSignal("") + const [animating, setAnimating] = createSignal(true) + const [visible, setVisible] = createSignal(true) + let resolveOutcome: (() => void) | undefined + const renderer = await createCliRenderer({ + stdin: process.stdin, + useMouse: false, + autoFocus: false, + openConsoleOnError: false, + exitOnCtrlC: false, + screenMode: "split-footer", + footerHeight: 4, + targetFps: 60, + useKittyKeyboard: {}, + consoleOptions: { + keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }], + }, + externalOutputMode: "capture-stdout", + consoleMode: "disabled", + }) + const terminalMode = renderer.waitForThemeMode(1000).catch(() => null) + await render( + () => ( + + resolveOutcome?.()} + /> + + ), + renderer, + ).catch((error) => { + if (!renderer.isDestroyed) renderer.destroy() + throw error + }) + let shownAt = performance.now() + const waitForStage = async () => { + const remaining = stageFloor - (performance.now() - shownAt) + if (remaining > 0) await setTimeout(remaining) + } + const advance = async (stage: number) => { + await waitForStage() + if (outcome() !== "running") return + setActive(stage) + shownAt = performance.now() + } + // Service.start currently exposes only its start boundary, so this first + // transition is time-based. Finer lifecycle callbacks remain follow-up work. + const auto = advance(1) + const transitionTo = async (next: "success" | "failure", hold: number) => { + const settled = Promise.withResolvers() + resolveOutcome = settled.resolve + setOutcome(next) + const completed = await Promise.race([ + settled.promise.then(() => true), + setTimeout(transitionDuration + 500).then(() => false), + ]) + resolveOutcome = undefined + setAnimating(false) + if (completed) await setTimeout(hold) + } + let closing: Promise | undefined + let transferred = false + const close = () => + (closing ??= (async () => { + if (transferred) return + setAnimating(false) + if (renderer.isDestroyed) return + renderer.pause() + await Promise.race([renderer.idle(), setTimeout(500)]) + renderer.destroy() + })()) + let loading: Promise | undefined + const load = () => + (loading ??= (async () => { + await auto + await advance(2) + })()) + let settled: Promise | undefined + const settle = (task: () => Promise) => (settled ??= task()) + return { + loading: load, + finish: async () => { + await settle(async () => { + await load() + await waitForStage() + await transitionTo("success", completionHold) + }) + const mode = await terminalMode + renderer.externalOutputMode = "passthrough" + renderer.screenMode = "alternate-screen" + renderer.consoleMode = "console-overlay" + renderer.requestRender() + await Promise.race([renderer.idle(), setTimeout(500)]) + transferred = true + return { + renderer, + mode, + complete: () => setVisible(false), + } + }, + fail: (message) => + settle(async () => { + setFailure(message) + await transitionTo("failure", 250) + await close() + }), + close, + } +} + +const colors = { + accent: RGBA.fromHex("#a6b8ff"), + accentBright: RGBA.fromHex("#eef1ff"), + accentDim: RGBA.fromHex("#596998"), + error: RGBA.fromHex("#ff8192"), + muted: RGBA.fromHex("#808080"), + success: RGBA.fromHex("#8bd5a5"), + text: RGBA.fromHex("#eeeeee"), +} + +const monogram = go.right.slice(1) +const sweepBlend = 8 +const textDim = RGBA.fromHex("#4c4c4c") +const rampSteps = 32 + +const blend = (from: RGBA, to: RGBA, amount: number) => + RGBA.fromValues( + from.r + (to.r - from.r) * amount, + from.g + (to.g - from.g) * amount, + from.b + (to.b - from.b) * amount, + ) +const ramp = (from: RGBA, to: RGBA) => + Array.from({ length: rampSteps + 1 }, (_, step) => blend(from, to, step / rampSteps)) +const railRamp = ramp(colors.accentDim, colors.accentBright) +const monogramRamp = ramp(colors.muted, colors.accent) +const rampCache = new Map>() +const rampFor = (color: RGBA) => { + const cached = rampCache.get(color) + if (cached) return cached + const result = ramp(textDim, color) + rampCache.set(color, result) + return result +} +const shade = (palette: ReadonlyArray, brightness: number) => + palette[Math.round(Math.max(0, Math.min(1, brightness)) * rampSteps)] + +type Cell = { readonly char: string; readonly color: RGBA; readonly bold?: boolean } +const styled = (text: string, color: RGBA, bold?: boolean): Cell[] => + Array.from(text).map((char) => ({ char, color, bold })) +const phrase = (...segments: ReadonlyArray): Cell[] => + segments.flatMap((segment, index) => [ + ...(index > 0 ? styled(" ", colors.muted) : []), + ...styled(segment[0], segment[1], segment[2]), + ]) + +function Monogram(props: { ink: () => RGBA }) { + const shadow = createMemo(() => { + const ink = props.ink() + return RGBA.fromValues(ink.r * 0.25, ink.g * 0.25, ink.b * 0.25) + }) + return ( + + + {(line) => ( + + + {(char) => + char === "_" ? ( + + {" "} + + ) : ( + + {char} + + ) + } + + + )} + + + ) +} + +type CellTransition = { from: Cell[]; to: Cell[]; done?: () => void } + +function createTransition(render: (transition: CellTransition, progress: number) => Cell[]) { + const [state, setState] = createSignal<{ from: Cell[]; to: Cell[]; done?: () => void } | undefined>() + const [progress, setProgress] = createSignal(0) + let elapsed = 0 + const cells = createMemo(() => { + const transition = state() + if (!transition) return undefined + return render(transition, progress()) + }) + return { + start(from: Cell[], to: Cell[], done?: () => void) { + elapsed = 0 + setProgress(0) + setState({ from, to, done }) + }, + tick(deltaTime: number) { + const transition = state() + if (!transition) return + elapsed = Math.min(transitionDuration, elapsed + deltaTime) + setProgress(elapsed / transitionDuration) + if (elapsed < transitionDuration) return + setState(undefined) + transition.done?.() + }, + cells, + progress, + } +} + +const createSweep = () => + createTransition((transition, progress) => { + const length = Math.max(transition.from.length, transition.to.length) + const front = smoothstep(progress) * (length + 2 * sweepBlend) - sweepBlend + return Array.from({ length }, (_, index) => { + const passed = Math.max(0, Math.min(1, (front - index) / sweepBlend)) + const brightness = smoothstep(Math.abs(passed * 2 - 1)) + const cell = (passed >= 0.5 ? transition.to[index] : transition.from[index]) ?? { + char: " ", + color: colors.text, + } + return { ...cell, color: shade(rampFor(cell.color), brightness) } + }) + }) + +const createFade = () => + createTransition((transition, progress) => { + const entering = progress >= 0.5 + const brightness = smoothstep(entering ? progress * 2 - 1 : 1 - progress * 2) + return (entering ? transition.to : transition.from).map((cell) => ({ + ...cell, + color: shade(rampFor(cell.color), brightness), + })) + }) + +const smoothstep = (value: number) => value * value * (3 - 2 * value) +const frameDone = Promise.resolve() + +function UpdateFooter(props: { + from?: string + active: () => number + outcome: () => "running" | "success" | "failure" + failure: () => string + animating: () => boolean + renderer: CliRenderer + onOutcomeSettled: () => void +}) { + const term = useTerminalDimensions() + const [position, setPosition] = createSignal(0) + const [pulse, setPulse] = createSignal(0) + const headerFade = createFade() + const statusSweep = createSweep() + const runningHeader = () => + phrase( + ["OpenCode", colors.muted, true], + ["is updating", colors.muted], + ...(props.from + ? ([ + ["from", colors.muted], + [props.from, colors.accentDim], + ] as const) + : []), + ["to", colors.muted], + [InstallationVersion, colors.accent], + ) + const completedHeader = phrase( + ["OpenCode", colors.muted, true], + ["updated to", colors.muted], + [InstallationVersion, colors.accent], + ) + const pausedHeader = phrase(["OpenCode", colors.muted, true], ["update paused", colors.muted]) + const outcomeStatus = () => + props.outcome() === "success" + ? [...styled("✓", colors.success), ...styled(" Ready", colors.text)] + : [...styled("!", colors.error), ...styled(" " + props.failure(), colors.text)] + let previousStage: string = stages[0] + createEffect( + on(props.active, (index) => { + if (props.outcome() !== "running") return + const next = stages[index] + if (next === previousStage) return + statusSweep.start(styled(previousStage, colors.text), styled(next, colors.text)) + previousStage = next + }), + ) + createEffect( + on( + props.outcome, + (outcome) => { + if (outcome === "running") return + const visibleStatus = untrack(statusSweep.cells) ?? styled(previousStage, colors.text) + headerFade.start(runningHeader(), outcome === "success" ? completedHeader : pausedHeader) + statusSweep.start([...styled(" ", colors.text), ...visibleStatus], outcomeStatus(), props.onOutcomeSettled) + }, + { defer: true }, + ), + ) + const header = createMemo( + () => + headerFade.cells() ?? + (props.outcome() === "success" + ? completedHeader + : props.outcome() === "failure" + ? pausedHeader + : runningHeader()), + ) + const monogramInk = createMemo(() => + props.outcome() === "success" ? shade(monogramRamp, smoothstep(headerFade.progress())) : colors.muted, + ) + const rail = createMemo(() => { + const width = Math.max(0, Math.min(30, term().width - 39)) + if (width === 0) return [] + const filled = Math.round(position() * width) + const glowRadius = 6 + const span = Math.max(1, filled + glowRadius * 2) + const center = pulse() * span - glowRadius + const success = props.outcome() === "success" + const completion = smoothstep(headerFade.progress()) + return Array.from({ length: width }, (_, index) => { + const color = + index >= filled + ? colors.muted + : shade(railRamp, Math.max(0, 1 - Math.abs(index - center) / glowRadius) ** 2) + return { + char: success || index < filled ? "━" : "·", + color: success ? blend(color, colors.accent, completion) : color, + } + }) + }) + + onMount(() => { + let value = 0 + let velocity = 0 + let phase = 0 + const frame = (deltaTime: number) => { + if (!props.animating()) return frameDone + const elapsed = Math.min(0.032, deltaTime / 1_000) + const stiffness = 110 + const damping = 2 * Math.sqrt(stiffness) + const target = props.outcome() === "success" ? 1 : (props.active() + 1) / stages.length + velocity += (stiffness * (target - value) - damping * velocity) * elapsed + value += velocity * elapsed + phase = (phase + deltaTime / 900) % 1 + batch(() => { + setPosition(Math.max(0, Math.min(1, value))) + setPulse(phase) + }) + headerFade.tick(deltaTime) + statusSweep.tick(deltaTime) + return frameDone + } + props.renderer.setFrameCallback(frame) + onCleanup(() => props.renderer.removeFrameCallback(frame)) + }) + + return ( + + + + + } + > + + + + + + + + + {props.outcome() === "success" ? stages.length : props.active() + 1}/{stages.length} + + + + + ) +} + +function CellLine(props: { cells: ReadonlyArray }) { + return ( + + + {(cell) => ( + + {cell().char} + + )} + + + ) +} + +export * as UpdatePreflight from "./update-preflight" diff --git a/packages/cli/src/services/updater.test.ts b/packages/cli/src/services/updater.test.ts new file mode 100644 index 0000000000..e11de2a0d9 --- /dev/null +++ b/packages/cli/src/services/updater.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, test } from "bun:test" +import { action, decodePolicy } from "./updater" + +describe("updater", () => { + test("reads autoupdate from JSONC", () => { + expect(decodePolicy('{ // preference\n "autoupdate": "notify",\n}')).toBe("notify") + expect(decodePolicy('{ "autoupdate": false }')).toBe(false) + expect(decodePolicy('{ "autoupdate": "invalid" }')).toBeUndefined() + }) + + test("automatically updates patches and minors", () => { + expect(action("1.2.3", "1.2.4", true)).toBe("upgrade") + expect(action("1.2.3", "1.3.0", true)).toBe("upgrade") + expect(action("1.2.3", "1.2.4", "notify")).toBe("upgrade") + expect(action("1.2.3", "1.3.0", "notify")).toBe("upgrade") + }) + + test("skips when autoupdate is disabled", () => { + expect(action("1.2.3", "1.2.4", false)).toBe("none") + }) + + test("never automatically updates majors", () => { + expect(action("1.2.3", "2.0.0", true)).toBe("none") + }) + + test("reports up-to-date only when versions match", () => { + expect(action("1.2.3", "1.2.3", true)).toBe("none") + }) + + test("upgrades when latest is lower (rollback)", () => { + expect(action("1.2.4", "1.2.3", true)).toBe("upgrade") + }) +}) diff --git a/packages/cli/src/services/updater.ts b/packages/cli/src/services/updater.ts new file mode 100644 index 0000000000..fd46b90084 --- /dev/null +++ b/packages/cli/src/services/updater.ts @@ -0,0 +1,161 @@ +import { Global } from "@opencode-ai/core/global" +import { Flag } from "@opencode-ai/core/flag/flag" +import { AppProcess } from "@opencode-ai/core/process" +import { + InstallationChannel, + InstallationLocal, + InstallationVersion, +} from "@opencode-ai/core/installation/version" +import { Context, Duration, Effect, FileSystem, Layer } from "effect" +import { ChildProcess } from "effect/unstable/process" +import { parse, type ParseError } from "jsonc-parser" +import path from "node:path" +import semver from "semver" + +declare const OPENCODE_CLI_NAME: string | undefined + +export type Policy = boolean | "notify" +export type Action = "none" | "upgrade" +type Method = "npm" | "pnpm" | "bun" | "yarn" + +const packageName = + typeof OPENCODE_CLI_NAME === "string" && OPENCODE_CLI_NAME === "opencode2-node" + ? OPENCODE_CLI_NAME + : "@opencode-ai/cli" + +export interface Interface { + readonly check: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/cli/Updater") {} + +export function decodePolicy(text: string): Policy | undefined { + // The CLI only projects this host-level preference instead of initializing + // the location-scoped server configuration graph. + const errors: ParseError[] = [] + const input: unknown = parse(text, errors, { allowTrailingComma: true }) + if (errors.length || typeof input !== "object" || input === null || !("autoupdate" in input)) return + const value = input.autoupdate + if (typeof value === "boolean" || value === "notify") return value +} + +export function action(current: string, latest: string, policy: Policy): Action { + if (policy === false) return "none" + if (!semver.valid(current) || !semver.valid(latest) || semver.eq(latest, current)) return "none" + // Major upgrades are never installed automatically. + if (semver.major(latest) !== semver.major(current)) return "none" + return "upgrade" +} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + const global = yield* Global.Service + const appProcess = yield* AppProcess.Service + const channel = InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-") + + const readPolicy = Effect.fnUntraced(function* () { + const values = yield* Effect.forEach(["config.json", "opencode.json", "opencode.jsonc"], (name) => + fs + .readFileString(path.join(global.config, name)) + .pipe(Effect.map(decodePolicy), Effect.catch(() => Effect.succeed(undefined))), + ) + return values.findLast((value) => value !== undefined) ?? true + }) + + const run = Effect.fnUntraced(function* (command: string[], timeout: Duration.Input = "10 seconds") { + return yield* appProcess + .run(ChildProcess.make(command[0], command.slice(1)), { + timeout, + maxOutputBytes: 100_000, + maxErrorBytes: 100_000, + }) + .pipe( + Effect.map((result) => ({ + code: result.exitCode, + stdout: result.stdout.toString("utf8"), + stderr: result.stderr.toString("utf8"), + })), + Effect.catch(() => Effect.succeed({ code: 1, stdout: "", stderr: "" })), + ) + }) + + const method = Effect.fnUntraced(function* () { + const checks: ReadonlyArray<{ method: Method; command: string[] }> = [ + { method: "npm", command: ["npm", "list", "-g", "--depth=0", packageName] }, + { method: "pnpm", command: ["pnpm", "list", "-g", "--depth=0", packageName] }, + { method: "bun", command: ["bun", "pm", "ls", "-g"] }, + { method: "yarn", command: ["yarn", "global", "list"] }, + ] + const results = yield* Effect.forEach( + checks, + (check) => run(check.command).pipe(Effect.map((result) => ({ check, result }))), + { concurrency: "unbounded" }, + ) + return results.find((result) => result.result.stdout.includes(packageName))?.check.method + }) + + const latest = Effect.fnUntraced(function* () { + const response = yield* Effect.tryPromise({ + try: () => + fetch( + `https://registry.npmjs.org/${encodeURIComponent(packageName)}/${encodeURIComponent(InstallationChannel)}`, + { headers: { "User-Agent": `opencode/${InstallationVersion}` }, signal: AbortSignal.timeout(10_000) }, + ), + catch: (cause) => new Error("Failed to check for updates", { cause }), + }) + if (!response.ok) return yield* Effect.fail(new Error(`Update check failed with status ${response.status}`)) + const data = yield* Effect.tryPromise({ + try: () => response.json(), + catch: (cause) => new Error("Failed to read update information", { cause }), + }) + if (typeof data !== "object" || data === null || !("version" in data) || typeof data.version !== "string") { + return yield* Effect.fail(new Error("Update information did not include a version")) + } + return data.version + }) + + const upgrade = Effect.fnUntraced(function* (method: Method, version: string) { + const target = `${packageName}@${version}` + const commands: Record = { + npm: ["npm", "install", "--global", target], + pnpm: ["pnpm", "install", "--global", target], + bun: ["bun", "install", "--global", target], + yarn: ["yarn", "global", "add", target], + } + const result = yield* run(commands[method], "5 minutes") + if (result.code === 0) return + return yield* Effect.fail(new Error(result.stderr.trim() || `Failed to update with ${method}`)) + }) + + const check = Effect.fn("cli.updater.check")(function* () { + if (InstallationLocal || Flag.OPENCODE_DISABLE_AUTOUPDATE) + return yield* Effect.logInfo("update check skipped", { + reason: InstallationLocal ? "local-install" : "disabled", + version: InstallationVersion, + channel: InstallationChannel, + }) + const policy = yield* readPolicy() + if (policy === false) return yield* Effect.logInfo("update check skipped", { reason: "policy-disabled" }) + + return yield* Effect.gen(function* () { + const version = yield* latest() + yield* Effect.logInfo("update check", { + current: InstallationVersion, + latest: version, + }) + const next = action(InstallationVersion, version, policy) + if (next === "none") return yield* Effect.logInfo("update check done", { action: "up-to-date" }) + const detected = yield* method() + if (!detected) return yield* Effect.logWarning("automatic update skipped: installation method not found") + yield* upgrade(detected, version) + yield* Effect.logInfo("updated OpenCode", { from: InstallationVersion, to: version, method: detected }) + }) + }, Effect.catchCause((cause) => Effect.logWarning("automatic update failed", { cause }))) + + return Service.of({ check }) + }), +) + +export * as Updater from "./updater" diff --git a/packages/cli/src/tui.ts b/packages/cli/src/tui.ts deleted file mode 100644 index 5100e1c99a..0000000000 --- a/packages/cli/src/tui.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { run } from "@opencode-ai/tui" -import { TuiConfig } from "@opencode-ai/tui/config" -import { Effect } from "effect" -import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { Global } from "@opencode-ai/core/global" - -export function runTui(transport: { url: string; headers: RequestInit["headers"] }) { - const config = TuiConfig.resolve({}, { terminalSuspend: false }) - return run({ - ...transport, - args: {}, - config, - fetch: gracefulFetch, - pluginHost: { - async start() {}, - async dispose() {}, - }, - }).pipe(Effect.provide(AppNodeBuilder.build(Global.node))) -} - -const legacyDefaults: Record = { - "/config/providers": { providers: [], default: {} }, - "/provider": { all: [], default: {}, connected: [] }, - "/agent": [], - "/config": {}, -} - -const gracefulFetch = Object.assign( - async (input: RequestInfo | URL, init?: RequestInit) => { - const response = await fetch(input, init) - if (response.status !== 404) return response - const fallback = legacyDefaults[new URL(input instanceof Request ? input.url : input).pathname] - if (fallback === undefined) return response - return Response.json(fallback) - }, - { preconnect: fetch.preconnect }, -) diff --git a/packages/cli/src/ui/timeline.tsx b/packages/cli/src/ui/timeline.tsx new file mode 100644 index 0000000000..8c7e6612c6 --- /dev/null +++ b/packages/cli/src/ui/timeline.tsx @@ -0,0 +1,242 @@ +/** @jsxImportSource @opentui/solid */ +import { createCliRenderer, RGBA, type CliRenderer, type ColorInput, type ScrollbackWriter } from "@opentui/core" +import { createScrollbackWriter, render, useKeyboard } from "@opentui/solid" +import { registerOpencodeSpinner } from "@opencode-ai/tui/component/register-spinner" +import { Show, createSignal } from "solid-js" + +registerOpencodeSpinner() + +export type TimelineHost = { + readonly signal: AbortSignal + intro(text: string): Promise + item(text: string): Promise + pending(text: string): Promise + success(text: string): Promise + failure(text: string): Promise + outro(text: string): Promise + close(): Promise +} + +type RowKind = "intro" | "item" | "success" | "failure" | "outro" + +const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] +const IDLE_TIMEOUT = 1_000 +const COLORS = { + accent: RGBA.fromIndex(6), + error: RGBA.fromIndex(1), + foreground: RGBA.defaultForeground(), + muted: RGBA.fromIndex(8), + success: RGBA.fromIndex(2), +} +const ROWS: Record = { + intro: { marker: "┌", color: COLORS.muted, connector: true }, + item: { marker: "●", color: COLORS.accent, connector: true }, + success: { marker: "◇", color: COLORS.success, connector: true }, + failure: { marker: "■", color: COLORS.error, connector: false }, + outro: { marker: "└", color: COLORS.muted, connector: false }, +} + +function row(kind: RowKind, value: string): ScrollbackWriter { + const style = ROWS[kind] + return createScrollbackWriter( + () => ( + + + + {style.marker} + + + {value} + + + + + + + ), + { startOnNewLine: true, trailingNewline: !style.connector }, + ) +} + +function TimelineFooter(props: { pending: () => string | undefined; cancel: () => void }) { + useKeyboard((event) => { + if (event.name !== "escape" && !(event.ctrl && event.name === "c")) return + event.preventDefault() + props.cancel() + }) + + return ( + + + {(text) => ( + <> + + + {text()} + + + )} + + + ) +} + +function bounded(task: Promise) { + return new Promise((resolve) => { + const timer = setTimeout(resolve, IDLE_TIMEOUT) + timer.unref() + const finish = () => { + clearTimeout(timer) + resolve() + } + void task.then(finish, finish) + }) +} + +async function shutdown(renderer: CliRenderer): Promise { + await bounded(renderer.idle()) + try { + renderer.externalOutputMode = "passthrough" + } finally { + try { + renderer.screenMode = "main-screen" + } finally { + if (!renderer.isDestroyed) renderer.destroy() + } + } +} + +export async function createTimelineHost(): Promise { + const stdout = process.stdout + const controller = new AbortController() + const signals: NodeJS.Signals[] = ["SIGINT", "SIGHUP", "SIGQUIT"] + const cancel = () => { + if (!controller.signal.aborted) controller.abort() + } + signals.forEach((signal) => process.on(signal, cancel)) + + if (!stdout.isTTY || !process.stdin.isTTY) { + let closed = false + let writing = false + let active: Promise | undefined + let closeTask: Promise | undefined + const write = async (kind: RowKind | "pending", text: string) => { + if (closed) throw new Error("timeline closed") + if (writing) throw new Error("timeline write already in progress") + writing = true + try { + const style = kind === "pending" ? undefined : ROWS[kind] + const marker = kind === "pending" ? "." : ROWS[kind].marker + const connector = style?.connector ? "│\n" : "" + active = new Promise((resolve, reject) => { + stdout.write(`${marker} ${text}\n${connector}`, (error) => (error ? reject(error) : resolve())) + }) + await active + } finally { + writing = false + active = undefined + } + } + const close = () => { + if (closeTask) return closeTask + closed = true + closeTask = (async () => { + await active?.catch(() => { }) + signals.forEach((signal) => process.off(signal, cancel)) + })() + return closeTask + } + return { + signal: controller.signal, + intro: (text) => write("intro", text), + item: (text) => write("item", text), + pending: (text) => write("pending", text), + success: (text) => write("success", text), + failure: (text) => write("failure", text), + outro: (text) => write("outro", text), + close, + } + } + + let renderer: CliRenderer | undefined + + try { + // Start on a fresh row so delayed SSH cursor reports cannot make + // split-footer overwrite the shell command. + process.stdout.write("\n") + renderer = await createCliRenderer({ + stdin: process.stdin, + useMouse: false, + autoFocus: false, + openConsoleOnError: false, + exitOnCtrlC: false, + exitSignals: [], + screenMode: "split-footer", + footerHeight: 1, + externalOutputMode: "capture-stdout", + consoleMode: "disabled", + clearOnShutdown: false, + }) + const activeRenderer = renderer + const [pending, setPending] = createSignal() + const renderTask = render(() => , activeRenderer) + void renderTask.catch(cancel) + await bounded(activeRenderer.idle()) + + let closed = false + let writing = false + let active: Promise | undefined + let closeTask: Promise | undefined + const write = (kind: RowKind | "pending", text: string) => { + if (closed) return Promise.reject(new Error("timeline closed")) + if (writing) return Promise.reject(new Error("timeline write already in progress")) + writing = true + active = (async () => { + if (kind === "pending") { + setPending(text) + activeRenderer.requestRender() + } else { + if (kind === "success" || kind === "failure" || kind === "outro") setPending(undefined) + activeRenderer.writeToScrollback(row(kind, text)) + activeRenderer.requestRender() + } + await bounded(activeRenderer.idle()) + })().finally(() => { + writing = false + active = undefined + }) + return active + } + const close = () => { + if (closeTask) return closeTask + closed = true + closeTask = (async () => { + await active?.catch(() => { }) + try { + await shutdown(activeRenderer) + await bounded(renderTask) + } finally { + signals.forEach((signal) => process.off(signal, cancel)) + } + })() + return closeTask + } + return { + signal: controller.signal, + intro: (text) => write("intro", text), + item: (text) => write("item", text), + pending: (text) => write("pending", text), + success: (text) => write("success", text), + failure: (text) => write("failure", text), + outro: (text) => write("outro", text), + close, + } + } catch (error) { + try { + if (renderer) await shutdown(renderer) + } finally { + signals.forEach((signal) => process.off(signal, cancel)) + } + throw error + } +} diff --git a/packages/cli/src/util/io.ts b/packages/cli/src/util/io.ts new file mode 100644 index 0000000000..e1995779a7 --- /dev/null +++ b/packages/cli/src/util/io.ts @@ -0,0 +1,5 @@ +import { text } from "node:stream/consumers" + +export function readStdin() { + return text(process.stdin) +} diff --git a/packages/cli/src/util/process.ts b/packages/cli/src/util/process.ts new file mode 100644 index 0000000000..a0c567aad5 --- /dev/null +++ b/packages/cli/src/util/process.ts @@ -0,0 +1,26 @@ +import path from "node:path" + +export function selfCommand() { + const runtime = path.basename(process.execPath, path.extname(process.execPath)).toLowerCase() + if (runtime !== "bun" && runtime !== "node" && runtime !== "nodejs") return [process.execPath] + if (!process.argv[1]) throw new Error("Failed to resolve CLI entrypoint") + if (runtime === "node" || runtime === "nodejs") return [process.execPath, ...nodeFlags(), process.argv[1]] + return [process.execPath, process.argv[1]] +} + +function nodeFlags() { + return process.execArgv.flatMap((arg, index, args) => { + if (index > 0 && args[index - 1] === "--conditions") return [] + if (arg === "--conditions") return args[index + 1] ? [arg, args[index + 1]] : [] + if (arg.startsWith("--conditions=")) return [arg] + if ( + arg === "--experimental-ffi" || + arg === "--use-system-ca" || + arg === "--enable-source-maps" || + arg === "--no-addons" + ) + return [arg] + if (arg === "--no-warnings" || arg.startsWith("--disable-warning=")) return [arg] + return [] + }) +} diff --git a/packages/cli/test/config.test.ts b/packages/cli/test/config.test.ts new file mode 100644 index 0000000000..41c023d1a1 --- /dev/null +++ b/packages/cli/test/config.test.ts @@ -0,0 +1,143 @@ +import { NodeFileSystem } from "@effect/platform-node" +import { Global } from "@opencode-ai/core/global" +import { Effect } from "effect" +import { expect, test } from "bun:test" +import path from "path" +import { Config } from "../src/config" + +function run(directory: string, effect: Effect.Effect) { + return Effect.runPromise( + effect.pipe( + Effect.provide(Config.layer), + Effect.provide(Global.layerWith({ config: directory, state: directory })), + Effect.provide(NodeFileSystem.layer), + ), + ) +} + +test("migrates tui and kv config into cli.json", async () => { + const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim()) + await Bun.write( + path.join(directory, "tui.json"), + JSON.stringify({ + theme: "legacy", + keybinds: { leader: "ctrl+o" }, + plugin: [["example", { mode: "safe" }]], + plugin_enabled: { disabled: false }, + leader_timeout: 500, + scroll_speed: 2, + scroll_acceleration: { enabled: true }, + diff_style: "stacked", + mouse: false, + }), + ) + await Bun.write( + path.join(directory, "kv.json"), + JSON.stringify({ + theme_mode_lock: "light", + attention_sound_pack: "custom.pack", + diff_wrap_mode: "none", + diff_viewer_show_file_tree: false, + diff_viewer_single_patch: true, + diff_viewer_view: "split", + terminal_title_enabled: false, + file_context_enabled: false, + paste_summary_enabled: false, + sidebar: "hide", + scrollbar_visible: true, + thinking_mode: "show", + exploration_grouping: false, + dismissed_getting_started: true, + animations_enabled: false, + skipped_version: "9.9.9", + which_key_layout: "overlay", + }), + ) + + try { + const config = await run( + directory, + Effect.gen(function* () { + const service = yield* Config.Service + return yield* service.get() + }), + ) + + expect(config).toMatchObject({ + theme: { name: "legacy", mode: "light" }, + keybinds: { leader: "ctrl+o" }, + plugins: [{ package: "example", options: { mode: "safe" } }, "-disabled"], + leader: { timeout: 500 }, + scroll: { speed: 2, acceleration: true }, + attention: { sound_pack: "custom.pack" }, + diffs: { wrap: "none", tree: false, single: true, view: "split" }, + terminal: { title: false }, + prompt: { editor: false, paste: "full" }, + session: { sidebar: "hide", scrollbar: true, thinking: "show", grouping: "none" }, + hints: { onboarding: false }, + animations: false, + mouse: false, + }) + expect(config).not.toHaveProperty("skipped_version") + expect(config).not.toHaveProperty("which_key") + expect((await Bun.file(path.join(directory, "cli.json")).json()).keybinds).toEqual({ leader: "ctrl+o" }) + expect(await Bun.file(path.join(directory, "cli.json")).exists()).toBe(true) + expect(await Bun.file(path.join(directory, "tui.json")).exists()).toBe(true) + expect(await Bun.file(path.join(directory, "kv.json")).exists()).toBe(true) + } finally { + await Bun.$`rm -rf ${directory}` + } +}) + +test("migrates before the first update and does not remigrate afterward", async () => { + const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim()) + await Bun.write(path.join(directory, "tui.json"), JSON.stringify({ theme: "legacy" })) + + try { + const config = await run( + directory, + Effect.gen(function* () { + const service = yield* Config.Service + yield* service.update((draft) => { + draft.animations = false + draft.mouse = false + }) + yield* Effect.promise(() => + Bun.write(path.join(directory, "tui.json"), JSON.stringify({ theme: "changed" })), + ) + return yield* service.get() + }), + ) + + expect(config).toEqual({ theme: { name: "legacy" }, animations: false, mouse: false }) + expect(await Bun.file(path.join(directory, "cli.json")).json()).toEqual({ + theme: { name: "legacy" }, + animations: false, + mouse: false, + }) + } finally { + await Bun.$`rm -rf ${directory}` + } +}) + +test("updates a config draft while preserving JSONC comments", async () => { + const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim()) + await Bun.write(path.join(directory, "cli.json"), "{\n // Keep this comment\n \"animations\": true\n}\n") + + try { + const config = await run( + directory, + Effect.gen(function* () { + const service = yield* Config.Service + return yield* service.update((draft) => { + draft.prompt = { paste: "compact" } + }) + }), + ) + + expect(config).toEqual({ animations: true, prompt: { paste: "compact" } }) + expect(await Bun.file(path.join(directory, "cli.json")).text()).toContain("// Keep this comment") + } finally { + await Bun.$`rm -rf ${directory}` + } +}) diff --git a/packages/cli/test/fixture/standalone-owner.ts b/packages/cli/test/fixture/standalone-owner.ts new file mode 100644 index 0000000000..62e1b74887 --- /dev/null +++ b/packages/cli/test/fixture/standalone-owner.ts @@ -0,0 +1,19 @@ +import { Effect } from "effect" +import { Service } from "@opencode-ai/client/effect/service" +import path from "node:path" +import { Standalone } from "../../src/services/standalone" + +process.argv[1] = path.join(import.meta.dir, "../../src/index.ts") + +await Effect.runPromise( + Effect.scoped( + Effect.gen(function* () { + const endpoint = yield* Standalone.start() + const response = yield* Effect.promise(() => + fetch(new URL("/api/health", endpoint.url), { headers: Service.headers(endpoint) }), + ) + console.log(`${endpoint.pid} ${endpoint.url} ${response.status}`) + return yield* Effect.never + }), + ), +) diff --git a/packages/cli/test/footer-keymap.test.tsx b/packages/cli/test/footer-keymap.test.tsx new file mode 100644 index 0000000000..6318d973fe --- /dev/null +++ b/packages/cli/test/footer-keymap.test.tsx @@ -0,0 +1,95 @@ +/** @jsxImportSource @opentui/solid */ +import { testRender } from "@opentui/solid" +import { Keymap } from "@opencode-ai/tui/context/keymap" +import { resolve } from "@opencode-ai/tui/config/v1" +import { expect, test } from "bun:test" +import { createSignal } from "solid-js" +import { RunFooterView } from "../src/mini/footer.view" +import { RUN_THEME_FALLBACK } from "../src/mini/theme" +import type { FooterState, FooterSubagentState, FooterView } from "../src/mini/types" + +test("down opens subagents from an empty prompt", async () => { + const [state] = createSignal({ + phase: "idle", + status: "", + queue: 0, + model: "gpt-5", + duration: "", + usage: "", + first: false, + interrupt: 0, + exit: 0, + }) + const [view] = createSignal({ type: "prompt" }) + const [subagents] = createSignal({ + tabs: [ + { + sessionID: "subagent-1", + partID: "part-1", + callID: "call-1", + label: "Explore", + description: "Inspect the keymap", + status: "running", + lastUpdatedAt: 1, + }, + ], + details: {}, + permissions: [], + questions: [], + }) + const config = resolve( + { keybinds: { editor_open: "none", session_queued_prompts: "none" } }, + { terminalSuspend: true }, + ) + function Harness() { + return ( + + []} + agents={() => []} + references={() => []} + commands={() => []} + providers={() => undefined} + currentModel={() => undefined} + variants={() => []} + currentVariant={() => undefined} + state={state} + view={view} + subagent={subagents} + theme={() => RUN_THEME_FALLBACK} + tuiConfig={config} + agent="opencode" + onSubmit={() => true} + onPermissionReply={() => {}} + onQuestionReply={() => {}} + onQuestionReject={() => {}} + onCycle={() => {}} + onInterrupt={() => false} + onEditorOpen={async () => undefined} + onInputClear={() => {}} + onExit={() => {}} + onModelSelect={() => {}} + onVariantSelect={() => {}} + onRows={() => {}} + onLayout={() => {}} + onStatus={() => {}} + onQueuedRemove={async () => true} + /> + + ) + } + + const app = await testRender(() => , { width: 100, height: 8, kittyKeyboard: true }) + try { + await app.renderOnce() + expect(app.renderer.currentFocusedEditor?.plainText).toBe("") + app.mockInput.pressArrow("down") + await app.renderOnce() + expect(app.captureCharFrame()).toContain("Select subagent") + } finally { + app.renderer.currentFocusedRenderable?.blur() + app.renderer.currentFocusedEditor?.blur() + app.renderer.destroy() + } +}) diff --git a/packages/cli/test/mini.test.ts b/packages/cli/test/mini.test.ts new file mode 100644 index 0000000000..5fb8b3be7f --- /dev/null +++ b/packages/cli/test/mini.test.ts @@ -0,0 +1,168 @@ +import { describe, expect, test } from "bun:test" +import { InstallationVersion } from "@opencode-ai/core/installation/version" +import path from "node:path" +import { mergeInteractiveInput, mergeNonInteractiveInput, parseRunModel, pickRunModel } from "../src/mini" +import { toolInlineInfo, toolView } from "../src/mini/tool" + +async function cli(args: string[]) { + const child = Bun.spawn([process.execPath, "run", "src/index.ts", ...args], { + cwd: path.join(import.meta.dir, ".."), + stdout: "pipe", + stderr: "pipe", + }) + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + child.exited, + ]) + return { stdout, stderr, exitCode } +} + +describe("mini command", () => { + test("renders the renamed shell tool with the shell rule", () => { + const part = { + id: "part-shell", + sessionID: "session-shell", + messageID: "message-shell", + callID: "call-shell", + tool: "shell", + state: { + status: "pending" as const, + input: { command: "pwd" }, + }, + } as const + + expect(toolView(part.tool)).toEqual({ output: true, final: false }) + expect(toolInlineInfo(part)).toMatchObject({ icon: "$", title: "pwd", mode: "block" }) + }) + + test("uses piped stdin as the initial prompt", () => { + expect(mergeInteractiveInput("from stdin", undefined)).toBe("from stdin") + expect(mergeInteractiveInput("from stdin", "from flag")).toBe("from stdin\nfrom flag") + }) + + test("keeps run as mini's non-interactive input mode", () => { + expect(mergeNonInteractiveInput("from args", "from stdin")).toBe("from args\nfrom stdin") + expect(mergeNonInteractiveInput(undefined, "from stdin")).toBe("from stdin") + }) + + test("applies a variant to a resumed session's model", () => { + expect( + pickRunModel( + undefined, + "high", + { providerID: "session-provider", modelID: "session-model" }, + { providerID: "default-provider", modelID: "default-model" }, + ), + ).toEqual({ providerID: "session-provider", modelID: "session-model" }) + }) + + test("parses model variants from the model reference", () => { + expect(JSON.stringify(parseRunModel("openrouter/openai/gpt-5#high"))).toBe( + JSON.stringify({ model: { providerID: "openrouter", modelID: "openai/gpt-5" }, variant: "high" }), + ) + }) + + test("is registered in the preview CLI", async () => { + const result = await cli(["--help"]) + + expect(result.exitCode).toBe(0) + expect(result.stdout).toContain("mini Start the minimal interactive interface") + expect(result.stdout).toContain("run Run OpenCode with a message") + }) + + test("exposes run without legacy attach or command modes", async () => { + const result = await cli(["run", "--help"]) + + expect(result.exitCode).toBe(0) + expect(result.stdout).toContain("--server string") + expect(result.stdout).not.toContain("--variant") + expect(result.stdout).not.toContain("--attach") + expect(result.stdout).not.toContain("--command") + }) + + test("keeps option-like prompt text after the argument separator", async () => { + const result = await cli(["run", "--server", "http://127.0.0.1:1", "--", "--foo"]) + + expect(result.exitCode).toBe(1) + expect(result.stderr).not.toContain("You must provide a message") + }) + + test("preserves a run failure exit code", async () => { + let modelRequests = 0 + const server = Bun.serve({ + port: 0, + fetch(request) { + const url = new URL(request.url) + if (url.pathname === "/api/health") + return Response.json({ healthy: true, version: InstallationVersion, pid: process.pid }) + if (url.pathname === "/api/location") + return Response.json({ directory: process.cwd(), project: { id: "global", directory: process.cwd() } }) + if (url.pathname === "/api/model") { + modelRequests++ + return Response.json({ + location: { directory: process.cwd(), project: { id: "global", directory: process.cwd() } }, + data: modelRequests === 1 ? [{ id: "missing", providerID: "definitely" }] : [], + }) + } + return new Response(undefined, { status: 404 }) + }, + }) + + try { + const result = await cli([ + "run", + "--server", + server.url.toString(), + "--model", + "definitely/missing", + "hi", + ]) + + expect(result.exitCode).toBe(1) + expect(result.stderr).toContain("Model unavailable: definitely/missing") + } finally { + server.stop(true) + } + }) + + test("reports pre-admission errors as JSON", async () => { + const server = Bun.serve({ + port: 0, + fetch(request) { + if (new URL(request.url).pathname === "/api/session") return new Response("boom", { status: 500 }) + return Response.json({ healthy: true, version: "incompatible", pid: process.pid }) + }, + }) + + try { + const result = await cli(["run", "--format", "json", "--server", server.url.toString(), "hi"]) + + expect(result.exitCode).toBe(1) + expect(JSON.parse(result.stdout)).toMatchObject({ + type: "error", + sessionID: "", + error: { type: "unknown", message: "UnexpectedStatus" }, + }) + } finally { + server.stop(true) + } + }) + + test("uses the shared V2 server option instead of an attach command", async () => { + const result = await cli(["mini", "--help"]) + + expect(result.exitCode).toBe(0) + expect(result.stdout).toContain("--server string") + expect(result.stdout).not.toContain("SUBCOMMANDS") + }) + + test("routes local and explicit-server invocations into mini", async () => { + for (const args of [["mini"], ["mini", "--server", "http://127.0.0.1:1"]]) { + const result = await cli(args) + + expect(result.exitCode).toBe(1) + expect(result.stderr).toContain("opencode mini requires a TTY stdout") + } + }) +}) diff --git a/packages/cli/test/server-connection.test.ts b/packages/cli/test/server-connection.test.ts new file mode 100644 index 0000000000..41a061f57c --- /dev/null +++ b/packages/cli/test/server-connection.test.ts @@ -0,0 +1,57 @@ +import { NodeFileSystem } from "@effect/platform-node" +import { Global } from "@opencode-ai/core/global" +import { InstallationVersion } from "@opencode-ai/core/installation/version" +import { expect, test } from "bun:test" +import { Effect, FileSystem, Scope } from "effect" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { ServerConnection } from "../src/services/server-connection" +import { ServiceConfig } from "../src/services/service-config" + +test("resolution groups Effect-native lifecycle operations only for the managed service", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-server-resolution-")) + const id = "server-resolution-test" + const server = Bun.serve({ + port: 0, + fetch() { + return Response.json({ + healthy: true, + version: InstallationVersion, + pid: process.pid, + }) + }, + }) + const registration = path.join(root, "state", ServiceConfig.filename()) + const layer = Global.layerWith({ config: path.join(root, "config"), state: path.join(root, "state") }) + const runPromise = (effect: Effect.Effect) => + Effect.runPromise(effect.pipe(Effect.provide(layer), Effect.provide(NodeFileSystem.layer), Effect.scoped)) + + try { + await fs.mkdir(path.dirname(registration), { recursive: true }) + await fs.writeFile( + registration, + JSON.stringify({ + id, + version: InstallationVersion, + url: server.url.toString(), + pid: process.pid, + }), + ) + const resolved = await runPromise(ServerConnection.resolve({})) + + expect(resolved.endpoint.url).toBe(server.url.toString()) + expect(resolved.service).toBeDefined() + if (!resolved.service) throw new Error("Expected managed service capabilities") + expect(Effect.isEffect(resolved.service.reconnect())).toBe(true) + expect(Effect.isEffect(resolved.service.restart())).toBe(true) + expect(await runPromise(resolved.service.reconnect())).toEqual(resolved.endpoint) + + const explicit = await runPromise(ServerConnection.resolve({ server: server.url.toString() })) + expect(explicit.endpoint.url).toBe(server.url.toString()) + expect(explicit.service).toBeUndefined() + } finally { + await server.stop(true) + await fs.rm(root, { recursive: true, force: true }) + } +}) diff --git a/packages/cli/test/service.test.ts b/packages/cli/test/service.test.ts new file mode 100644 index 0000000000..707a3fbdb7 --- /dev/null +++ b/packages/cli/test/service.test.ts @@ -0,0 +1,629 @@ +import { NodeFileSystem } from "@effect/platform-node" +import { Service, type Info } from "@opencode-ai/client/effect/service" +import { Database } from "@opencode-ai/core/database/database" +import { EventV2 } from "@opencode-ai/core/event" +import { EventTable } from "@opencode-ai/core/event/sql" +import { Global } from "@opencode-ai/core/global" +import { InstallationVersion } from "@opencode-ai/core/installation/version" +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 { SessionEvent } from "@opencode-ai/core/session/event" +import { SessionTable } from "@opencode-ai/core/session/sql" +import { expect, test } from "bun:test" +import { Effect, Schedule, Schema } from "effect" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { ServiceConfig } from "../src/services/service-config" + +test("managed service ports are stable per installation channel", () => { + expect(ServiceConfig.defaultPort("latest")).toBe(0xc0de) + expect(ServiceConfig.defaultPort("local")).toBe(0xc0df) + expect(ServiceConfig.defaultPort("preview-a")).toBe(ServiceConfig.defaultPort("preview-a")) + expect(ServiceConfig.defaultPort("preview-a")).not.toBe(ServiceConfig.defaultPort("preview-b")) +}) + +test("local channel stores service config with the local service filename", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-")) + try { + await Effect.runPromise( + ServiceConfig.set("hostname", "127.0.0.2").pipe( + Effect.provide(Global.layerWith({ config: path.join(root, "config"), state: path.join(root, "state") })), + Effect.provide(NodeFileSystem.layer), + ), + ) + expect(await Bun.file(path.join(root, "config", "service-local.json")).json()).toEqual({ + hostname: "127.0.0.2", + }) + expect(await Bun.file(path.join(root, "config", "service.json")).exists()).toBe(false) + } finally { + await fs.rm(root, { recursive: true, force: true }) + } +}) + +test("service filenames isolate installation channels", () => { + expect(ServiceConfig.filename("latest")).toBe("service.json") + expect(ServiceConfig.filename("local")).toBe("service-local.json") + expect(ServiceConfig.filename("preview-a")).not.toBe(ServiceConfig.filename("preview-b")) + expect(ServiceConfig.filename("preview-a")).not.toBe(ServiceConfig.filename("latest")) + expect(ServiceConfig.versionBelongsToChannel("0.0.0-preview-a-1234", "preview-a")).toBe(true) + expect(ServiceConfig.versionBelongsToChannel("0.0.0-preview-a-1234.2", "preview-a")).toBe(true) + expect(ServiceConfig.versionBelongsToChannel("0.0.0-preview-a-other-1234", "preview-a")).toBe(false) + expect(ServiceConfig.versionBelongsToChannel("1.2.3", "preview-a")).toBe(false) +}) + +test("preview registration migration never moves stable discovery", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-migration-")) + const legacy = path.join(root, "service.json") + const target = path.join(root, ServiceConfig.filename("preview-a")) + try { + await fs.writeFile( + legacy, + JSON.stringify({ id: "old-preview", version: "0.0.0-preview-a-1234", url: "http://localhost:4096", pid: 1 }), + ) + await Effect.runPromise( + ServiceConfig.migrateRegistration(legacy, target, "preview-a", "0.0.0-preview-a-5678").pipe( + Effect.provide(NodeFileSystem.layer), + ), + ) + expect(await Bun.file(legacy).exists()).toBe(true) + expect(await Bun.file(target).json()).toMatchObject({ id: "old-preview" }) + + await fs.rm(target) + await fs.writeFile(legacy, JSON.stringify({ id: "stable", version: "1.2.3", url: "http://localhost:4096", pid: 1 })) + await Effect.runPromise( + ServiceConfig.migrateRegistration(legacy, target, "preview-a", "0.0.0-preview-a-5678").pipe( + Effect.provide(NodeFileSystem.layer), + ), + ) + expect(await Bun.file(legacy).exists()).toBe(true) + expect(await Bun.file(target).exists()).toBe(false) + + await fs.writeFile( + legacy, + JSON.stringify({ id: "old-preview", version: "0.0.0-preview-a-1234", url: "http://localhost:4096", pid: 1 }), + ) + await fs.writeFile(target, JSON.stringify({ id: "current-preview" })) + await Effect.runPromise( + ServiceConfig.migrateRegistration(legacy, target, "preview-a", "0.0.0-preview-a-5678").pipe( + Effect.provide(NodeFileSystem.layer), + ), + ) + expect(await Bun.file(legacy).exists()).toBe(true) + expect(await Bun.file(target).json()).toMatchObject({ id: "current-preview" }) + } finally { + await fs.rm(root, { recursive: true, force: true }) + } +}) + +test("managed service writes its registration once", async () => { + const service = await startManagedService("opencode-service-once-") + try { + const before = await fs.stat(service.registration) + await Bun.sleep(6_000) + const after = await fs.stat(service.registration) + expect(after.ino).toBe(before.ino) + expect(after.mtimeMs).toBe(before.mtimeMs) + expect(await Bun.file(service.registration).json()).toEqual(service.info) + } finally { + await stopManagedService(service) + } +}, 30_000) + +test("deleting a managed service registration stops its owner", async () => { + const service = await startManagedService("opencode-service-delete-") + try { + await fs.rm(service.registration) + expect(await waitForExit(service.owner)).toBe(true) + expect(await Bun.file(service.registration).exists()).toBe(false) + await expectPortAvailable(service.port) + } finally { + await stopManagedService(service) + } +}, 30_000) + +test("deleting a failed service registration stops its owner", async () => { + const service = await startManagedService("opencode-service-failed-delete-", true) + try { + await waitForFailed(service.info) + await fs.rm(service.registration) + expect(await waitForExit(service.owner)).toBe(true) + await expectPortAvailable(service.port) + } finally { + await stopManagedService(service) + } +}, 30_000) + +test("corrupting a managed service registration stops its owner", async () => { + const service = await startManagedService("opencode-service-corrupt-") + try { + await fs.writeFile(service.registration, "not-json") + expect(await waitForExit(service.owner)).toBe(true) + expect(await Bun.file(service.registration).text()).toBe("not-json") + await expectPortAvailable(service.port) + } finally { + await stopManagedService(service) + } +}, 30_000) + +test("replacing a managed service registration stops its owner and preserves the foreign owner", async () => { + const service = await startManagedService("opencode-service-foreign-") + const foreign = { ...service.info, id: "foreign-owner", pid: process.pid } + try { + await fs.writeFile(service.registration, JSON.stringify(foreign)) + expect(await waitForExit(service.owner)).toBe(true) + expect(await Bun.file(service.registration).json()).toEqual(foreign) + await expectPortAvailable(service.port) + } finally { + await stopManagedService(service) + } +}, 30_000) + +test("clean managed service shutdown removes its registration", async () => { + const service = await startManagedService("opencode-service-clean-") + try { + await Effect.runPromise(Service.stop({ file: service.registration }).pipe(Effect.provide(NodeFileSystem.layer))) + expect(await waitForExit(service.owner)).toBe(true) + expect(await Bun.file(service.registration).exists()).toBe(false) + } finally { + await stopManagedService(service) + } +}, 30_000) + +test("concurrent service processes elect one server", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-election-")) + const database = path.join(root, "opencode.db") + const env = { + ...process.env, + HOME: root, + OPENCODE_DB: database, + OPENCODE_TEST_HOME: root, + XDG_CACHE_HOME: path.join(root, "cache"), + XDG_CONFIG_HOME: path.join(root, "config"), + XDG_DATA_HOME: path.join(root, "data"), + XDG_STATE_HOME: path.join(root, "state"), + } + const sessionID = SessionV2.ID.make("ses_service_recovery") + await withDatabase( + database, + Effect.gen(function* () { + const { db } = yield* Database.Service + yield* db + .insert(ProjectTable) + .values({ id: Project.ID.global, worktree: AbsolutePath.make(root), sandboxes: [] }) + .run() + .pipe(Effect.orDie) + yield* db + .insert(SessionTable) + .values({ + id: sessionID, + project_id: Project.ID.global, + slug: "recovery", + directory: root, + title: "recovery", + version: "test", + time_suspended: Date.now(), + }) + .run() + .pipe(Effect.orDie) + }), + ) + const command = [process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"] + const registration = path.join(root, "state", "opencode", "service-local.json") + const port = await availablePort() + const config = path.join(root, "config", "opencode", "service-local.json") + await fs.mkdir(path.join(root, "config", "opencode"), { recursive: true }) + await fs.writeFile(config, JSON.stringify({ port })) + const processes = Array.from({ length: 10 }, () => Bun.spawn(command, { env, stderr: "pipe", stdout: "pipe" })) + + try { + const info = await waitForInfo(registration) + const winner = processes.find((process) => process.pid === info.pid) + const losers = processes.filter((process) => process.pid !== info.pid) + const exited = await Promise.all( + losers.map((process) => Promise.race([process.exited.then(() => true), Bun.sleep(60_000).then(() => false)])), + ) + + expect(exited).toEqual(losers.map(() => true)) + const errors = await Promise.all( + losers.map( + async (process) => (await new Response(process.stdout).text()) + (await new Response(process.stderr).text()), + ), + ) + expect( + losers.map((process) => process.exitCode), + errors.filter(Boolean).join("\n"), + ).toEqual(losers.map(() => 0)) + expect(winner?.exitCode).toBe(null) + expect(new URL(info.url).port).toBe(String(port)) + expect((await Bun.file(config).json()).password).toBe(info.password) + expect(await Bun.file(registration + ".lock").exists()).toBe(false) + expect( + await fetch(new URL("/api/health", info.url), { + headers: { authorization: "Basic " + btoa(`opencode:${info.password}`) }, + }).then((response) => response.json()), + ).toEqual({ + healthy: true, + version: info.version, + pid: info.pid, + }) + const contender = Bun.spawn(command, { env, stderr: "pipe", stdout: "ignore" }) + try { + const contenderExited = await Promise.race([ + contender.exited.then(() => true), + Bun.sleep(10_000).then(() => false), + ]) + expect(contenderExited).toBe(true) + expect(contender.exitCode).toBe(0) + expect((await waitForInfo(registration)).id).toBe(info.id) + } finally { + contender.kill("SIGTERM") + await contender.exited + } + expect( + await withDatabase( + database, + Effect.gen(function* () { + const { db } = yield* Database.Service + return yield* db + .select({ timeSuspended: SessionTable.time_suspended }) + .from(SessionTable) + .get() + .pipe(Effect.orDie) + }), + ), + ).toEqual({ timeSuspended: null }) + expect(await waitForExecutionStart(database, sessionID)).toBe(1) + await Effect.runPromise(Service.stop({ file: registration }).pipe(Effect.provide(NodeFileSystem.layer))) + await winner?.exited + expect(await Bun.file(registration).exists()).toBe(false) + } finally { + processes.forEach((process) => process.kill("SIGTERM")) + await Promise.all(processes.map((process) => process.exited)) + await fs.rm(root, { recursive: true, force: true }) + } +}, 120_000) + +test("configured managed service port overrides the channel default", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-port-")) + const port = await availablePort() + const env = serviceEnv(root) + const registration = path.join(root, "state", "opencode", "service-local.json") + const config = path.join(root, "config", "opencode", "service-local.json") + await fs.mkdir(path.join(root, "config", "opencode"), { recursive: true }) + await fs.writeFile(config, JSON.stringify({ port, password: "" })) + const owner = Bun.spawn([process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"], { + env, + stderr: "pipe", + stdout: "ignore", + }) + try { + const info = await waitForInfo(registration) + expect(new URL(info.url).port).toBe(String(port)) + expect(info.password).not.toBe("") + expect((await Bun.file(config).json()).password).toBe(info.password) + await Effect.runPromise(Service.stop({ file: registration }).pipe(Effect.provide(NodeFileSystem.layer))) + await owner.exited + } finally { + owner.kill("SIGTERM") + await owner.exited + await fs.rm(root, { recursive: true, force: true }) + } +}, 30_000) + +test("unrelated managed port occupancy reports an actionable conflict", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-conflict-")) + const listener = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response("unrelated") }) + const port = listener.port + const registration = path.join(root, "state", "opencode", "service-local.json") + await fs.mkdir(path.join(root, "config", "opencode"), { recursive: true }) + await fs.writeFile(path.join(root, "config", "opencode", "service-local.json"), JSON.stringify({ port })) + const contender = Bun.spawn([process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"], { + env: serviceEnv(root), + stderr: "pipe", + stdout: "pipe", + }) + try { + expect(await contender.exited).not.toBe(0) + const output = (await new Response(contender.stdout).text()) + (await new Response(contender.stderr).text()) + expect(output).toContain(`Managed service port ${port} on 127.0.0.1 is already in use by another process`) + expect(output).toContain("opencode service set port ") + expect(await Bun.file(registration).exists()).toBe(false) + } finally { + listener.stop(true) + contender.kill("SIGTERM") + await contender.exited + await fs.rm(root, { recursive: true, force: true }) + } +}, 30_000) + +test("unresponsive managed port occupancy reports a bounded conflict", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-unresponsive-conflict-")) + const recognizing = Promise.withResolvers() + const requests = { count: 0 } + using listener = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + fetch() { + requests.count += 1 + if (requests.count === 2) recognizing.resolve() + return new Promise(() => {}) + }, + }) + const registration = path.join(root, "state", "opencode", "service-local.json") + await fs.mkdir(path.join(root, "config", "opencode"), { recursive: true }) + await fs.mkdir(path.dirname(registration), { recursive: true }) + await fs.writeFile( + path.join(root, "config", "opencode", "service-local.json"), + JSON.stringify({ port: listener.port }), + ) + const stale = { + id: "stale", + version: InstallationVersion, + url: "http://127.0.0.1:1", + pid: process.pid, + password: "stale", + } + await fs.writeFile(registration, JSON.stringify(stale)) + const contender = Bun.spawn([process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"], { + env: serviceEnv(root), + stderr: "pipe", + stdout: "pipe", + }) + + try { + expect(await Promise.race([recognizing.promise.then(() => true), Bun.sleep(20_000).then(() => false)])).toBe(true) + const exitCode = await Promise.race([contender.exited, Bun.sleep(20_000).then(() => undefined)]) + expect(exitCode).toBe(1) + const output = (await new Response(contender.stdout).text()) + (await new Response(contender.stderr).text()) + expect(output).toContain(`Managed service port ${listener.port} on 127.0.0.1 is already in use by another process`) + expect(await Bun.file(registration).json()).toEqual(stale) + } finally { + contender.kill("SIGTERM") + await contender.exited + await fs.rm(root, { recursive: true, force: true }) + } +}, 45_000) + +test("port contender recognizes an incumbent registered during the bind race", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-bind-race-")) + const recognizing = Promise.withResolvers() + const requests = { count: 0 } + using listener = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + fetch() { + requests.count += 1 + if (requests.count === 2) recognizing.resolve() + return Response.json({ healthy: true, version: InstallationVersion, pid: process.pid }, { status: 503 }) + }, + }) + const registration = path.join(root, "state", "opencode", "service-local.json") + const config = path.join(root, "config", "opencode", "service-local.json") + await fs.mkdir(path.dirname(config), { recursive: true }) + await fs.writeFile(config, JSON.stringify({ port: listener.port })) + await fs.mkdir(path.dirname(registration), { recursive: true }) + await fs.writeFile( + registration, + JSON.stringify({ + id: "stale", + version: InstallationVersion, + url: "http://127.0.0.1:1", + pid: 2_147_483_647, + password: "stale", + }), + ) + const contender = Bun.spawn([process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"], { + env: serviceEnv(root), + stderr: "pipe", + stdout: "ignore", + }) + + try { + expect(await Promise.race([recognizing.promise.then(() => true), Bun.sleep(20_000).then(() => false)])).toBe(true) + await Bun.sleep(8_000) + const info = { + id: "incumbent", + version: InstallationVersion, + url: `http://127.0.0.1:${listener.port}`, + pid: process.pid, + password: "incumbent", + } + await fs.writeFile(registration, JSON.stringify(info)) + + expect(await Promise.race([contender.exited, Bun.sleep(20_000).then(() => undefined)])).toBe(0) + expect(await Bun.file(registration).json()).toEqual(info) + } finally { + contender.kill("SIGTERM") + await contender.exited + await fs.rm(root, { recursive: true, force: true }) + } +}, 45_000) + +test("stale dead registration is replaced after binding the selected port", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-stale-")) + const port = await availablePort() + const registration = path.join(root, "state", "opencode", "service-local.json") + await fs.mkdir(path.join(root, "config", "opencode"), { recursive: true }) + await fs.mkdir(path.dirname(registration), { recursive: true }) + await fs.writeFile(path.join(root, "config", "opencode", "service-local.json"), JSON.stringify({ port })) + await fs.writeFile( + registration, + JSON.stringify({ id: "dead", version: "dead", url: `http://127.0.0.1:${port}`, pid: 2_147_483_647 }), + ) + const owner = Bun.spawn([process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"], { + env: serviceEnv(root), + stderr: "pipe", + stdout: "ignore", + }) + try { + const info = await waitForInfo(registration, (value) => value.id !== "dead") + expect(new URL(info.url).port).toBe(String(port)) + expect(info.pid).toBe(owner.pid) + await Effect.runPromise(Service.stop({ file: registration }).pipe(Effect.provide(NodeFileSystem.layer))) + await owner.exited + } finally { + owner.kill("SIGTERM") + await owner.exited + await fs.rm(root, { recursive: true, force: true }) + } +}, 30_000) + +test("a failed service stays registered and owns the selected port until stopped", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-failed-")) + const database = path.join(root, "database") + await fs.mkdir(database) + const env = { + ...process.env, + HOME: root, + OPENCODE_DB: database, + OPENCODE_TEST_HOME: root, + XDG_CACHE_HOME: path.join(root, "cache"), + XDG_CONFIG_HOME: path.join(root, "config"), + XDG_DATA_HOME: path.join(root, "data"), + XDG_STATE_HOME: path.join(root, "state"), + } + const command = [process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"] + const registration = path.join(root, "state", "opencode", "service-local.json") + const owner = Bun.spawn(command, { env, stderr: "pipe", stdout: "ignore" }) + + try { + const info = await waitForInfo(registration) + await waitForFailed(info) + expect(owner.exitCode).toBe(null) + + const contender = Bun.spawn(command, { env, stderr: "pipe", stdout: "ignore" }) + expect(await Promise.race([contender.exited.then(() => true), Bun.sleep(10_000).then(() => false)])).toBe(true) + expect(contender.exitCode).toBe(0) + expect((await waitForInfo(registration)).id).toBe(info.id) + expect(owner.exitCode).toBe(null) + + await Effect.runPromise(Service.stop({ file: registration }).pipe(Effect.provide(NodeFileSystem.layer))) + await owner.exited + expect(await Bun.file(registration).exists()).toBe(false) + } finally { + owner.kill("SIGTERM") + await owner.exited + await fs.rm(root, { recursive: true, force: true }) + } +}, 30_000) + +function withDatabase(file: string, effect: Effect.Effect) { + return Effect.runPromise(effect.pipe(Effect.provide(Database.layerFromPath(file)), Effect.scoped)) +} + +function waitForExecutionStart(file: string, sessionID: SessionV2.ID) { + return withDatabase( + file, + Effect.gen(function* () { + const { db } = yield* Database.Service + return yield* db + .select({ id: EventTable.id, sessionID: EventTable.aggregate_id, type: EventTable.type }) + .from(EventTable) + .all() + .pipe( + Effect.orDie, + Effect.map((rows) => + rows.filter( + (row) => + row.sessionID === sessionID && + row.type === + EventV2.versionedType( + SessionEvent.Execution.Started.type, + SessionEvent.Execution.Started.durable.version, + ), + ), + ), + Effect.filterOrFail((rows) => rows.length > 0), + Effect.map((rows) => rows.length), + Effect.retry(Schedule.max([Schedule.spaced("50 millis"), Schedule.recurs(200)])), + ) + }), + ) +} + +async function waitForInfo(file: string, accept: (info: Info) => boolean = () => true) { + for (let attempt = 0; attempt < 400; attempt++) { + const value = await Bun.file(file) + .json() + .catch(() => undefined) + if (value !== undefined) { + const info = await Schema.decodeUnknownPromise(Service.Info)(value) + if (accept(info)) return info + } + await Bun.sleep(50) + } + throw new Error("Timed out waiting for service registration") +} + +async function waitForFailed(info: Info) { + for (let attempt = 0; attempt < 400; attempt++) { + const status = await fetch(new URL("/api/health", info.url), { + headers: { authorization: "Basic " + btoa(`opencode:${info.password}`) }, + }) + .then((response) => response.status) + .catch(() => undefined) + if (status === 500) return + await Bun.sleep(50) + } + throw new Error("Timed out waiting for service boot failure") +} + +async function availablePort() { + const server = Bun.serve({ port: 0, fetch: () => new Response() }) + const port = server.port + await server.stop(true) + if (port === undefined) throw new Error("Server did not bind a port") + return port +} + +function serviceEnv(root: string) { + return { + ...process.env, + HOME: root, + OPENCODE_DB: path.join(root, "opencode.db"), + OPENCODE_TEST_HOME: root, + XDG_CACHE_HOME: path.join(root, "cache"), + XDG_CONFIG_HOME: path.join(root, "config"), + XDG_DATA_HOME: path.join(root, "data"), + XDG_STATE_HOME: path.join(root, "state"), + } +} + +async function startManagedService(prefix: string, failBoot = false) { + const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix)) + const port = await availablePort() + const registration = path.join(root, "state", "opencode", "service-local.json") + await fs.mkdir(path.join(root, "config", "opencode"), { recursive: true }) + if (failBoot) await fs.mkdir(path.join(root, "database")) + await fs.writeFile(path.join(root, "config", "opencode", "service-local.json"), JSON.stringify({ port })) + const owner = Bun.spawn([process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"], { + env: failBoot ? { ...serviceEnv(root), OPENCODE_DB: path.join(root, "database") } : serviceEnv(root), + stderr: "pipe", + stdout: "ignore", + }) + const info = await waitForInfo(registration).catch(async (cause) => { + owner.kill("SIGTERM") + await owner.exited + await fs.rm(root, { recursive: true, force: true }) + throw cause + }) + return { root, port, registration, owner, info } +} + +async function stopManagedService(service: Awaited>) { + service.owner.kill("SIGTERM") + await service.owner.exited + await fs.rm(service.root, { recursive: true, force: true }) +} + +function waitForExit(process: Bun.Subprocess, timeout = 10_000) { + return Promise.race([process.exited.then(() => true), Bun.sleep(timeout).then(() => false)]) +} + +async function expectPortAvailable(port: number) { + const server = Bun.serve({ hostname: "127.0.0.1", port, fetch: () => new Response() }) + await server.stop(true) +} diff --git a/packages/cli/test/standalone.test.ts b/packages/cli/test/standalone.test.ts new file mode 100644 index 0000000000..3cfe25855f --- /dev/null +++ b/packages/cli/test/standalone.test.ts @@ -0,0 +1,66 @@ +import { expect, test } from "bun:test" +import path from "node:path" + +test("standalone server exits when its owner is killed", async () => { + const owner = Bun.spawn([process.execPath, path.join(import.meta.dir, "fixture/standalone-owner.ts")], { + cwd: path.join(import.meta.dir, ".."), + env: { ...process.env, OPENCODE_SERVER_USERNAME: "custom" }, + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + }) + const line = await Promise.race([readLine(owner.stdout), Bun.sleep(10_000).then(() => undefined)]) + const [rawPID, url, status] = line?.split(" ") ?? [] + const pid = Number(rawPID) + + try { + expect(pid).toBeGreaterThan(0) + expect(url).toStartWith("http://127.0.0.1:") + expect(status).toBe("200") + expect(running(pid)).toBe(true) + + owner.kill("SIGKILL") + await owner.exited + + expect(await waitForExit(pid)).toBe(true) + } finally { + owner.kill("SIGKILL") + if (running(pid)) process.kill(pid, "SIGKILL") + } +}) + +async function readLine(stream: ReadableStream) { + const reader = stream.getReader() + const decoder = new TextDecoder() + const chunks: string[] = [] + while (true) { + const result = await reader.read() + if (result.done) break + chunks.push(decoder.decode(result.value, { stream: true })) + const output = chunks.join("") + const newline = output.indexOf("\n") + if (newline !== -1) { + reader.releaseLock() + return output.slice(0, newline) + } + } + reader.releaseLock() + return chunks.join("") + decoder.decode() +} + +async function waitForExit(pid: number, attempts = 100): Promise { + if (!running(pid)) return true + if (attempts === 0) return false + await Bun.sleep(50) + return waitForExit(pid, attempts - 1) +} + +function running(pid: number) { + if (!Number.isSafeInteger(pid) || pid <= 0) return false + try { + process.kill(pid, 0) + return true + } catch { + return false + } +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index ac9f4c63f7..0a60335b40 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -6,5 +6,6 @@ "jsxImportSource": "@opentui/solid", "lib": ["ESNext", "DOM", "DOM.Iterable"], "noUncheckedIndexedAccess": false - } + }, + "exclude": ["dist", "dist-node"] } diff --git a/packages/cli/vite.node.config.ts b/packages/cli/vite.node.config.ts new file mode 100644 index 0000000000..791a0131ac --- /dev/null +++ b/packages/cli/vite.node.config.ts @@ -0,0 +1,220 @@ +import path from "node:path" +import { readFile } from "node:fs/promises" +import { createRequire } from "node:module" +import { defineConfig, type Plugin, type UserConfig } from "vite" +import solid from "vite-plugin-solid" +import { nodeExecArgv, nodeTarget, type NodeTarget, photonWasmAsset } from "./src/node/target" + +const dir = import.meta.dirname + +function rawTextPlugin(): Plugin { + return { + name: "opencode:raw-text", + async load(id) { + if (!id.endsWith(".md")) return + return `export default ${JSON.stringify(await readFile(id, "utf8"))}` + }, + } +} + +function runtimeRequirePlugin(): Plugin { + return { + name: "opencode:runtime-require", + enforce: "pre", + transform(code, id) { + if (!id.endsWith("turndown/lib/turndown.es.js")) return + const transformed = code.replace(" var domino = require('@mixmark-io/domino');", "") + if (transformed === code) this.error("Failed to rewrite Turndown's Domino require") + return `import domino from "@mixmark-io/domino"\n${transformed}` + }, + } +} + +const resolve = { + alias: [ + { find: /^solid-js\/store$/, replacement: "solid-js/store/dist/store.js" }, + { find: /^solid-js$/, replacement: "solid-js/dist/solid.js" }, + { + find: /^ws$/, + replacement: path.join(path.dirname(createRequire(import.meta.url).resolve("ws/package.json")), "wrapper.mjs"), + }, + ], + conditions: ["node"], +} + +const output = (entryFileNames: string, banner?: string) => ({ + format: "esm" as const, + entryFileNames, + inlineDynamicImports: true, + banner, +}) + +function nodePrelude(input: NodeBuildInput) { + const nodePtySpawnHelper = + input.target.platform === "darwin" + ? `${input.target.nodePtyPackage}/prebuilds/darwin-${input.target.arch}/spawn-helper` + : undefined + const promiseModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.promise")] +if (!sdk) throw new Error("OpenCode Promise plugin SDK is unavailable") +export const Agent = sdk.Agent +export const Command = sdk.Command +export const Connection = sdk.Connection +export const Credential = sdk.Credential +export const Integration = sdk.Integration +export const Model = sdk.Model +export const Plugin = sdk.Plugin +export const Provider = sdk.Provider +export const Reference = sdk.Reference +export const Skill = sdk.Skill` + const effectModule = promiseModule + .replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect") + .replace("Promise plugin", "Effect plugin") + const promisePluginModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.promise")] +if (!sdk) throw new Error("OpenCode Promise plugin SDK is unavailable") +export const define = sdk.Plugin.define` + const effectPluginModule = promisePluginModule + .replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect") + .replace("Promise plugin", "Effect plugin") + const effectToolModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.effect")] +if (!sdk) throw new Error("OpenCode Effect plugin SDK is unavailable") +export const Tool = sdk.Tool +export const Failure = sdk.Tool.Failure +export const RegistrationError = sdk.Tool.RegistrationError +export const make = sdk.Tool.make +export const validateName = sdk.Tool.validateName +export const registrationEntries = sdk.Tool.registrationEntries +export const withPermission = sdk.Tool.withPermission +export const permission = sdk.Tool.permission +export const definition = sdk.Tool.definition +export const settle = sdk.Tool.settle` + return `#!/usr/bin/env -S node ${nodeExecArgv.join(" ")} +import __cjs_mod__ from "node:module" +import { chmodSync as __ocChmod, existsSync as __ocExists, lstatSync as __ocLstat, mkdirSync as __ocMkdir, renameSync as __ocRename, rmSync as __ocRm, writeFileSync as __ocWrite } from "node:fs" +import { tmpdir as __ocTmpdir } from "node:os" +import __ocPath from "node:path" +import { getAssetKeys as __ocAssetKeys, getRawAsset as __ocRawAsset, isSea as __ocIsSea } from "node:sea" +import { fileURLToPath as __ocFileURLToPath } from "node:url" +const __filename = import.meta.filename +const __dirname = import.meta.dirname +const require = __cjs_mod__.createRequire(import.meta.url) +const __ocPluginModules = ${JSON.stringify({ + "@opencode-ai/plugin/v2": "opencode:plugin-v2", + "@opencode-ai/plugin/v2/plugin": "opencode:plugin-v2-plugin", + "@opencode-ai/plugin/v2/effect": "opencode:plugin-v2-effect", + "@opencode-ai/plugin/v2/effect/plugin": "opencode:plugin-v2-effect-plugin", + "@opencode-ai/plugin/v2/effect/tool": "opencode:plugin-v2-effect-tool", + })} +const __ocPluginSources = ${JSON.stringify({ + "opencode:plugin-v2": promiseModule, + "opencode:plugin-v2-plugin": promisePluginModule, + "opencode:plugin-v2-effect": effectModule, + "opencode:plugin-v2-effect-plugin": effectPluginModule, + "opencode:plugin-v2-effect-tool": effectToolModule, + })} +__cjs_mod__.registerHooks({ + resolve(__ocSpecifier, __ocContext, __ocNextResolve) { + const __ocUrl = __ocPluginModules[__ocSpecifier] + return __ocUrl ? { url: __ocUrl, shortCircuit: true } : __ocNextResolve(__ocSpecifier, __ocContext) + }, + load(__ocUrl, __ocContext, __ocNextLoad) { + const __ocSource = __ocPluginSources[__ocUrl] + return __ocSource + ? { format: "module", source: __ocSource, shortCircuit: true } + : __ocNextLoad(__ocUrl, __ocContext) + }, +}) +const __ocUid = typeof process.getuid === "function" ? process.getuid() : undefined +const __ocCacheRoot = __ocPath.join(__ocTmpdir(), \`opencode-node-\${__ocUid ?? "user"}\`) +if (__ocIsSea()) { + try { + __ocMkdir(__ocCacheRoot, { mode: 0o700 }) + } catch (__ocError) { + if (!__ocExists(__ocCacheRoot)) throw __ocError + } + const __ocCacheInfo = __ocLstat(__ocCacheRoot) + if (!__ocCacheInfo.isDirectory() || __ocCacheInfo.isSymbolicLink()) throw new Error("Unsafe Node asset cache path") + if (__ocUid !== undefined && __ocCacheInfo.uid !== __ocUid) throw new Error("Node asset cache is owned by another user") + if (__ocUid !== undefined) __ocChmod(__ocCacheRoot, 0o700) +} +const __ocAssetRoot = __ocIsSea() + ? __ocPath.join(__ocCacheRoot, ${JSON.stringify(`${input.assetHash}-${input.target.platform}-${input.target.arch}`)}) + : __ocFileURLToPath(new URL("./assets/", import.meta.url)) +if (__ocIsSea()) { + for (const __ocKey of __ocAssetKeys()) { + const __ocTarget = __ocPath.join(__ocAssetRoot, __ocKey) + if (__ocExists(__ocTarget)) continue + __ocMkdir(__ocPath.dirname(__ocTarget), { recursive: true }) + const __ocTemporary = \`${"${__ocTarget}"}.${"${process.pid}"}.${"${crypto.randomUUID()}"}.tmp\` + __ocWrite(__ocTemporary, new Uint8Array(__ocRawAsset(__ocKey))) + try { + __ocRename(__ocTemporary, __ocTarget) + } catch (__ocError) { + __ocRm(__ocTemporary, { force: true }) + if (!__ocExists(__ocTarget)) throw __ocError + } + } + const __ocPtySpawnHelper = ${JSON.stringify(nodePtySpawnHelper)} + if (__ocPtySpawnHelper) __ocChmod(__ocPath.join(__ocAssetRoot, __ocPtySpawnHelper), 0o755) +} +process.env.OPENCODE_NODE_ASSETS_DIR = __ocAssetRoot +process.env.OTUI_ASSET_ROOT = __ocAssetRoot +process.env.OPENCODE_NODE_PTY_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(input.target.nodePtyEntryAsset)}) +process.env.OPENCODE_PARCEL_WATCHER_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(input.target.parcelWatcherAsset)}) +process.env.OPENCODE_PHOTON_WASM_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(photonWasmAsset)}) +globalThis.__OPENCODE_PHOTON_WASM_PATH = process.env.OPENCODE_PHOTON_WASM_PATH +if (process.platform === "linux") process.env.OPENTUI_LIBC = "glibc"` +} + +export type NodeBuildInput = { + readonly version: string + readonly channel: string + readonly models: string + readonly assetHash: string + readonly target: NodeTarget +} + +export function mainConfig(input: NodeBuildInput): UserConfig { + return defineConfig({ + root: dir, + plugins: [ + rawTextPlugin(), + runtimeRequirePlugin(), + solid({ + solid: { + generate: "universal", + moduleName: "@opentui/solid", + }, + }), + ], + resolve, + esbuild: { jsx: "automatic" }, + define: { + OPENCODE_VERSION: JSON.stringify(input.version), + OPENCODE_CLI_NAME: JSON.stringify("opencode2-node"), + OPENCODE_MODELS_DEV: input.models, + OPENCODE_CHANNEL: JSON.stringify(input.channel), + OPENCODE_LIBC: input.target.platform === "linux" ? JSON.stringify("glibc") : "undefined", + FFF_LIBC: input.target.platform === "linux" ? JSON.stringify("gnu") : "undefined", + }, + ssr: { noExternal: true }, + build: { + ssr: "src/node/index.ts", + target: "node26", + outDir: "dist-node", + emptyOutDir: false, + minify: true, + rollupOptions: { + external: [/^@opencode-ai\/simulation(?:\/|$)/], + output: output("opencode.mjs", nodePrelude(input)), + }, + }, + }) +} + +export default mainConfig({ + version: process.env.OPENCODE_VERSION ?? "local", + channel: process.env.OPENCODE_CHANNEL ?? "local", + models: "undefined", + assetHash: "local", + target: nodeTarget(process.platform, process.arch), +}) diff --git a/packages/client/package.json b/packages/client/package.json index 4f2445ca9d..ffb6458d84 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,16 +1,33 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/client", - "private": true, + "version": "1.17.13", "type": "module", "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/anomalyco/opencode.git", + "directory": "packages/client" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], "exports": { - ".": "./src/index.ts", - "./effect": "./src/effect.ts" + ".": "./src/promise/index.ts", + "./promise": "./src/promise/index.ts", + "./promise/api": "./src/promise/api.ts", + "./service": "./src/promise/service.ts", + "./effect": "./src/effect/index.ts", + "./effect/api": "./src/effect/api.ts", + "./effect/service": "./src/effect/service.ts" }, "scripts": { + "build": "bun run script/build-package.ts", "generate": "bun run script/build.ts", - "check:generated": "bun run generate && git diff --exit-code -- src/generated src/generated-effect", + "check:generated": "bun run generate && git diff --exit-code -- src/promise/generated src/effect/generated src/effect/api", "test": "bun test --timeout 5000", "typecheck": "tsgo --noEmit" }, @@ -19,7 +36,7 @@ "@opencode-ai/protocol": "workspace:*" }, "peerDependencies": { - "effect": "4.0.0-beta.83" + "effect": "4.0.0-beta.98" }, "peerDependenciesMeta": { "effect": { @@ -28,9 +45,7 @@ }, "devDependencies": { "@effect/platform-node": "catalog:", - "@opencode-ai/core": "workspace:*", "@opencode-ai/httpapi-codegen": "workspace:*", - "@opencode-ai/server": "workspace:*", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@typescript/native-preview": "catalog:", diff --git a/packages/client/script/build-package.ts b/packages/client/script/build-package.ts new file mode 100644 index 0000000000..323a63ddf9 --- /dev/null +++ b/packages/client/script/build-package.ts @@ -0,0 +1,9 @@ +#!/usr/bin/env bun + +import { $ } from "bun" +import { fileURLToPath } from "node:url" + +process.chdir(fileURLToPath(new URL("..", import.meta.url))) + +await $`rm -rf dist` +await $`bun tsc -p tsconfig.build.json` diff --git a/packages/client/script/build.ts b/packages/client/script/build.ts index aeec4b3e34..8e432b0a17 100644 --- a/packages/client/script/build.ts +++ b/packages/client/script/build.ts @@ -1,30 +1,35 @@ import { NodeFileSystem } from "@effect/platform-node" -import { compile, emitEffectImported, emitPromise, write } from "@opencode-ai/httpapi-codegen" -import { ClientApi, endpointNames, groupNames, omitEndpoints } from "../src/contract" +import { compile, emitEffectImported, emitEffectShape, emitPromise, write } from "@opencode-ai/httpapi-codegen" +import { + ClientApi, + effectOmitEndpoints, + groupNames, + promiseOmitEndpoints, +} from "@opencode-ai/protocol/client" import { Effect } from "effect" import { fileURLToPath } from "url" -const contract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints }) +const promiseContract = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints }) +const effectContract = compile(ClientApi, { groupNames, omitEndpoints: effectOmitEndpoints }) await Effect.runPromise( Effect.all( [ write( - emitPromise(contract, { - outputTypes: { - "events.subscribe": { - name: "OpenCodeEventEncoded", - import: 'import type { OpenCodeEventEncoded } from "@opencode-ai/protocol/groups/event"', - }, - }, + emitPromise(promiseContract, { + mutableOutputs: true, }), - fileURLToPath(new URL("../src/generated", import.meta.url)), + fileURLToPath(new URL("../src/promise/generated", import.meta.url)), ), write( - emitEffectImported(contract, { module: "../contract", api: "ClientApi" }), - fileURLToPath(new URL("../src/generated-effect", import.meta.url)), + emitEffectImported(effectContract, { module: "../../contract", api: "ClientApi" }), + fileURLToPath(new URL("../src/effect/generated", import.meta.url)), + ), + write( + emitEffectShape(effectContract, { module: "../../contract", api: "ClientApi" }), + fileURLToPath(new URL("../src/effect/api", import.meta.url)), ), ], - { concurrency: 2, discard: true }, + { concurrency: 3, discard: true }, ).pipe(Effect.provide(NodeFileSystem.layer)), ) diff --git a/packages/client/script/publish.ts b/packages/client/script/publish.ts new file mode 100644 index 0000000000..8e37674b64 --- /dev/null +++ b/packages/client/script/publish.ts @@ -0,0 +1,45 @@ +#!/usr/bin/env bun + +import { Script } from "@opencode-ai/script" +import { $ } from "bun" +import { rm } from "node:fs/promises" +import { fileURLToPath } from "node:url" + +process.chdir(fileURLToPath(new URL("..", import.meta.url))) + +const originalText = await Bun.file("package.json").text() +const pkg = JSON.parse(originalText) as { + name: string + version: string + exports: Record +} +const tarball = `${pkg.name.replace("@", "").replace("/", "-")}-${pkg.version}.tgz` + +if ((await $`npm view ${pkg.name}@${pkg.version} version`.nothrow()).exitCode === 0) { + console.log(`already published ${pkg.name}@${pkg.version}`) + process.exit(0) +} + +try { + await $`bun run typecheck` + await $`bun run build` + pkg.exports = Object.fromEntries( + Object.entries(pkg.exports).map(([key, value]) => { + if (typeof value !== "string") return [key, value] + return [ + key, + { + import: value.replace("./src/", "./dist/").replace(/\.ts$/, ".js"), + types: value.replace("./src/", "./dist/").replace(/\.ts$/, ".d.ts"), + }, + ] + }), + ) + await Bun.write("package.json", JSON.stringify(pkg, null, 2) + "\n") + await rm(tarball, { force: true }) + await $`bun pm pack` + await $`npm publish ${tarball} --tag ${Script.channel} --access public` +} finally { + await Bun.write("package.json", originalText) + await rm(tarball, { force: true }) +} diff --git a/packages/client/src/contract.ts b/packages/client/src/contract.ts index 413fea9dc3..8fd9994f5c 100644 --- a/packages/client/src/contract.ts +++ b/packages/client/src/contract.ts @@ -1,53 +1,6 @@ -import { makeDefaultApi } from "@opencode-ai/protocol/api" -import { InvalidRequestError, SessionNotFoundError } from "@opencode-ai/protocol/errors" -import { HttpApiMiddleware } from "effect/unstable/httpapi" - -class LocationMiddleware extends HttpApiMiddleware.Service()( - "@opencode-ai/client/LocationMiddleware", -) {} - -class SessionLocationMiddleware extends HttpApiMiddleware.Service()( - "@opencode-ai/client/SessionLocationMiddleware", - { error: [InvalidRequestError, SessionNotFoundError] }, -) {} - -export const ClientApi = makeDefaultApi({ - locationMiddleware: LocationMiddleware, - sessionLocationMiddleware: SessionLocationMiddleware, -}) - -export const groupNames = { - "server.health": "health", - "server.location": "location", - "server.agent": "agents", - "server.session": "sessions", - "server.message": "messages", - "server.model": "models", - "server.provider": "providers", - "server.integration": "integrations", - "server.credential": "credentials", - "server.permission": "permissions", - "server.fs": "files", - "server.command": "commands", - "server.skill": "skills", - "server.event": "events", - "server.pty": "ptys", - "server.question": "questions", - "server.reference": "references", - "server.projectCopy": "projectCopies", -} as const - -export const endpointNames = { - "session.messages": "list", - "integration.connect.key": "connectKey", - "integration.connect.oauth": "connectOauth", - "integration.attempt.status": "attemptStatus", - "integration.attempt.complete": "attemptComplete", - "integration.attempt.cancel": "attemptCancel", - "permission.request.list": "listRequests", - "permission.saved.list": "listSaved", - "permission.saved.remove": "removeSaved", - "question.request.list": "listRequests", -} as const - -export const omitEndpoints = new Set(["fs.read", "pty.connect", "pty.connectToken"]) +export { + ClientApi, + effectOmitEndpoints, + groupNames, + promiseOmitEndpoints, +} from "@opencode-ai/protocol/client" diff --git a/packages/client/src/effect/api.ts b/packages/client/src/effect/api.ts new file mode 100644 index 0000000000..e7cb2012f5 --- /dev/null +++ b/packages/client/src/effect/api.ts @@ -0,0 +1,8 @@ +import type { ModelApi, ProviderApi } from "./api/api.js" + +export type * from "./api/api.js" + +export interface CatalogApi { + readonly provider: ProviderApi + readonly model: ModelApi +} diff --git a/packages/client/src/effect/api/.httpapi-codegen.json b/packages/client/src/effect/api/.httpapi-codegen.json new file mode 100644 index 0000000000..d1d5afdb85 --- /dev/null +++ b/packages/client/src/effect/api/.httpapi-codegen.json @@ -0,0 +1,3 @@ +[ + "api.ts" +] diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts new file mode 100644 index 0000000000..2aee13e673 --- /dev/null +++ b/packages/client/src/effect/api/api.ts @@ -0,0 +1,1036 @@ +// Generated by @opencode-ai/httpapi-codegen. Do not edit. +import type { Effect, Stream } from "effect" +import type { HttpApiClient } from "effect/unstable/httpapi" +import type { ClientApi } from "../../contract" + +type RawClient = HttpApiClient.ForApi +type EffectValue = A extends Effect.Effect ? Success : never +type StreamValue = A extends Stream.Stream ? Success : never + +export type Endpoint0_0Output = EffectValue> +export type HealthGetOperation = () => Effect.Effect + +type Endpoint0_1Request = Parameters[0] +export type Endpoint0_1Input = { readonly instanceID: Endpoint0_1Request["payload"]["instanceID"] } +export type Endpoint0_1Output = EffectValue> +export type HealthStopOperation = (input: Endpoint0_1Input) => Effect.Effect + +export interface HealthApi { + readonly get: HealthGetOperation + readonly stop: HealthStopOperation +} + +export type Endpoint1_0Output = EffectValue> +export type ServerGetOperation = () => Effect.Effect + +export interface ServerApi { + readonly get: ServerGetOperation +} + +type Endpoint2_0Request = Parameters[0] +export type Endpoint2_0Input = { readonly location?: Endpoint2_0Request["query"]["location"] } +export type Endpoint2_0Output = EffectValue> +export type LocationGetOperation = (input?: Endpoint2_0Input) => Effect.Effect + +export interface LocationApi { + readonly get: LocationGetOperation +} + +type Endpoint3_0Request = Parameters[0] +export type Endpoint3_0Input = { readonly location?: Endpoint3_0Request["query"]["location"] } +export type Endpoint3_0Output = EffectValue> +export type AgentListOperation = (input?: Endpoint3_0Input) => Effect.Effect + +export interface AgentApi { + readonly list: AgentListOperation +} + +type Endpoint4_0Request = Parameters[0] +export type Endpoint4_0Input = { readonly location?: Endpoint4_0Request["query"]["location"] } +export type Endpoint4_0Output = EffectValue> +export type PluginListOperation = (input?: Endpoint4_0Input) => Effect.Effect + +export interface PluginApi { + readonly list: PluginListOperation +} + +type Endpoint5_0Request = Parameters[0] +export type Endpoint5_0Input = { + readonly workspace?: Endpoint5_0Request["query"]["workspace"] + readonly limit?: Endpoint5_0Request["query"]["limit"] + readonly order?: Endpoint5_0Request["query"]["order"] + readonly search?: Endpoint5_0Request["query"]["search"] + readonly parentID?: Endpoint5_0Request["query"]["parentID"] + readonly directory?: Endpoint5_0Request["query"]["directory"] + readonly project?: Endpoint5_0Request["query"]["project"] + readonly subpath?: Endpoint5_0Request["query"]["subpath"] + readonly cursor?: Endpoint5_0Request["query"]["cursor"] +} +export type Endpoint5_0Output = EffectValue> +export type SessionListOperation = (input?: Endpoint5_0Input) => Effect.Effect + +type Endpoint5_1Request = Parameters[0] +export type Endpoint5_1Input = { + readonly id?: Endpoint5_1Request["payload"]["id"] + readonly agent?: Endpoint5_1Request["payload"]["agent"] + readonly model?: Endpoint5_1Request["payload"]["model"] + readonly location?: Endpoint5_1Request["payload"]["location"] +} +export type Endpoint5_1Output = EffectValue>["data"] +export type SessionCreateOperation = (input?: Endpoint5_1Input) => Effect.Effect + +export type Endpoint5_2Output = EffectValue>["data"] +export type SessionActiveOperation = () => Effect.Effect + +type Endpoint5_3Request = Parameters[0] +export type Endpoint5_3Input = { readonly sessionID: Endpoint5_3Request["params"]["sessionID"] } +export type Endpoint5_3Output = EffectValue>["data"] +export type SessionGetOperation = (input: Endpoint5_3Input) => Effect.Effect + +type Endpoint5_4Request = Parameters[0] +export type Endpoint5_4Input = { readonly sessionID: Endpoint5_4Request["params"]["sessionID"] } +export type Endpoint5_4Output = EffectValue> +export type SessionRemoveOperation = (input: Endpoint5_4Input) => Effect.Effect + +type Endpoint5_5Request = Parameters[0] +export type Endpoint5_5Input = { + readonly sessionID: Endpoint5_5Request["params"]["sessionID"] + readonly messageID?: Endpoint5_5Request["payload"]["messageID"] +} +export type Endpoint5_5Output = EffectValue>["data"] +export type SessionForkOperation = (input: Endpoint5_5Input) => Effect.Effect + +type Endpoint5_6Request = Parameters[0] +export type Endpoint5_6Input = { + readonly sessionID: Endpoint5_6Request["params"]["sessionID"] + readonly agent: Endpoint5_6Request["payload"]["agent"] +} +export type Endpoint5_6Output = EffectValue> +export type SessionSwitchAgentOperation = (input: Endpoint5_6Input) => Effect.Effect + +type Endpoint5_7Request = Parameters[0] +export type Endpoint5_7Input = { + readonly sessionID: Endpoint5_7Request["params"]["sessionID"] + readonly model: Endpoint5_7Request["payload"]["model"] +} +export type Endpoint5_7Output = EffectValue> +export type SessionSwitchModelOperation = (input: Endpoint5_7Input) => Effect.Effect + +type Endpoint5_8Request = Parameters[0] +export type Endpoint5_8Input = { + readonly sessionID: Endpoint5_8Request["params"]["sessionID"] + readonly title: Endpoint5_8Request["payload"]["title"] +} +export type Endpoint5_8Output = EffectValue> +export type SessionRenameOperation = (input: Endpoint5_8Input) => Effect.Effect + +type Endpoint5_9Request = Parameters[0] +export type Endpoint5_9Input = { + readonly sessionID: Endpoint5_9Request["params"]["sessionID"] + readonly directory: Endpoint5_9Request["payload"]["directory"] + readonly workspaceID?: Endpoint5_9Request["payload"]["workspaceID"] +} +export type Endpoint5_9Output = EffectValue> +export type SessionMoveOperation = (input: Endpoint5_9Input) => Effect.Effect + +type Endpoint5_10Request = Parameters[0] +export type Endpoint5_10Input = { + readonly sessionID: Endpoint5_10Request["params"]["sessionID"] + readonly id?: Endpoint5_10Request["payload"]["id"] + readonly text: Endpoint5_10Request["payload"]["text"] + readonly files?: Endpoint5_10Request["payload"]["files"] + readonly agents?: Endpoint5_10Request["payload"]["agents"] + readonly metadata?: Endpoint5_10Request["payload"]["metadata"] + readonly delivery?: Endpoint5_10Request["payload"]["delivery"] + readonly resume?: Endpoint5_10Request["payload"]["resume"] +} +export type Endpoint5_10Output = EffectValue>["data"] +export type SessionPromptOperation = (input: Endpoint5_10Input) => Effect.Effect + +type Endpoint5_11Request = Parameters[0] +export type Endpoint5_11Input = { + readonly sessionID: Endpoint5_11Request["params"]["sessionID"] + readonly id?: Endpoint5_11Request["payload"]["id"] + readonly command: Endpoint5_11Request["payload"]["command"] + readonly arguments?: Endpoint5_11Request["payload"]["arguments"] + readonly agent?: Endpoint5_11Request["payload"]["agent"] + readonly model?: Endpoint5_11Request["payload"]["model"] + readonly files?: Endpoint5_11Request["payload"]["files"] + readonly agents?: Endpoint5_11Request["payload"]["agents"] + readonly delivery?: Endpoint5_11Request["payload"]["delivery"] + readonly resume?: Endpoint5_11Request["payload"]["resume"] +} +export type Endpoint5_11Output = EffectValue>["data"] +export type SessionCommandOperation = (input: Endpoint5_11Input) => Effect.Effect + +type Endpoint5_12Request = Parameters[0] +export type Endpoint5_12Input = { + readonly sessionID: Endpoint5_12Request["params"]["sessionID"] + readonly id?: Endpoint5_12Request["payload"]["id"] + readonly skill: Endpoint5_12Request["payload"]["skill"] + readonly resume?: Endpoint5_12Request["payload"]["resume"] +} +export type Endpoint5_12Output = EffectValue> +export type SessionSkillOperation = (input: Endpoint5_12Input) => Effect.Effect + +type Endpoint5_13Request = Parameters[0] +export type Endpoint5_13Input = { + readonly sessionID: Endpoint5_13Request["params"]["sessionID"] + readonly id?: Endpoint5_13Request["payload"]["id"] + readonly text: Endpoint5_13Request["payload"]["text"] + readonly description?: Endpoint5_13Request["payload"]["description"] + readonly metadata?: Endpoint5_13Request["payload"]["metadata"] + readonly delivery?: Endpoint5_13Request["payload"]["delivery"] + readonly resume?: Endpoint5_13Request["payload"]["resume"] +} +export type Endpoint5_13Output = EffectValue>["data"] +export type SessionSyntheticOperation = (input: Endpoint5_13Input) => Effect.Effect + +type Endpoint5_14Request = Parameters[0] +export type Endpoint5_14Input = { + readonly sessionID: Endpoint5_14Request["params"]["sessionID"] + readonly id?: Endpoint5_14Request["payload"]["id"] + readonly command: Endpoint5_14Request["payload"]["command"] +} +export type Endpoint5_14Output = EffectValue> +export type SessionShellOperation = (input: Endpoint5_14Input) => Effect.Effect + +type Endpoint5_15Request = Parameters[0] +export type Endpoint5_15Input = { + readonly sessionID: Endpoint5_15Request["params"]["sessionID"] + readonly id?: Endpoint5_15Request["payload"]["id"] +} +export type Endpoint5_15Output = EffectValue>["data"] +export type SessionCompactOperation = (input: Endpoint5_15Input) => Effect.Effect + +type Endpoint5_16Request = Parameters[0] +export type Endpoint5_16Input = { readonly sessionID: Endpoint5_16Request["params"]["sessionID"] } +export type Endpoint5_16Output = EffectValue> +export type SessionWaitOperation = (input: Endpoint5_16Input) => Effect.Effect + +type Endpoint5_17Request = Parameters[0] +export type Endpoint5_17Input = { + readonly sessionID: Endpoint5_17Request["params"]["sessionID"] + readonly messageID: Endpoint5_17Request["payload"]["messageID"] + readonly files?: Endpoint5_17Request["payload"]["files"] +} +export type Endpoint5_17Output = EffectValue>["data"] +export type SessionRevertStageOperation = (input: Endpoint5_17Input) => Effect.Effect + +type Endpoint5_18Request = Parameters[0] +export type Endpoint5_18Input = { readonly sessionID: Endpoint5_18Request["params"]["sessionID"] } +export type Endpoint5_18Output = EffectValue> +export type SessionRevertClearOperation = (input: Endpoint5_18Input) => Effect.Effect + +type Endpoint5_19Request = Parameters[0] +export type Endpoint5_19Input = { readonly sessionID: Endpoint5_19Request["params"]["sessionID"] } +export type Endpoint5_19Output = EffectValue> +export type SessionRevertCommitOperation = (input: Endpoint5_19Input) => Effect.Effect + +type Endpoint5_20Request = Parameters[0] +export type Endpoint5_20Input = { readonly sessionID: Endpoint5_20Request["params"]["sessionID"] } +export type Endpoint5_20Output = EffectValue>["data"] +export type SessionContextOperation = (input: Endpoint5_20Input) => Effect.Effect + +type Endpoint5_21Request = Parameters[0] +export type Endpoint5_21Input = { readonly sessionID: Endpoint5_21Request["params"]["sessionID"] } +export type Endpoint5_21Output = EffectValue>["data"] +export type SessionPendingListOperation = (input: Endpoint5_21Input) => Effect.Effect + +type Endpoint5_22Request = Parameters[0] +export type Endpoint5_22Input = { readonly sessionID: Endpoint5_22Request["params"]["sessionID"] } +export type Endpoint5_22Output = EffectValue< + ReturnType +>["data"] +export type SessionInstructionsEntryListOperation = ( + input: Endpoint5_22Input, +) => Effect.Effect + +type Endpoint5_23Request = Parameters[0] +export type Endpoint5_23Input = { + readonly sessionID: Endpoint5_23Request["params"]["sessionID"] + readonly key: Endpoint5_23Request["params"]["key"] + readonly value: Endpoint5_23Request["payload"]["value"] +} +export type Endpoint5_23Output = EffectValue> +export type SessionInstructionsEntryPutOperation = ( + input: Endpoint5_23Input, +) => Effect.Effect + +type Endpoint5_24Request = Parameters[0] +export type Endpoint5_24Input = { + readonly sessionID: Endpoint5_24Request["params"]["sessionID"] + readonly key: Endpoint5_24Request["params"]["key"] +} +export type Endpoint5_24Output = EffectValue< + ReturnType +> +export type SessionInstructionsEntryRemoveOperation = ( + input: Endpoint5_24Input, +) => Effect.Effect + +type Endpoint5_25Request = Parameters[0] +export type Endpoint5_25Input = { + readonly sessionID: Endpoint5_25Request["params"]["sessionID"] + readonly prompt: Endpoint5_25Request["payload"]["prompt"] +} +export type Endpoint5_25Output = EffectValue>["data"] +export type SessionGenerateOperation = (input: Endpoint5_25Input) => Effect.Effect + +type Endpoint5_26Request = Parameters[0] +export type Endpoint5_26Input = { + readonly sessionID: Endpoint5_26Request["params"]["sessionID"] + readonly after?: Endpoint5_26Request["query"]["after"] + readonly follow?: Endpoint5_26Request["query"]["follow"] +} +export type Endpoint5_26Output = StreamValue>> +export type SessionLogOperation = (input: Endpoint5_26Input) => Stream.Stream + +type Endpoint5_27Request = Parameters[0] +export type Endpoint5_27Input = { readonly sessionID: Endpoint5_27Request["params"]["sessionID"] } +export type Endpoint5_27Output = EffectValue> +export type SessionInterruptOperation = (input: Endpoint5_27Input) => Effect.Effect + +type Endpoint5_28Request = Parameters[0] +export type Endpoint5_28Input = { readonly sessionID: Endpoint5_28Request["params"]["sessionID"] } +export type Endpoint5_28Output = EffectValue> +export type SessionBackgroundOperation = (input: Endpoint5_28Input) => Effect.Effect + +type Endpoint5_29Request = Parameters[0] +export type Endpoint5_29Input = { + readonly sessionID: Endpoint5_29Request["params"]["sessionID"] + readonly messageID: Endpoint5_29Request["params"]["messageID"] +} +export type Endpoint5_29Output = EffectValue>["data"] +export type SessionMessageOperation = (input: Endpoint5_29Input) => Effect.Effect + +export interface SessionApi { + readonly list: SessionListOperation + readonly create: SessionCreateOperation + readonly active: SessionActiveOperation + readonly get: SessionGetOperation + readonly remove: SessionRemoveOperation + readonly fork: SessionForkOperation + readonly switchAgent: SessionSwitchAgentOperation + readonly switchModel: SessionSwitchModelOperation + readonly rename: SessionRenameOperation + readonly move: SessionMoveOperation + readonly prompt: SessionPromptOperation + readonly command: SessionCommandOperation + readonly skill: SessionSkillOperation + readonly synthetic: SessionSyntheticOperation + readonly shell: SessionShellOperation + readonly compact: SessionCompactOperation + readonly wait: SessionWaitOperation + readonly revert: { + readonly stage: SessionRevertStageOperation + readonly clear: SessionRevertClearOperation + readonly commit: SessionRevertCommitOperation + } + readonly context: SessionContextOperation + readonly pending: { readonly list: SessionPendingListOperation } + readonly instructions: { + readonly entry: { + readonly list: SessionInstructionsEntryListOperation + readonly put: SessionInstructionsEntryPutOperation + readonly remove: SessionInstructionsEntryRemoveOperation + } + } + readonly generate: SessionGenerateOperation + readonly log: SessionLogOperation + readonly interrupt: SessionInterruptOperation + readonly background: SessionBackgroundOperation + readonly message: SessionMessageOperation +} + +type Endpoint6_0Request = Parameters[0] +export type Endpoint6_0Input = { + readonly sessionID: Endpoint6_0Request["params"]["sessionID"] + readonly limit?: Endpoint6_0Request["query"]["limit"] + readonly order?: Endpoint6_0Request["query"]["order"] + readonly cursor?: Endpoint6_0Request["query"]["cursor"] +} +export type Endpoint6_0Output = EffectValue> +export type MessageListOperation = (input: Endpoint6_0Input) => Effect.Effect + +export interface MessageApi { + readonly list: MessageListOperation +} + +type Endpoint7_0Request = Parameters[0] +export type Endpoint7_0Input = { readonly location?: Endpoint7_0Request["query"]["location"] } +export type Endpoint7_0Output = EffectValue> +export type ModelListOperation = (input?: Endpoint7_0Input) => Effect.Effect + +type Endpoint7_1Request = Parameters[0] +export type Endpoint7_1Input = { readonly location?: Endpoint7_1Request["query"]["location"] } +export type Endpoint7_1Output = EffectValue> +export type ModelDefaultOperation = (input?: Endpoint7_1Input) => Effect.Effect + +export interface ModelApi { + readonly list: ModelListOperation + readonly default: ModelDefaultOperation +} + +type Endpoint8_0Request = Parameters[0] +export type Endpoint8_0Input = { + readonly location?: Endpoint8_0Request["query"]["location"] + readonly prompt: Endpoint8_0Request["payload"]["prompt"] + readonly model?: Endpoint8_0Request["payload"]["model"] +} +export type Endpoint8_0Output = EffectValue>["data"] +export type GenerateTextOperation = (input: Endpoint8_0Input) => Effect.Effect + +export interface GenerateApi { + readonly text: GenerateTextOperation +} + +type Endpoint9_0Request = Parameters[0] +export type Endpoint9_0Input = { readonly location?: Endpoint9_0Request["query"]["location"] } +export type Endpoint9_0Output = EffectValue> +export type ProviderListOperation = (input?: Endpoint9_0Input) => Effect.Effect + +type Endpoint9_1Request = Parameters[0] +export type Endpoint9_1Input = { + readonly providerID: Endpoint9_1Request["params"]["providerID"] + readonly location?: Endpoint9_1Request["query"]["location"] +} +export type Endpoint9_1Output = EffectValue> +export type ProviderGetOperation = (input: Endpoint9_1Input) => Effect.Effect + +export interface ProviderApi { + readonly list: ProviderListOperation + readonly get: ProviderGetOperation +} + +type Endpoint10_0Request = Parameters[0] +export type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["location"] } +export type Endpoint10_0Output = EffectValue> +export type IntegrationListOperation = (input?: Endpoint10_0Input) => Effect.Effect + +type Endpoint10_1Request = Parameters[0] +export type Endpoint10_1Input = { + readonly integrationID: Endpoint10_1Request["params"]["integrationID"] + readonly location?: Endpoint10_1Request["query"]["location"] +} +export type Endpoint10_1Output = EffectValue> +export type IntegrationGetOperation = (input: Endpoint10_1Input) => Effect.Effect + +type Endpoint10_2Request = Parameters[0] +export type Endpoint10_2Input = { + readonly location?: Endpoint10_2Request["query"]["location"] + readonly url: Endpoint10_2Request["payload"]["url"] +} +export type Endpoint10_2Output = EffectValue> +export type IntegrationWellknownAddOperation = ( + input: Endpoint10_2Input, +) => Effect.Effect + +type Endpoint10_3Request = Parameters[0] +export type Endpoint10_3Input = { + readonly integrationID: Endpoint10_3Request["params"]["integrationID"] + readonly location?: Endpoint10_3Request["query"]["location"] + readonly key: Endpoint10_3Request["payload"]["key"] + readonly label?: Endpoint10_3Request["payload"]["label"] +} +export type Endpoint10_3Output = EffectValue> +export type IntegrationConnectKeyOperation = ( + input: Endpoint10_3Input, +) => Effect.Effect + +type Endpoint10_4Request = Parameters[0] +export type Endpoint10_4Input = { + readonly integrationID: Endpoint10_4Request["params"]["integrationID"] + readonly location?: Endpoint10_4Request["query"]["location"] + readonly methodID: Endpoint10_4Request["payload"]["methodID"] + readonly inputs: Endpoint10_4Request["payload"]["inputs"] + readonly label?: Endpoint10_4Request["payload"]["label"] +} +export type Endpoint10_4Output = EffectValue> +export type IntegrationOauthConnectOperation = ( + input: Endpoint10_4Input, +) => Effect.Effect + +type Endpoint10_5Request = Parameters[0] +export type Endpoint10_5Input = { + readonly integrationID: Endpoint10_5Request["params"]["integrationID"] + readonly attemptID: Endpoint10_5Request["params"]["attemptID"] + readonly location?: Endpoint10_5Request["query"]["location"] +} +export type Endpoint10_5Output = EffectValue> +export type IntegrationOauthStatusOperation = ( + input: Endpoint10_5Input, +) => Effect.Effect + +type Endpoint10_6Request = Parameters[0] +export type Endpoint10_6Input = { + readonly integrationID: Endpoint10_6Request["params"]["integrationID"] + readonly attemptID: Endpoint10_6Request["params"]["attemptID"] + readonly location?: Endpoint10_6Request["query"]["location"] + readonly code?: Endpoint10_6Request["payload"]["code"] +} +export type Endpoint10_6Output = EffectValue> +export type IntegrationOauthCompleteOperation = ( + input: Endpoint10_6Input, +) => Effect.Effect + +type Endpoint10_7Request = Parameters[0] +export type Endpoint10_7Input = { + readonly integrationID: Endpoint10_7Request["params"]["integrationID"] + readonly attemptID: Endpoint10_7Request["params"]["attemptID"] + readonly location?: Endpoint10_7Request["query"]["location"] +} +export type Endpoint10_7Output = EffectValue> +export type IntegrationOauthCancelOperation = ( + input: Endpoint10_7Input, +) => Effect.Effect + +type Endpoint10_8Request = Parameters[0] +export type Endpoint10_8Input = { + readonly integrationID: Endpoint10_8Request["params"]["integrationID"] + readonly location?: Endpoint10_8Request["query"]["location"] + readonly methodID: Endpoint10_8Request["payload"]["methodID"] + readonly label?: Endpoint10_8Request["payload"]["label"] +} +export type Endpoint10_8Output = EffectValue> +export type IntegrationCommandConnectOperation = ( + input: Endpoint10_8Input, +) => Effect.Effect + +type Endpoint10_9Request = Parameters[0] +export type Endpoint10_9Input = { + readonly integrationID: Endpoint10_9Request["params"]["integrationID"] + readonly attemptID: Endpoint10_9Request["params"]["attemptID"] + readonly location?: Endpoint10_9Request["query"]["location"] +} +export type Endpoint10_9Output = EffectValue> +export type IntegrationCommandStatusOperation = ( + input: Endpoint10_9Input, +) => Effect.Effect + +type Endpoint10_10Request = Parameters[0] +export type Endpoint10_10Input = { + readonly integrationID: Endpoint10_10Request["params"]["integrationID"] + readonly attemptID: Endpoint10_10Request["params"]["attemptID"] + readonly location?: Endpoint10_10Request["query"]["location"] +} +export type Endpoint10_10Output = EffectValue> +export type IntegrationCommandCancelOperation = ( + input: Endpoint10_10Input, +) => Effect.Effect + +export interface IntegrationApi { + readonly list: IntegrationListOperation + readonly get: IntegrationGetOperation + readonly wellknown: { readonly add: IntegrationWellknownAddOperation } + readonly connect: { readonly key: IntegrationConnectKeyOperation } + readonly oauth: { + readonly connect: IntegrationOauthConnectOperation + readonly status: IntegrationOauthStatusOperation + readonly complete: IntegrationOauthCompleteOperation + readonly cancel: IntegrationOauthCancelOperation + } + readonly command: { + readonly connect: IntegrationCommandConnectOperation + readonly status: IntegrationCommandStatusOperation + readonly cancel: IntegrationCommandCancelOperation + } +} + +type Endpoint11_0Request = Parameters[0] +export type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] } +export type Endpoint11_0Output = EffectValue> +export type McpListOperation = (input?: Endpoint11_0Input) => Effect.Effect + +type Endpoint11_1Request = Parameters[0] +export type Endpoint11_1Input = { readonly location?: Endpoint11_1Request["query"]["location"] } +export type Endpoint11_1Output = EffectValue> +export type McpResourceCatalogOperation = (input?: Endpoint11_1Input) => Effect.Effect + +export interface McpApi { + readonly list: McpListOperation + readonly resource: { readonly catalog: McpResourceCatalogOperation } +} + +type Endpoint12_0Request = Parameters[0] +export type Endpoint12_0Input = { + readonly credentialID: Endpoint12_0Request["params"]["credentialID"] + readonly location?: Endpoint12_0Request["query"]["location"] + readonly label: Endpoint12_0Request["payload"]["label"] +} +export type Endpoint12_0Output = EffectValue> +export type CredentialUpdateOperation = (input: Endpoint12_0Input) => Effect.Effect + +type Endpoint12_1Request = Parameters[0] +export type Endpoint12_1Input = { + readonly credentialID: Endpoint12_1Request["params"]["credentialID"] + readonly location?: Endpoint12_1Request["query"]["location"] +} +export type Endpoint12_1Output = EffectValue> +export type CredentialRemoveOperation = (input: Endpoint12_1Input) => Effect.Effect + +export interface CredentialApi { + readonly update: CredentialUpdateOperation + readonly remove: CredentialRemoveOperation +} + +export type Endpoint13_0Output = EffectValue> +export type ProjectListOperation = () => Effect.Effect + +type Endpoint13_1Request = Parameters[0] +export type Endpoint13_1Input = { readonly location?: Endpoint13_1Request["query"]["location"] } +export type Endpoint13_1Output = EffectValue> +export type ProjectCurrentOperation = (input?: Endpoint13_1Input) => Effect.Effect + +type Endpoint13_2Request = Parameters[0] +export type Endpoint13_2Input = { + readonly projectID: Endpoint13_2Request["params"]["projectID"] + readonly location?: Endpoint13_2Request["query"]["location"] +} +export type Endpoint13_2Output = EffectValue> +export type ProjectDirectoriesOperation = (input: Endpoint13_2Input) => Effect.Effect + +export interface ProjectApi { + readonly list: ProjectListOperation + readonly current: ProjectCurrentOperation + readonly directories: ProjectDirectoriesOperation +} + +type Endpoint14_0Request = Parameters[0] +export type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] } +export type Endpoint14_0Output = EffectValue> +export type FormRequestListOperation = (input?: Endpoint14_0Input) => Effect.Effect + +type Endpoint14_1Request = Parameters[0] +export type Endpoint14_1Input = { readonly sessionID: Endpoint14_1Request["params"]["sessionID"] } +export type Endpoint14_1Output = EffectValue>["data"] +export type FormListOperation = (input: Endpoint14_1Input) => Effect.Effect + +type Endpoint14_2Request = Parameters[0] +export type Endpoint14_2Input = { + readonly sessionID: Endpoint14_2Request["params"]["sessionID"] + readonly id?: Endpoint14_2Request["payload"]["id"] + readonly title: Endpoint14_2Request["payload"]["title"] + readonly metadata?: Endpoint14_2Request["payload"]["metadata"] + readonly fields: Endpoint14_2Request["payload"]["fields"] +} +export type Endpoint14_2Output = EffectValue>["data"] +export type FormCreateOperation = (input: Endpoint14_2Input) => Effect.Effect + +type Endpoint14_3Request = Parameters[0] +export type Endpoint14_3Input = { + readonly sessionID: Endpoint14_3Request["params"]["sessionID"] + readonly formID: Endpoint14_3Request["params"]["formID"] +} +export type Endpoint14_3Output = EffectValue>["data"] +export type FormGetOperation = (input: Endpoint14_3Input) => Effect.Effect + +type Endpoint14_4Request = Parameters[0] +export type Endpoint14_4Input = { + readonly sessionID: Endpoint14_4Request["params"]["sessionID"] + readonly formID: Endpoint14_4Request["params"]["formID"] +} +export type Endpoint14_4Output = EffectValue>["data"] +export type FormStateOperation = (input: Endpoint14_4Input) => Effect.Effect + +type Endpoint14_5Request = Parameters[0] +export type Endpoint14_5Input = { + readonly sessionID: Endpoint14_5Request["params"]["sessionID"] + readonly formID: Endpoint14_5Request["params"]["formID"] + readonly answer: Endpoint14_5Request["payload"]["answer"] +} +export type Endpoint14_5Output = EffectValue> +export type FormReplyOperation = (input: Endpoint14_5Input) => Effect.Effect + +type Endpoint14_6Request = Parameters[0] +export type Endpoint14_6Input = { + readonly sessionID: Endpoint14_6Request["params"]["sessionID"] + readonly formID: Endpoint14_6Request["params"]["formID"] +} +export type Endpoint14_6Output = EffectValue> +export type FormCancelOperation = (input: Endpoint14_6Input) => Effect.Effect + +export interface FormApi { + readonly request: { readonly list: FormRequestListOperation } + readonly list: FormListOperation + readonly create: FormCreateOperation + readonly get: FormGetOperation + readonly state: FormStateOperation + readonly reply: FormReplyOperation + readonly cancel: FormCancelOperation +} + +type Endpoint15_0Request = Parameters[0] +export type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] } +export type Endpoint15_0Output = EffectValue> +export type PermissionRequestListOperation = ( + input?: Endpoint15_0Input, +) => Effect.Effect + +type Endpoint15_1Request = Parameters[0] +export type Endpoint15_1Input = { readonly projectID?: Endpoint15_1Request["query"]["projectID"] } +export type Endpoint15_1Output = EffectValue< + ReturnType +>["data"] +export type PermissionSavedListOperation = ( + input?: Endpoint15_1Input, +) => Effect.Effect + +type Endpoint15_2Request = Parameters[0] +export type Endpoint15_2Input = { readonly id: Endpoint15_2Request["params"]["id"] } +export type Endpoint15_2Output = EffectValue> +export type PermissionSavedRemoveOperation = ( + input: Endpoint15_2Input, +) => Effect.Effect + +type Endpoint15_3Request = Parameters[0] +export type Endpoint15_3Input = { + readonly sessionID: Endpoint15_3Request["params"]["sessionID"] + readonly id?: Endpoint15_3Request["payload"]["id"] + readonly action: Endpoint15_3Request["payload"]["action"] + readonly resources: Endpoint15_3Request["payload"]["resources"] + readonly save?: Endpoint15_3Request["payload"]["save"] + readonly metadata?: Endpoint15_3Request["payload"]["metadata"] + readonly source?: Endpoint15_3Request["payload"]["source"] + readonly agent?: Endpoint15_3Request["payload"]["agent"] +} +export type Endpoint15_3Output = EffectValue< + ReturnType +>["data"] +export type PermissionCreateOperation = (input: Endpoint15_3Input) => Effect.Effect + +type Endpoint15_4Request = Parameters[0] +export type Endpoint15_4Input = { readonly sessionID: Endpoint15_4Request["params"]["sessionID"] } +export type Endpoint15_4Output = EffectValue< + ReturnType +>["data"] +export type PermissionListOperation = (input: Endpoint15_4Input) => Effect.Effect + +type Endpoint15_5Request = Parameters[0] +export type Endpoint15_5Input = { + readonly sessionID: Endpoint15_5Request["params"]["sessionID"] + readonly requestID: Endpoint15_5Request["params"]["requestID"] +} +export type Endpoint15_5Output = EffectValue< + ReturnType +>["data"] +export type PermissionGetOperation = (input: Endpoint15_5Input) => Effect.Effect + +type Endpoint15_6Request = Parameters[0] +export type Endpoint15_6Input = { + readonly sessionID: Endpoint15_6Request["params"]["sessionID"] + readonly requestID: Endpoint15_6Request["params"]["requestID"] + readonly reply: Endpoint15_6Request["payload"]["reply"] + readonly message?: Endpoint15_6Request["payload"]["message"] +} +export type Endpoint15_6Output = EffectValue> +export type PermissionReplyOperation = (input: Endpoint15_6Input) => Effect.Effect + +export interface PermissionApi { + readonly request: { readonly list: PermissionRequestListOperation } + readonly saved: { readonly list: PermissionSavedListOperation; readonly remove: PermissionSavedRemoveOperation } + readonly create: PermissionCreateOperation + readonly list: PermissionListOperation + readonly get: PermissionGetOperation + readonly reply: PermissionReplyOperation +} + +type Endpoint16_0Request = Parameters[0] +export type Endpoint16_0Input = { + readonly location?: Endpoint16_0Request["query"]["location"] + readonly path?: Endpoint16_0Request["query"]["path"] +} +export type Endpoint16_0Output = EffectValue> +export type FileListOperation = (input?: Endpoint16_0Input) => Effect.Effect + +type Endpoint16_1Request = Parameters[0] +export type Endpoint16_1Input = { + readonly location?: Endpoint16_1Request["query"]["location"] + readonly query: Endpoint16_1Request["query"]["query"] + readonly type?: Endpoint16_1Request["query"]["type"] + readonly limit?: Endpoint16_1Request["query"]["limit"] +} +export type Endpoint16_1Output = EffectValue> +export type FileFindOperation = (input: Endpoint16_1Input) => Effect.Effect + +export interface FileApi { + readonly list: FileListOperation + readonly find: FileFindOperation +} + +type Endpoint17_0Request = Parameters[0] +export type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] } +export type Endpoint17_0Output = EffectValue> +export type CommandListOperation = (input?: Endpoint17_0Input) => Effect.Effect + +export interface CommandApi { + readonly list: CommandListOperation +} + +type Endpoint18_0Request = Parameters[0] +export type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] } +export type Endpoint18_0Output = EffectValue> +export type SkillListOperation = (input?: Endpoint18_0Input) => Effect.Effect + +export interface SkillApi { + readonly list: SkillListOperation +} + +export type Endpoint19_0Output = StreamValue>> +export type EventSubscribeOperation = () => Stream.Stream + +export interface EventApi { + readonly subscribe: EventSubscribeOperation +} + +type Endpoint20_0Request = Parameters[0] +export type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] } +export type Endpoint20_0Output = EffectValue> +export type PtyListOperation = (input?: Endpoint20_0Input) => Effect.Effect + +type Endpoint20_1Request = Parameters[0] +export type Endpoint20_1Input = { + readonly location?: Endpoint20_1Request["query"]["location"] + readonly command?: Endpoint20_1Request["payload"]["command"] + readonly args?: Endpoint20_1Request["payload"]["args"] + readonly cwd?: Endpoint20_1Request["payload"]["cwd"] + readonly title?: Endpoint20_1Request["payload"]["title"] + readonly env?: Endpoint20_1Request["payload"]["env"] +} +export type Endpoint20_1Output = EffectValue> +export type PtyCreateOperation = (input?: Endpoint20_1Input) => Effect.Effect + +type Endpoint20_2Request = Parameters[0] +export type Endpoint20_2Input = { + readonly ptyID: Endpoint20_2Request["params"]["ptyID"] + readonly location?: Endpoint20_2Request["query"]["location"] +} +export type Endpoint20_2Output = EffectValue> +export type PtyGetOperation = (input: Endpoint20_2Input) => Effect.Effect + +type Endpoint20_3Request = Parameters[0] +export type Endpoint20_3Input = { + readonly ptyID: Endpoint20_3Request["params"]["ptyID"] + readonly location?: Endpoint20_3Request["query"]["location"] + readonly title?: Endpoint20_3Request["payload"]["title"] + readonly size?: Endpoint20_3Request["payload"]["size"] +} +export type Endpoint20_3Output = EffectValue> +export type PtyUpdateOperation = (input: Endpoint20_3Input) => Effect.Effect + +type Endpoint20_4Request = Parameters[0] +export type Endpoint20_4Input = { + readonly ptyID: Endpoint20_4Request["params"]["ptyID"] + readonly location?: Endpoint20_4Request["query"]["location"] +} +export type Endpoint20_4Output = EffectValue> +export type PtyRemoveOperation = (input: Endpoint20_4Input) => Effect.Effect + +export interface PtyApi { + readonly list: PtyListOperation + readonly create: PtyCreateOperation + readonly get: PtyGetOperation + readonly update: PtyUpdateOperation + readonly remove: PtyRemoveOperation +} + +type Endpoint21_0Request = Parameters[0] +export type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] } +export type Endpoint21_0Output = EffectValue> +export type ShellListOperation = (input?: Endpoint21_0Input) => Effect.Effect + +type Endpoint21_1Request = Parameters[0] +export type Endpoint21_1Input = { + readonly location?: Endpoint21_1Request["query"]["location"] + readonly command: Endpoint21_1Request["payload"]["command"] + readonly cwd?: Endpoint21_1Request["payload"]["cwd"] + readonly timeout: Endpoint21_1Request["payload"]["timeout"] + readonly metadata?: Endpoint21_1Request["payload"]["metadata"] +} +export type Endpoint21_1Output = EffectValue> +export type ShellCreateOperation = (input: Endpoint21_1Input) => Effect.Effect + +type Endpoint21_2Request = Parameters[0] +export type Endpoint21_2Input = { + readonly id: Endpoint21_2Request["params"]["id"] + readonly location?: Endpoint21_2Request["query"]["location"] +} +export type Endpoint21_2Output = EffectValue> +export type ShellGetOperation = (input: Endpoint21_2Input) => Effect.Effect + +type Endpoint21_3Request = Parameters[0] +export type Endpoint21_3Input = { + readonly id: Endpoint21_3Request["params"]["id"] + readonly location?: Endpoint21_3Request["query"]["location"] + readonly timeout: Endpoint21_3Request["payload"]["timeout"] +} +export type Endpoint21_3Output = EffectValue> +export type ShellTimeoutOperation = (input: Endpoint21_3Input) => Effect.Effect + +type Endpoint21_4Request = Parameters[0] +export type Endpoint21_4Input = { + readonly id: Endpoint21_4Request["params"]["id"] + readonly location?: Endpoint21_4Request["query"]["location"] + readonly cursor?: Endpoint21_4Request["query"]["cursor"] + readonly limit?: Endpoint21_4Request["query"]["limit"] +} +export type Endpoint21_4Output = EffectValue> +export type ShellOutputOperation = (input: Endpoint21_4Input) => Effect.Effect + +type Endpoint21_5Request = Parameters[0] +export type Endpoint21_5Input = { + readonly id: Endpoint21_5Request["params"]["id"] + readonly location?: Endpoint21_5Request["query"]["location"] +} +export type Endpoint21_5Output = EffectValue> +export type ShellRemoveOperation = (input: Endpoint21_5Input) => Effect.Effect + +export interface ShellApi { + readonly list: ShellListOperation + readonly create: ShellCreateOperation + readonly get: ShellGetOperation + readonly timeout: ShellTimeoutOperation + readonly output: ShellOutputOperation + readonly remove: ShellRemoveOperation +} + +type Endpoint22_0Request = Parameters[0] +export type Endpoint22_0Input = { readonly location?: Endpoint22_0Request["query"]["location"] } +export type Endpoint22_0Output = EffectValue> +export type QuestionRequestListOperation = ( + input?: Endpoint22_0Input, +) => Effect.Effect + +type Endpoint22_1Request = Parameters[0] +export type Endpoint22_1Input = { readonly sessionID: Endpoint22_1Request["params"]["sessionID"] } +export type Endpoint22_1Output = EffectValue>["data"] +export type QuestionListOperation = (input: Endpoint22_1Input) => Effect.Effect + +type Endpoint22_2Request = Parameters[0] +export type Endpoint22_2Input = { + readonly sessionID: Endpoint22_2Request["params"]["sessionID"] + readonly requestID: Endpoint22_2Request["params"]["requestID"] + readonly answers: Endpoint22_2Request["payload"]["answers"] +} +export type Endpoint22_2Output = EffectValue> +export type QuestionReplyOperation = (input: Endpoint22_2Input) => Effect.Effect + +type Endpoint22_3Request = Parameters[0] +export type Endpoint22_3Input = { + readonly sessionID: Endpoint22_3Request["params"]["sessionID"] + readonly requestID: Endpoint22_3Request["params"]["requestID"] +} +export type Endpoint22_3Output = EffectValue> +export type QuestionRejectOperation = (input: Endpoint22_3Input) => Effect.Effect + +export interface QuestionApi { + readonly request: { readonly list: QuestionRequestListOperation } + readonly list: QuestionListOperation + readonly reply: QuestionReplyOperation + readonly reject: QuestionRejectOperation +} + +type Endpoint23_0Request = Parameters[0] +export type Endpoint23_0Input = { readonly location?: Endpoint23_0Request["query"]["location"] } +export type Endpoint23_0Output = EffectValue> +export type ReferenceListOperation = (input?: Endpoint23_0Input) => Effect.Effect + +export interface ReferenceApi { + readonly list: ReferenceListOperation +} + +type Endpoint24_0Request = Parameters[0] +export type Endpoint24_0Input = { + readonly projectID: Endpoint24_0Request["params"]["projectID"] + readonly location?: Endpoint24_0Request["query"]["location"] + readonly strategy: Endpoint24_0Request["payload"]["strategy"] + readonly directory: Endpoint24_0Request["payload"]["directory"] + readonly name?: Endpoint24_0Request["payload"]["name"] +} +export type Endpoint24_0Output = EffectValue> +export type ProjectCopyCreateOperation = (input: Endpoint24_0Input) => Effect.Effect + +type Endpoint24_1Request = Parameters[0] +export type Endpoint24_1Input = { + readonly projectID: Endpoint24_1Request["params"]["projectID"] + readonly location?: Endpoint24_1Request["query"]["location"] + readonly directory: Endpoint24_1Request["payload"]["directory"] + readonly force: Endpoint24_1Request["payload"]["force"] +} +export type Endpoint24_1Output = EffectValue> +export type ProjectCopyRemoveOperation = (input: Endpoint24_1Input) => Effect.Effect + +type Endpoint24_2Request = Parameters[0] +export type Endpoint24_2Input = { + readonly projectID: Endpoint24_2Request["params"]["projectID"] + readonly location?: Endpoint24_2Request["query"]["location"] +} +export type Endpoint24_2Output = EffectValue> +export type ProjectCopyRefreshOperation = (input: Endpoint24_2Input) => Effect.Effect + +export interface ProjectCopyApi { + readonly create: ProjectCopyCreateOperation + readonly remove: ProjectCopyRemoveOperation + readonly refresh: ProjectCopyRefreshOperation +} + +type Endpoint25_0Request = Parameters[0] +export type Endpoint25_0Input = { readonly location?: Endpoint25_0Request["query"]["location"] } +export type Endpoint25_0Output = EffectValue> +export type VcsStatusOperation = (input?: Endpoint25_0Input) => Effect.Effect + +type Endpoint25_1Request = Parameters[0] +export type Endpoint25_1Input = { + readonly location?: Endpoint25_1Request["query"]["location"] + readonly mode: Endpoint25_1Request["query"]["mode"] + readonly context?: Endpoint25_1Request["query"]["context"] +} +export type Endpoint25_1Output = EffectValue> +export type VcsDiffOperation = (input: Endpoint25_1Input) => Effect.Effect + +export interface VcsApi { + readonly status: VcsStatusOperation + readonly diff: VcsDiffOperation +} + +export type Endpoint26_0Output = EffectValue> +export type DebugLocationListOperation = () => Effect.Effect + +type Endpoint26_1Request = Parameters[0] +export type Endpoint26_1Input = { readonly location?: Endpoint26_1Request["query"]["location"] } +export type Endpoint26_1Output = EffectValue> +export type DebugLocationEvictOperation = (input?: Endpoint26_1Input) => Effect.Effect + +export interface DebugApi { + readonly location: { readonly list: DebugLocationListOperation; readonly evict: DebugLocationEvictOperation } +} + +export interface AppApi { + readonly health: HealthApi + readonly server: ServerApi + readonly location: LocationApi + readonly agent: AgentApi + readonly plugin: PluginApi + readonly session: SessionApi + readonly message: MessageApi + readonly model: ModelApi + readonly generate: GenerateApi + readonly provider: ProviderApi + readonly integration: IntegrationApi + readonly mcp: McpApi + readonly credential: CredentialApi + readonly project: ProjectApi + readonly form: FormApi + readonly permission: PermissionApi + readonly file: FileApi + readonly command: CommandApi + readonly skill: SkillApi + readonly event: EventApi + readonly pty: PtyApi + readonly shell: ShellApi + readonly question: QuestionApi + readonly reference: ReferenceApi + readonly projectCopy: ProjectCopyApi + readonly vcs: VcsApi + readonly debug: DebugApi +} diff --git a/packages/client/src/generated-effect/.httpapi-codegen.json b/packages/client/src/effect/generated/.httpapi-codegen.json similarity index 100% rename from packages/client/src/generated-effect/.httpapi-codegen.json rename to packages/client/src/effect/generated/.httpapi-codegen.json diff --git a/packages/client/src/generated-effect/client-error.ts b/packages/client/src/effect/generated/client-error.ts similarity index 100% rename from packages/client/src/generated-effect/client-error.ts rename to packages/client/src/effect/generated/client-error.ts diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts new file mode 100644 index 0000000000..0de7fad7bd --- /dev/null +++ b/packages/client/src/effect/generated/client.ts @@ -0,0 +1,1231 @@ +// Generated by @opencode-ai/httpapi-codegen. Do not edit. +import { Effect, Stream, Schema } from "effect" +import { Sse } from "effect/unstable/encoding" +import { HttpClientError } from "effect/unstable/http" +import { HttpApiClient } from "effect/unstable/httpapi" +import { ClientApi } from "../../contract" +import { ClientError } from "./client-error" + +type RawClient = HttpApiClient.ForApi + +const mapClientError = (error: E) => + HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) + ? new ClientError({ cause: error }) + : error + +const Endpoint0_0 = (raw: RawClient["server.health"]) => () => + raw["health.get"]({}).pipe(Effect.mapError(mapClientError)) + +type Endpoint0_1Request = Parameters[0] +type Endpoint0_1Input = { readonly instanceID: Endpoint0_1Request["payload"]["instanceID"] } +const Endpoint0_1 = (raw: RawClient["server.health"]) => (input: Endpoint0_1Input) => + raw["health.stop"]({ payload: { instanceID: input["instanceID"] } }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup0 = (raw: RawClient["server.health"]) => ({ get: Endpoint0_0(raw), stop: Endpoint0_1(raw) }) + +const Endpoint1_0 = (raw: RawClient["server.server"]) => () => + raw["server.get"]({}).pipe(Effect.mapError(mapClientError)) + +const adaptGroup1 = (raw: RawClient["server.server"]) => ({ get: Endpoint1_0(raw) }) + +type Endpoint2_0Request = Parameters[0] +type Endpoint2_0Input = { readonly location?: Endpoint2_0Request["query"]["location"] } +const Endpoint2_0 = (raw: RawClient["server.location"]) => (input?: Endpoint2_0Input) => + raw["location.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup2 = (raw: RawClient["server.location"]) => ({ get: Endpoint2_0(raw) }) + +type Endpoint3_0Request = Parameters[0] +type Endpoint3_0Input = { readonly location?: Endpoint3_0Request["query"]["location"] } +const Endpoint3_0 = (raw: RawClient["server.agent"]) => (input?: Endpoint3_0Input) => + raw["agent.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup3 = (raw: RawClient["server.agent"]) => ({ list: Endpoint3_0(raw) }) + +type Endpoint4_0Request = Parameters[0] +type Endpoint4_0Input = { readonly location?: Endpoint4_0Request["query"]["location"] } +const Endpoint4_0 = (raw: RawClient["server.plugin"]) => (input?: Endpoint4_0Input) => + raw["plugin.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup4 = (raw: RawClient["server.plugin"]) => ({ list: Endpoint4_0(raw) }) + +type Endpoint5_0Request = Parameters[0] +type Endpoint5_0Input = { + readonly workspace?: Endpoint5_0Request["query"]["workspace"] + readonly limit?: Endpoint5_0Request["query"]["limit"] + readonly order?: Endpoint5_0Request["query"]["order"] + readonly search?: Endpoint5_0Request["query"]["search"] + readonly parentID?: Endpoint5_0Request["query"]["parentID"] + readonly directory?: Endpoint5_0Request["query"]["directory"] + readonly project?: Endpoint5_0Request["query"]["project"] + readonly subpath?: Endpoint5_0Request["query"]["subpath"] + readonly cursor?: Endpoint5_0Request["query"]["cursor"] +} +const Endpoint5_0 = (raw: RawClient["server.session"]) => (input?: Endpoint5_0Input) => + raw["session.list"]({ + query: { + workspace: input?.["workspace"], + limit: input?.["limit"], + order: input?.["order"], + search: input?.["search"], + parentID: input?.["parentID"], + directory: input?.["directory"], + project: input?.["project"], + subpath: input?.["subpath"], + cursor: input?.["cursor"], + }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint5_1Request = Parameters[0] +type Endpoint5_1Input = { + readonly id?: Endpoint5_1Request["payload"]["id"] + readonly agent?: Endpoint5_1Request["payload"]["agent"] + readonly model?: Endpoint5_1Request["payload"]["model"] + readonly location?: Endpoint5_1Request["payload"]["location"] +} +const Endpoint5_1 = (raw: RawClient["server.session"]) => (input?: Endpoint5_1Input) => + raw["session.create"]({ + payload: { id: input?.["id"], agent: input?.["agent"], model: input?.["model"], location: input?.["location"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +const Endpoint5_2 = (raw: RawClient["server.session"]) => () => + raw["session.active"]({}).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint5_3Request = Parameters[0] +type Endpoint5_3Input = { readonly sessionID: Endpoint5_3Request["params"]["sessionID"] } +const Endpoint5_3 = (raw: RawClient["server.session"]) => (input: Endpoint5_3Input) => + raw["session.get"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint5_4Request = Parameters[0] +type Endpoint5_4Input = { readonly sessionID: Endpoint5_4Request["params"]["sessionID"] } +const Endpoint5_4 = (raw: RawClient["server.session"]) => (input: Endpoint5_4Input) => + raw["session.remove"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint5_5Request = Parameters[0] +type Endpoint5_5Input = { + readonly sessionID: Endpoint5_5Request["params"]["sessionID"] + readonly messageID?: Endpoint5_5Request["payload"]["messageID"] +} +const Endpoint5_5 = (raw: RawClient["server.session"]) => (input: Endpoint5_5Input) => + raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { messageID: input["messageID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint5_6Request = Parameters[0] +type Endpoint5_6Input = { + readonly sessionID: Endpoint5_6Request["params"]["sessionID"] + readonly agent: Endpoint5_6Request["payload"]["agent"] +} +const Endpoint5_6 = (raw: RawClient["server.session"]) => (input: Endpoint5_6Input) => + raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe( + Effect.mapError(mapClientError), + ) + +type Endpoint5_7Request = Parameters[0] +type Endpoint5_7Input = { + readonly sessionID: Endpoint5_7Request["params"]["sessionID"] + readonly model: Endpoint5_7Request["payload"]["model"] +} +const Endpoint5_7 = (raw: RawClient["server.session"]) => (input: Endpoint5_7Input) => + raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe( + Effect.mapError(mapClientError), + ) + +type Endpoint5_8Request = Parameters[0] +type Endpoint5_8Input = { + readonly sessionID: Endpoint5_8Request["params"]["sessionID"] + readonly title: Endpoint5_8Request["payload"]["title"] +} +const Endpoint5_8 = (raw: RawClient["server.session"]) => (input: Endpoint5_8Input) => + raw["session.rename"]({ params: { sessionID: input["sessionID"] }, payload: { title: input["title"] } }).pipe( + Effect.mapError(mapClientError), + ) + +type Endpoint5_9Request = Parameters[0] +type Endpoint5_9Input = { + readonly sessionID: Endpoint5_9Request["params"]["sessionID"] + readonly directory: Endpoint5_9Request["payload"]["directory"] + readonly workspaceID?: Endpoint5_9Request["payload"]["workspaceID"] +} +const Endpoint5_9 = (raw: RawClient["server.session"]) => (input: Endpoint5_9Input) => + raw["session.move"]({ + params: { sessionID: input["sessionID"] }, + payload: { directory: input["directory"], workspaceID: input["workspaceID"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint5_10Request = Parameters[0] +type Endpoint5_10Input = { + readonly sessionID: Endpoint5_10Request["params"]["sessionID"] + readonly id?: Endpoint5_10Request["payload"]["id"] + readonly text: Endpoint5_10Request["payload"]["text"] + readonly files?: Endpoint5_10Request["payload"]["files"] + readonly agents?: Endpoint5_10Request["payload"]["agents"] + readonly metadata?: Endpoint5_10Request["payload"]["metadata"] + readonly delivery?: Endpoint5_10Request["payload"]["delivery"] + readonly resume?: Endpoint5_10Request["payload"]["resume"] +} +const Endpoint5_10 = (raw: RawClient["server.session"]) => (input: Endpoint5_10Input) => + raw["session.prompt"]({ + params: { sessionID: input["sessionID"] }, + payload: { + id: input["id"], + text: input["text"], + files: input["files"], + agents: input["agents"], + metadata: input["metadata"], + delivery: input["delivery"], + resume: input["resume"], + }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint5_11Request = Parameters[0] +type Endpoint5_11Input = { + readonly sessionID: Endpoint5_11Request["params"]["sessionID"] + readonly id?: Endpoint5_11Request["payload"]["id"] + readonly command: Endpoint5_11Request["payload"]["command"] + readonly arguments?: Endpoint5_11Request["payload"]["arguments"] + readonly agent?: Endpoint5_11Request["payload"]["agent"] + readonly model?: Endpoint5_11Request["payload"]["model"] + readonly files?: Endpoint5_11Request["payload"]["files"] + readonly agents?: Endpoint5_11Request["payload"]["agents"] + readonly delivery?: Endpoint5_11Request["payload"]["delivery"] + readonly resume?: Endpoint5_11Request["payload"]["resume"] +} +const Endpoint5_11 = (raw: RawClient["server.session"]) => (input: Endpoint5_11Input) => + raw["session.command"]({ + params: { sessionID: input["sessionID"] }, + payload: { + id: input["id"], + command: input["command"], + arguments: input["arguments"], + agent: input["agent"], + model: input["model"], + files: input["files"], + agents: input["agents"], + delivery: input["delivery"], + resume: input["resume"], + }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint5_12Request = Parameters[0] +type Endpoint5_12Input = { + readonly sessionID: Endpoint5_12Request["params"]["sessionID"] + readonly id?: Endpoint5_12Request["payload"]["id"] + readonly skill: Endpoint5_12Request["payload"]["skill"] + readonly resume?: Endpoint5_12Request["payload"]["resume"] +} +const Endpoint5_12 = (raw: RawClient["server.session"]) => (input: Endpoint5_12Input) => + raw["session.skill"]({ + params: { sessionID: input["sessionID"] }, + payload: { id: input["id"], skill: input["skill"], resume: input["resume"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint5_13Request = Parameters[0] +type Endpoint5_13Input = { + readonly sessionID: Endpoint5_13Request["params"]["sessionID"] + readonly id?: Endpoint5_13Request["payload"]["id"] + readonly text: Endpoint5_13Request["payload"]["text"] + readonly description?: Endpoint5_13Request["payload"]["description"] + readonly metadata?: Endpoint5_13Request["payload"]["metadata"] + readonly delivery?: Endpoint5_13Request["payload"]["delivery"] + readonly resume?: Endpoint5_13Request["payload"]["resume"] +} +const Endpoint5_13 = (raw: RawClient["server.session"]) => (input: Endpoint5_13Input) => + raw["session.synthetic"]({ + params: { sessionID: input["sessionID"] }, + payload: { + id: input["id"], + text: input["text"], + description: input["description"], + metadata: input["metadata"], + delivery: input["delivery"], + resume: input["resume"], + }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint5_14Request = Parameters[0] +type Endpoint5_14Input = { + readonly sessionID: Endpoint5_14Request["params"]["sessionID"] + readonly id?: Endpoint5_14Request["payload"]["id"] + readonly command: Endpoint5_14Request["payload"]["command"] +} +const Endpoint5_14 = (raw: RawClient["server.session"]) => (input: Endpoint5_14Input) => + raw["session.shell"]({ + params: { sessionID: input["sessionID"] }, + payload: { id: input["id"], command: input["command"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint5_15Request = Parameters[0] +type Endpoint5_15Input = { + readonly sessionID: Endpoint5_15Request["params"]["sessionID"] + readonly id?: Endpoint5_15Request["payload"]["id"] +} +const Endpoint5_15 = (raw: RawClient["server.session"]) => (input: Endpoint5_15Input) => + raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint5_16Request = Parameters[0] +type Endpoint5_16Input = { readonly sessionID: Endpoint5_16Request["params"]["sessionID"] } +const Endpoint5_16 = (raw: RawClient["server.session"]) => (input: Endpoint5_16Input) => + raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint5_17Request = Parameters[0] +type Endpoint5_17Input = { + readonly sessionID: Endpoint5_17Request["params"]["sessionID"] + readonly messageID: Endpoint5_17Request["payload"]["messageID"] + readonly files?: Endpoint5_17Request["payload"]["files"] +} +const Endpoint5_17 = (raw: RawClient["server.session"]) => (input: Endpoint5_17Input) => + raw["session.revert.stage"]({ + params: { sessionID: input["sessionID"] }, + payload: { messageID: input["messageID"], files: input["files"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint5_18Request = Parameters[0] +type Endpoint5_18Input = { readonly sessionID: Endpoint5_18Request["params"]["sessionID"] } +const Endpoint5_18 = (raw: RawClient["server.session"]) => (input: Endpoint5_18Input) => + raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint5_19Request = Parameters[0] +type Endpoint5_19Input = { readonly sessionID: Endpoint5_19Request["params"]["sessionID"] } +const Endpoint5_19 = (raw: RawClient["server.session"]) => (input: Endpoint5_19Input) => + raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint5_20Request = Parameters[0] +type Endpoint5_20Input = { readonly sessionID: Endpoint5_20Request["params"]["sessionID"] } +const Endpoint5_20 = (raw: RawClient["server.session"]) => (input: Endpoint5_20Input) => + raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint5_21Request = Parameters[0] +type Endpoint5_21Input = { readonly sessionID: Endpoint5_21Request["params"]["sessionID"] } +const Endpoint5_21 = (raw: RawClient["server.session"]) => (input: Endpoint5_21Input) => + raw["session.pending.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint5_22Request = Parameters[0] +type Endpoint5_22Input = { readonly sessionID: Endpoint5_22Request["params"]["sessionID"] } +const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22Input) => + raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint5_23Request = Parameters[0] +type Endpoint5_23Input = { + readonly sessionID: Endpoint5_23Request["params"]["sessionID"] + readonly key: Endpoint5_23Request["params"]["key"] + readonly value: Endpoint5_23Request["payload"]["value"] +} +const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23Input) => + raw["session.instructions.entry.put"]({ + params: { sessionID: input["sessionID"], key: input["key"] }, + payload: { value: input["value"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint5_24Request = Parameters[0] +type Endpoint5_24Input = { + readonly sessionID: Endpoint5_24Request["params"]["sessionID"] + readonly key: Endpoint5_24Request["params"]["key"] +} +const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) => + raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe( + Effect.mapError(mapClientError), + ) + +type Endpoint5_25Request = Parameters[0] +type Endpoint5_25Input = { + readonly sessionID: Endpoint5_25Request["params"]["sessionID"] + readonly prompt: Endpoint5_25Request["payload"]["prompt"] +} +const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) => + raw["session.generate"]({ params: { sessionID: input["sessionID"] }, payload: { prompt: input["prompt"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint5_26Request = Parameters[0] +type Endpoint5_26Input = { + readonly sessionID: Endpoint5_26Request["params"]["sessionID"] + readonly after?: Endpoint5_26Request["query"]["after"] + readonly follow?: Endpoint5_26Request["query"]["follow"] +} +const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) => + Stream.unwrap( + raw["session.log"]({ + params: { sessionID: input["sessionID"] }, + query: { after: input["after"], follow: input["follow"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), + ), + ) + +type Endpoint5_27Request = Parameters[0] +type Endpoint5_27Input = { readonly sessionID: Endpoint5_27Request["params"]["sessionID"] } +const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) => + raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint5_28Request = Parameters[0] +type Endpoint5_28Input = { readonly sessionID: Endpoint5_28Request["params"]["sessionID"] } +const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28Input) => + raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint5_29Request = Parameters[0] +type Endpoint5_29Input = { + readonly sessionID: Endpoint5_29Request["params"]["sessionID"] + readonly messageID: Endpoint5_29Request["params"]["messageID"] +} +const Endpoint5_29 = (raw: RawClient["server.session"]) => (input: Endpoint5_29Input) => + raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +const adaptGroup5 = (raw: RawClient["server.session"]) => ({ + list: Endpoint5_0(raw), + create: Endpoint5_1(raw), + active: Endpoint5_2(raw), + get: Endpoint5_3(raw), + remove: Endpoint5_4(raw), + fork: Endpoint5_5(raw), + switchAgent: Endpoint5_6(raw), + switchModel: Endpoint5_7(raw), + rename: Endpoint5_8(raw), + move: Endpoint5_9(raw), + prompt: Endpoint5_10(raw), + command: Endpoint5_11(raw), + skill: Endpoint5_12(raw), + synthetic: Endpoint5_13(raw), + shell: Endpoint5_14(raw), + compact: Endpoint5_15(raw), + wait: Endpoint5_16(raw), + revert: { stage: Endpoint5_17(raw), clear: Endpoint5_18(raw), commit: Endpoint5_19(raw) }, + context: Endpoint5_20(raw), + pending: { list: Endpoint5_21(raw) }, + instructions: { entry: { list: Endpoint5_22(raw), put: Endpoint5_23(raw), remove: Endpoint5_24(raw) } }, + generate: Endpoint5_25(raw), + log: Endpoint5_26(raw), + interrupt: Endpoint5_27(raw), + background: Endpoint5_28(raw), + message: Endpoint5_29(raw), +}) + +type Endpoint6_0Request = Parameters[0] +type Endpoint6_0Input = { + readonly sessionID: Endpoint6_0Request["params"]["sessionID"] + readonly limit?: Endpoint6_0Request["query"]["limit"] + readonly order?: Endpoint6_0Request["query"]["order"] + readonly cursor?: Endpoint6_0Request["query"]["cursor"] +} +const Endpoint6_0 = (raw: RawClient["server.message"]) => (input: Endpoint6_0Input) => + raw["session.messages"]({ + params: { sessionID: input["sessionID"] }, + query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] }, + }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup6 = (raw: RawClient["server.message"]) => ({ list: Endpoint6_0(raw) }) + +type Endpoint7_0Request = Parameters[0] +type Endpoint7_0Input = { readonly location?: Endpoint7_0Request["query"]["location"] } +const Endpoint7_0 = (raw: RawClient["server.model"]) => (input?: Endpoint7_0Input) => + raw["model.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint7_1Request = Parameters[0] +type Endpoint7_1Input = { readonly location?: Endpoint7_1Request["query"]["location"] } +const Endpoint7_1 = (raw: RawClient["server.model"]) => (input?: Endpoint7_1Input) => + raw["model.default"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup7 = (raw: RawClient["server.model"]) => ({ list: Endpoint7_0(raw), default: Endpoint7_1(raw) }) + +type Endpoint8_0Request = Parameters[0] +type Endpoint8_0Input = { + readonly location?: Endpoint8_0Request["query"]["location"] + readonly prompt: Endpoint8_0Request["payload"]["prompt"] + readonly model?: Endpoint8_0Request["payload"]["model"] +} +const Endpoint8_0 = (raw: RawClient["server.generate"]) => (input: Endpoint8_0Input) => + raw["generate.text"]({ + query: { location: input["location"] }, + payload: { prompt: input["prompt"], model: input["model"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +const adaptGroup8 = (raw: RawClient["server.generate"]) => ({ text: Endpoint8_0(raw) }) + +type Endpoint9_0Request = Parameters[0] +type Endpoint9_0Input = { readonly location?: Endpoint9_0Request["query"]["location"] } +const Endpoint9_0 = (raw: RawClient["server.provider"]) => (input?: Endpoint9_0Input) => + raw["provider.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint9_1Request = Parameters[0] +type Endpoint9_1Input = { + readonly providerID: Endpoint9_1Request["params"]["providerID"] + readonly location?: Endpoint9_1Request["query"]["location"] +} +const Endpoint9_1 = (raw: RawClient["server.provider"]) => (input: Endpoint9_1Input) => + raw["provider.get"]({ params: { providerID: input["providerID"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ) + +const adaptGroup9 = (raw: RawClient["server.provider"]) => ({ list: Endpoint9_0(raw), get: Endpoint9_1(raw) }) + +type Endpoint10_0Request = Parameters[0] +type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["location"] } +const Endpoint10_0 = (raw: RawClient["server.integration"]) => (input?: Endpoint10_0Input) => + raw["integration.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint10_1Request = Parameters[0] +type Endpoint10_1Input = { + readonly integrationID: Endpoint10_1Request["params"]["integrationID"] + readonly location?: Endpoint10_1Request["query"]["location"] +} +const Endpoint10_1 = (raw: RawClient["server.integration"]) => (input: Endpoint10_1Input) => + raw["integration.get"]({ + params: { integrationID: input["integrationID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint10_2Request = Parameters[0] +type Endpoint10_2Input = { + readonly location?: Endpoint10_2Request["query"]["location"] + readonly url: Endpoint10_2Request["payload"]["url"] +} +const Endpoint10_2 = (raw: RawClient["server.integration"]) => (input: Endpoint10_2Input) => + raw["integration.wellknown.add"]({ query: { location: input["location"] }, payload: { url: input["url"] } }).pipe( + Effect.mapError(mapClientError), + ) + +type Endpoint10_3Request = Parameters[0] +type Endpoint10_3Input = { + readonly integrationID: Endpoint10_3Request["params"]["integrationID"] + readonly location?: Endpoint10_3Request["query"]["location"] + readonly key: Endpoint10_3Request["payload"]["key"] + readonly label?: Endpoint10_3Request["payload"]["label"] +} +const Endpoint10_3 = (raw: RawClient["server.integration"]) => (input: Endpoint10_3Input) => + raw["integration.connect.key"]({ + params: { integrationID: input["integrationID"] }, + query: { location: input["location"] }, + payload: { key: input["key"], label: input["label"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint10_4Request = Parameters[0] +type Endpoint10_4Input = { + readonly integrationID: Endpoint10_4Request["params"]["integrationID"] + readonly location?: Endpoint10_4Request["query"]["location"] + readonly methodID: Endpoint10_4Request["payload"]["methodID"] + readonly inputs: Endpoint10_4Request["payload"]["inputs"] + readonly label?: Endpoint10_4Request["payload"]["label"] +} +const Endpoint10_4 = (raw: RawClient["server.integration"]) => (input: Endpoint10_4Input) => + raw["integration.oauth.connect"]({ + params: { integrationID: input["integrationID"] }, + query: { location: input["location"] }, + payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint10_5Request = Parameters[0] +type Endpoint10_5Input = { + readonly integrationID: Endpoint10_5Request["params"]["integrationID"] + readonly attemptID: Endpoint10_5Request["params"]["attemptID"] + readonly location?: Endpoint10_5Request["query"]["location"] +} +const Endpoint10_5 = (raw: RawClient["server.integration"]) => (input: Endpoint10_5Input) => + raw["integration.oauth.status"]({ + params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint10_6Request = Parameters[0] +type Endpoint10_6Input = { + readonly integrationID: Endpoint10_6Request["params"]["integrationID"] + readonly attemptID: Endpoint10_6Request["params"]["attemptID"] + readonly location?: Endpoint10_6Request["query"]["location"] + readonly code?: Endpoint10_6Request["payload"]["code"] +} +const Endpoint10_6 = (raw: RawClient["server.integration"]) => (input: Endpoint10_6Input) => + raw["integration.oauth.complete"]({ + params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, + query: { location: input["location"] }, + payload: { code: input["code"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint10_7Request = Parameters[0] +type Endpoint10_7Input = { + readonly integrationID: Endpoint10_7Request["params"]["integrationID"] + readonly attemptID: Endpoint10_7Request["params"]["attemptID"] + readonly location?: Endpoint10_7Request["query"]["location"] +} +const Endpoint10_7 = (raw: RawClient["server.integration"]) => (input: Endpoint10_7Input) => + raw["integration.oauth.cancel"]({ + params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint10_8Request = Parameters[0] +type Endpoint10_8Input = { + readonly integrationID: Endpoint10_8Request["params"]["integrationID"] + readonly location?: Endpoint10_8Request["query"]["location"] + readonly methodID: Endpoint10_8Request["payload"]["methodID"] + readonly label?: Endpoint10_8Request["payload"]["label"] +} +const Endpoint10_8 = (raw: RawClient["server.integration"]) => (input: Endpoint10_8Input) => + raw["integration.command.connect"]({ + params: { integrationID: input["integrationID"] }, + query: { location: input["location"] }, + payload: { methodID: input["methodID"], label: input["label"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint10_9Request = Parameters[0] +type Endpoint10_9Input = { + readonly integrationID: Endpoint10_9Request["params"]["integrationID"] + readonly attemptID: Endpoint10_9Request["params"]["attemptID"] + readonly location?: Endpoint10_9Request["query"]["location"] +} +const Endpoint10_9 = (raw: RawClient["server.integration"]) => (input: Endpoint10_9Input) => + raw["integration.command.status"]({ + params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint10_10Request = Parameters[0] +type Endpoint10_10Input = { + readonly integrationID: Endpoint10_10Request["params"]["integrationID"] + readonly attemptID: Endpoint10_10Request["params"]["attemptID"] + readonly location?: Endpoint10_10Request["query"]["location"] +} +const Endpoint10_10 = (raw: RawClient["server.integration"]) => (input: Endpoint10_10Input) => + raw["integration.command.cancel"]({ + params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup10 = (raw: RawClient["server.integration"]) => ({ + list: Endpoint10_0(raw), + get: Endpoint10_1(raw), + wellknown: { add: Endpoint10_2(raw) }, + connect: { key: Endpoint10_3(raw) }, + oauth: { + connect: Endpoint10_4(raw), + status: Endpoint10_5(raw), + complete: Endpoint10_6(raw), + cancel: Endpoint10_7(raw), + }, + command: { connect: Endpoint10_8(raw), status: Endpoint10_9(raw), cancel: Endpoint10_10(raw) }, +}) + +type Endpoint11_0Request = Parameters[0] +type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] } +const Endpoint11_0 = (raw: RawClient["server.mcp"]) => (input?: Endpoint11_0Input) => + raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint11_1Request = Parameters[0] +type Endpoint11_1Input = { readonly location?: Endpoint11_1Request["query"]["location"] } +const Endpoint11_1 = (raw: RawClient["server.mcp"]) => (input?: Endpoint11_1Input) => + raw["mcp.resource.catalog"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup11 = (raw: RawClient["server.mcp"]) => ({ + list: Endpoint11_0(raw), + resource: { catalog: Endpoint11_1(raw) }, +}) + +type Endpoint12_0Request = Parameters[0] +type Endpoint12_0Input = { + readonly credentialID: Endpoint12_0Request["params"]["credentialID"] + readonly location?: Endpoint12_0Request["query"]["location"] + readonly label: Endpoint12_0Request["payload"]["label"] +} +const Endpoint12_0 = (raw: RawClient["server.credential"]) => (input: Endpoint12_0Input) => + raw["credential.update"]({ + params: { credentialID: input["credentialID"] }, + query: { location: input["location"] }, + payload: { label: input["label"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint12_1Request = Parameters[0] +type Endpoint12_1Input = { + readonly credentialID: Endpoint12_1Request["params"]["credentialID"] + readonly location?: Endpoint12_1Request["query"]["location"] +} +const Endpoint12_1 = (raw: RawClient["server.credential"]) => (input: Endpoint12_1Input) => + raw["credential.remove"]({ + params: { credentialID: input["credentialID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup12 = (raw: RawClient["server.credential"]) => ({ update: Endpoint12_0(raw), remove: Endpoint12_1(raw) }) + +const Endpoint13_0 = (raw: RawClient["server.project"]) => () => + raw["project.list"]({}).pipe(Effect.mapError(mapClientError)) + +type Endpoint13_1Request = Parameters[0] +type Endpoint13_1Input = { readonly location?: Endpoint13_1Request["query"]["location"] } +const Endpoint13_1 = (raw: RawClient["server.project"]) => (input?: Endpoint13_1Input) => + raw["project.current"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint13_2Request = Parameters[0] +type Endpoint13_2Input = { + readonly projectID: Endpoint13_2Request["params"]["projectID"] + readonly location?: Endpoint13_2Request["query"]["location"] +} +const Endpoint13_2 = (raw: RawClient["server.project"]) => (input: Endpoint13_2Input) => + raw["project.directories"]({ + params: { projectID: input["projectID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup13 = (raw: RawClient["server.project"]) => ({ + list: Endpoint13_0(raw), + current: Endpoint13_1(raw), + directories: Endpoint13_2(raw), +}) + +type Endpoint14_0Request = Parameters[0] +type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] } +const Endpoint14_0 = (raw: RawClient["server.form"]) => (input?: Endpoint14_0Input) => + raw["form.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint14_1Request = Parameters[0] +type Endpoint14_1Input = { readonly sessionID: Endpoint14_1Request["params"]["sessionID"] } +const Endpoint14_1 = (raw: RawClient["server.form"]) => (input: Endpoint14_1Input) => + raw["session.form.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint14_2Request = Parameters[0] +type Endpoint14_2Input = { + readonly sessionID: Endpoint14_2Request["params"]["sessionID"] + readonly id?: Endpoint14_2Request["payload"]["id"] + readonly title: Endpoint14_2Request["payload"]["title"] + readonly metadata?: Endpoint14_2Request["payload"]["metadata"] + readonly fields: Endpoint14_2Request["payload"]["fields"] +} +const Endpoint14_2 = (raw: RawClient["server.form"]) => (input: Endpoint14_2Input) => + raw["session.form.create"]({ + params: { sessionID: input["sessionID"] }, + payload: { id: input["id"], title: input["title"], metadata: input["metadata"], fields: input["fields"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint14_3Request = Parameters[0] +type Endpoint14_3Input = { + readonly sessionID: Endpoint14_3Request["params"]["sessionID"] + readonly formID: Endpoint14_3Request["params"]["formID"] +} +const Endpoint14_3 = (raw: RawClient["server.form"]) => (input: Endpoint14_3Input) => + raw["session.form.get"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint14_4Request = Parameters[0] +type Endpoint14_4Input = { + readonly sessionID: Endpoint14_4Request["params"]["sessionID"] + readonly formID: Endpoint14_4Request["params"]["formID"] +} +const Endpoint14_4 = (raw: RawClient["server.form"]) => (input: Endpoint14_4Input) => + raw["session.form.state"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint14_5Request = Parameters[0] +type Endpoint14_5Input = { + readonly sessionID: Endpoint14_5Request["params"]["sessionID"] + readonly formID: Endpoint14_5Request["params"]["formID"] + readonly answer: Endpoint14_5Request["payload"]["answer"] +} +const Endpoint14_5 = (raw: RawClient["server.form"]) => (input: Endpoint14_5Input) => + raw["session.form.reply"]({ + params: { sessionID: input["sessionID"], formID: input["formID"] }, + payload: { answer: input["answer"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint14_6Request = Parameters[0] +type Endpoint14_6Input = { + readonly sessionID: Endpoint14_6Request["params"]["sessionID"] + readonly formID: Endpoint14_6Request["params"]["formID"] +} +const Endpoint14_6 = (raw: RawClient["server.form"]) => (input: Endpoint14_6Input) => + raw["session.form.cancel"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( + Effect.mapError(mapClientError), + ) + +const adaptGroup14 = (raw: RawClient["server.form"]) => ({ + request: { list: Endpoint14_0(raw) }, + list: Endpoint14_1(raw), + create: Endpoint14_2(raw), + get: Endpoint14_3(raw), + state: Endpoint14_4(raw), + reply: Endpoint14_5(raw), + cancel: Endpoint14_6(raw), +}) + +type Endpoint15_0Request = Parameters[0] +type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] } +const Endpoint15_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint15_0Input) => + raw["permission.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint15_1Request = Parameters[0] +type Endpoint15_1Input = { readonly projectID?: Endpoint15_1Request["query"]["projectID"] } +const Endpoint15_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint15_1Input) => + raw["permission.saved.list"]({ query: { projectID: input?.["projectID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint15_2Request = Parameters[0] +type Endpoint15_2Input = { readonly id: Endpoint15_2Request["params"]["id"] } +const Endpoint15_2 = (raw: RawClient["server.permission"]) => (input: Endpoint15_2Input) => + raw["permission.saved.remove"]({ params: { id: input["id"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint15_3Request = Parameters[0] +type Endpoint15_3Input = { + readonly sessionID: Endpoint15_3Request["params"]["sessionID"] + readonly id?: Endpoint15_3Request["payload"]["id"] + readonly action: Endpoint15_3Request["payload"]["action"] + readonly resources: Endpoint15_3Request["payload"]["resources"] + readonly save?: Endpoint15_3Request["payload"]["save"] + readonly metadata?: Endpoint15_3Request["payload"]["metadata"] + readonly source?: Endpoint15_3Request["payload"]["source"] + readonly agent?: Endpoint15_3Request["payload"]["agent"] +} +const Endpoint15_3 = (raw: RawClient["server.permission"]) => (input: Endpoint15_3Input) => + raw["session.permission.create"]({ + params: { sessionID: input["sessionID"] }, + payload: { + id: input["id"], + action: input["action"], + resources: input["resources"], + save: input["save"], + metadata: input["metadata"], + source: input["source"], + agent: input["agent"], + }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint15_4Request = Parameters[0] +type Endpoint15_4Input = { readonly sessionID: Endpoint15_4Request["params"]["sessionID"] } +const Endpoint15_4 = (raw: RawClient["server.permission"]) => (input: Endpoint15_4Input) => + raw["session.permission.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint15_5Request = Parameters[0] +type Endpoint15_5Input = { + readonly sessionID: Endpoint15_5Request["params"]["sessionID"] + readonly requestID: Endpoint15_5Request["params"]["requestID"] +} +const Endpoint15_5 = (raw: RawClient["server.permission"]) => (input: Endpoint15_5Input) => + raw["session.permission.get"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint15_6Request = Parameters[0] +type Endpoint15_6Input = { + readonly sessionID: Endpoint15_6Request["params"]["sessionID"] + readonly requestID: Endpoint15_6Request["params"]["requestID"] + readonly reply: Endpoint15_6Request["payload"]["reply"] + readonly message?: Endpoint15_6Request["payload"]["message"] +} +const Endpoint15_6 = (raw: RawClient["server.permission"]) => (input: Endpoint15_6Input) => + raw["session.permission.reply"]({ + params: { sessionID: input["sessionID"], requestID: input["requestID"] }, + payload: { reply: input["reply"], message: input["message"] }, + }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup15 = (raw: RawClient["server.permission"]) => ({ + request: { list: Endpoint15_0(raw) }, + saved: { list: Endpoint15_1(raw), remove: Endpoint15_2(raw) }, + create: Endpoint15_3(raw), + list: Endpoint15_4(raw), + get: Endpoint15_5(raw), + reply: Endpoint15_6(raw), +}) + +type Endpoint16_0Request = Parameters[0] +type Endpoint16_0Input = { + readonly location?: Endpoint16_0Request["query"]["location"] + readonly path?: Endpoint16_0Request["query"]["path"] +} +const Endpoint16_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint16_0Input) => + raw["fs.list"]({ query: { location: input?.["location"], path: input?.["path"] } }).pipe( + Effect.mapError(mapClientError), + ) + +type Endpoint16_1Request = Parameters[0] +type Endpoint16_1Input = { + readonly location?: Endpoint16_1Request["query"]["location"] + readonly query: Endpoint16_1Request["query"]["query"] + readonly type?: Endpoint16_1Request["query"]["type"] + readonly limit?: Endpoint16_1Request["query"]["limit"] +} +const Endpoint16_1 = (raw: RawClient["server.fs"]) => (input: Endpoint16_1Input) => + raw["fs.find"]({ + query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] }, + }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup16 = (raw: RawClient["server.fs"]) => ({ list: Endpoint16_0(raw), find: Endpoint16_1(raw) }) + +type Endpoint17_0Request = Parameters[0] +type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] } +const Endpoint17_0 = (raw: RawClient["server.command"]) => (input?: Endpoint17_0Input) => + raw["command.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup17 = (raw: RawClient["server.command"]) => ({ list: Endpoint17_0(raw) }) + +type Endpoint18_0Request = Parameters[0] +type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] } +const Endpoint18_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint18_0Input) => + raw["skill.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup18 = (raw: RawClient["server.skill"]) => ({ list: Endpoint18_0(raw) }) + +const Endpoint19_0 = (raw: RawClient["server.event"]) => () => + Stream.unwrap( + raw["event.subscribe"]({}).pipe( + Effect.mapError(mapClientError), + Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), + ), + ) + +const adaptGroup19 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint19_0(raw) }) + +type Endpoint20_0Request = Parameters[0] +type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] } +const Endpoint20_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_0Input) => + raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint20_1Request = Parameters[0] +type Endpoint20_1Input = { + readonly location?: Endpoint20_1Request["query"]["location"] + readonly command?: Endpoint20_1Request["payload"]["command"] + readonly args?: Endpoint20_1Request["payload"]["args"] + readonly cwd?: Endpoint20_1Request["payload"]["cwd"] + readonly title?: Endpoint20_1Request["payload"]["title"] + readonly env?: Endpoint20_1Request["payload"]["env"] +} +const Endpoint20_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_1Input) => + raw["pty.create"]({ + query: { location: input?.["location"] }, + payload: { + command: input?.["command"], + args: input?.["args"], + cwd: input?.["cwd"], + title: input?.["title"], + env: input?.["env"], + }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint20_2Request = Parameters[0] +type Endpoint20_2Input = { + readonly ptyID: Endpoint20_2Request["params"]["ptyID"] + readonly location?: Endpoint20_2Request["query"]["location"] +} +const Endpoint20_2 = (raw: RawClient["server.pty"]) => (input: Endpoint20_2Input) => + raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ) + +type Endpoint20_3Request = Parameters[0] +type Endpoint20_3Input = { + readonly ptyID: Endpoint20_3Request["params"]["ptyID"] + readonly location?: Endpoint20_3Request["query"]["location"] + readonly title?: Endpoint20_3Request["payload"]["title"] + readonly size?: Endpoint20_3Request["payload"]["size"] +} +const Endpoint20_3 = (raw: RawClient["server.pty"]) => (input: Endpoint20_3Input) => + raw["pty.update"]({ + params: { ptyID: input["ptyID"] }, + query: { location: input["location"] }, + payload: { title: input["title"], size: input["size"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint20_4Request = Parameters[0] +type Endpoint20_4Input = { + readonly ptyID: Endpoint20_4Request["params"]["ptyID"] + readonly location?: Endpoint20_4Request["query"]["location"] +} +const Endpoint20_4 = (raw: RawClient["server.pty"]) => (input: Endpoint20_4Input) => + raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ) + +const adaptGroup20 = (raw: RawClient["server.pty"]) => ({ + list: Endpoint20_0(raw), + create: Endpoint20_1(raw), + get: Endpoint20_2(raw), + update: Endpoint20_3(raw), + remove: Endpoint20_4(raw), +}) + +type Endpoint21_0Request = Parameters[0] +type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] } +const Endpoint21_0 = (raw: RawClient["server.shell"]) => (input?: Endpoint21_0Input) => + raw["shell.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint21_1Request = Parameters[0] +type Endpoint21_1Input = { + readonly location?: Endpoint21_1Request["query"]["location"] + readonly command: Endpoint21_1Request["payload"]["command"] + readonly cwd?: Endpoint21_1Request["payload"]["cwd"] + readonly timeout: Endpoint21_1Request["payload"]["timeout"] + readonly metadata?: Endpoint21_1Request["payload"]["metadata"] +} +const Endpoint21_1 = (raw: RawClient["server.shell"]) => (input: Endpoint21_1Input) => + raw["shell.create"]({ + query: { location: input["location"] }, + payload: { command: input["command"], cwd: input["cwd"], timeout: input["timeout"], metadata: input["metadata"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint21_2Request = Parameters[0] +type Endpoint21_2Input = { + readonly id: Endpoint21_2Request["params"]["id"] + readonly location?: Endpoint21_2Request["query"]["location"] +} +const Endpoint21_2 = (raw: RawClient["server.shell"]) => (input: Endpoint21_2Input) => + raw["shell.get"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ) + +type Endpoint21_3Request = Parameters[0] +type Endpoint21_3Input = { + readonly id: Endpoint21_3Request["params"]["id"] + readonly location?: Endpoint21_3Request["query"]["location"] + readonly timeout: Endpoint21_3Request["payload"]["timeout"] +} +const Endpoint21_3 = (raw: RawClient["server.shell"]) => (input: Endpoint21_3Input) => + raw["shell.timeout"]({ + params: { id: input["id"] }, + query: { location: input["location"] }, + payload: { timeout: input["timeout"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint21_4Request = Parameters[0] +type Endpoint21_4Input = { + readonly id: Endpoint21_4Request["params"]["id"] + readonly location?: Endpoint21_4Request["query"]["location"] + readonly cursor?: Endpoint21_4Request["query"]["cursor"] + readonly limit?: Endpoint21_4Request["query"]["limit"] +} +const Endpoint21_4 = (raw: RawClient["server.shell"]) => (input: Endpoint21_4Input) => + raw["shell.output"]({ + params: { id: input["id"] }, + query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint21_5Request = Parameters[0] +type Endpoint21_5Input = { + readonly id: Endpoint21_5Request["params"]["id"] + readonly location?: Endpoint21_5Request["query"]["location"] +} +const Endpoint21_5 = (raw: RawClient["server.shell"]) => (input: Endpoint21_5Input) => + raw["shell.remove"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ) + +const adaptGroup21 = (raw: RawClient["server.shell"]) => ({ + list: Endpoint21_0(raw), + create: Endpoint21_1(raw), + get: Endpoint21_2(raw), + timeout: Endpoint21_3(raw), + output: Endpoint21_4(raw), + remove: Endpoint21_5(raw), +}) + +type Endpoint22_0Request = Parameters[0] +type Endpoint22_0Input = { readonly location?: Endpoint22_0Request["query"]["location"] } +const Endpoint22_0 = (raw: RawClient["server.question"]) => (input?: Endpoint22_0Input) => + raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint22_1Request = Parameters[0] +type Endpoint22_1Input = { readonly sessionID: Endpoint22_1Request["params"]["sessionID"] } +const Endpoint22_1 = (raw: RawClient["server.question"]) => (input: Endpoint22_1Input) => + raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint22_2Request = Parameters[0] +type Endpoint22_2Input = { + readonly sessionID: Endpoint22_2Request["params"]["sessionID"] + readonly requestID: Endpoint22_2Request["params"]["requestID"] + readonly answers: Endpoint22_2Request["payload"]["answers"] +} +const Endpoint22_2 = (raw: RawClient["server.question"]) => (input: Endpoint22_2Input) => + raw["session.question.reply"]({ + params: { sessionID: input["sessionID"], requestID: input["requestID"] }, + payload: { answers: input["answers"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint22_3Request = Parameters[0] +type Endpoint22_3Input = { + readonly sessionID: Endpoint22_3Request["params"]["sessionID"] + readonly requestID: Endpoint22_3Request["params"]["requestID"] +} +const Endpoint22_3 = (raw: RawClient["server.question"]) => (input: Endpoint22_3Input) => + raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( + Effect.mapError(mapClientError), + ) + +const adaptGroup22 = (raw: RawClient["server.question"]) => ({ + request: { list: Endpoint22_0(raw) }, + list: Endpoint22_1(raw), + reply: Endpoint22_2(raw), + reject: Endpoint22_3(raw), +}) + +type Endpoint23_0Request = Parameters[0] +type Endpoint23_0Input = { readonly location?: Endpoint23_0Request["query"]["location"] } +const Endpoint23_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint23_0Input) => + raw["reference.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup23 = (raw: RawClient["server.reference"]) => ({ list: Endpoint23_0(raw) }) + +type Endpoint24_0Request = Parameters[0] +type Endpoint24_0Input = { + readonly projectID: Endpoint24_0Request["params"]["projectID"] + readonly location?: Endpoint24_0Request["query"]["location"] + readonly strategy: Endpoint24_0Request["payload"]["strategy"] + readonly directory: Endpoint24_0Request["payload"]["directory"] + readonly name?: Endpoint24_0Request["payload"]["name"] +} +const Endpoint24_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint24_0Input) => + raw["projectCopy.create"]({ + params: { projectID: input["projectID"] }, + query: { location: input["location"] }, + payload: { strategy: input["strategy"], directory: input["directory"], name: input["name"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint24_1Request = Parameters[0] +type Endpoint24_1Input = { + readonly projectID: Endpoint24_1Request["params"]["projectID"] + readonly location?: Endpoint24_1Request["query"]["location"] + readonly directory: Endpoint24_1Request["payload"]["directory"] + readonly force: Endpoint24_1Request["payload"]["force"] +} +const Endpoint24_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint24_1Input) => + raw["projectCopy.remove"]({ + params: { projectID: input["projectID"] }, + query: { location: input["location"] }, + payload: { directory: input["directory"], force: input["force"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint24_2Request = Parameters[0] +type Endpoint24_2Input = { + readonly projectID: Endpoint24_2Request["params"]["projectID"] + readonly location?: Endpoint24_2Request["query"]["location"] +} +const Endpoint24_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint24_2Input) => + raw["projectCopy.refresh"]({ + params: { projectID: input["projectID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup24 = (raw: RawClient["server.projectCopy"]) => ({ + create: Endpoint24_0(raw), + remove: Endpoint24_1(raw), + refresh: Endpoint24_2(raw), +}) + +type Endpoint25_0Request = Parameters[0] +type Endpoint25_0Input = { readonly location?: Endpoint25_0Request["query"]["location"] } +const Endpoint25_0 = (raw: RawClient["server.vcs"]) => (input?: Endpoint25_0Input) => + raw["vcs.status"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint25_1Request = Parameters[0] +type Endpoint25_1Input = { + readonly location?: Endpoint25_1Request["query"]["location"] + readonly mode: Endpoint25_1Request["query"]["mode"] + readonly context?: Endpoint25_1Request["query"]["context"] +} +const Endpoint25_1 = (raw: RawClient["server.vcs"]) => (input: Endpoint25_1Input) => + raw["vcs.diff"]({ query: { location: input["location"], mode: input["mode"], context: input["context"] } }).pipe( + Effect.mapError(mapClientError), + ) + +const adaptGroup25 = (raw: RawClient["server.vcs"]) => ({ status: Endpoint25_0(raw), diff: Endpoint25_1(raw) }) + +const Endpoint26_0 = (raw: RawClient["server.debug"]) => () => + raw["debug.location"]({}).pipe(Effect.mapError(mapClientError)) + +type Endpoint26_1Request = Parameters[0] +type Endpoint26_1Input = { readonly location?: Endpoint26_1Request["query"]["location"] } +const Endpoint26_1 = (raw: RawClient["server.debug"]) => (input?: Endpoint26_1Input) => + raw["debug.location.evict"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup26 = (raw: RawClient["server.debug"]) => ({ + location: { list: Endpoint26_0(raw), evict: Endpoint26_1(raw) }, +}) + +const adaptClient = (raw: RawClient) => ({ + health: adaptGroup0(raw["server.health"]), + server: adaptGroup1(raw["server.server"]), + location: adaptGroup2(raw["server.location"]), + agent: adaptGroup3(raw["server.agent"]), + plugin: adaptGroup4(raw["server.plugin"]), + session: adaptGroup5(raw["server.session"]), + message: adaptGroup6(raw["server.message"]), + model: adaptGroup7(raw["server.model"]), + generate: adaptGroup8(raw["server.generate"]), + provider: adaptGroup9(raw["server.provider"]), + integration: adaptGroup10(raw["server.integration"]), + mcp: adaptGroup11(raw["server.mcp"]), + credential: adaptGroup12(raw["server.credential"]), + project: adaptGroup13(raw["server.project"]), + form: adaptGroup14(raw["server.form"]), + permission: adaptGroup15(raw["server.permission"]), + file: adaptGroup16(raw["server.fs"]), + command: adaptGroup17(raw["server.command"]), + skill: adaptGroup18(raw["server.skill"]), + event: adaptGroup19(raw["server.event"]), + pty: adaptGroup20(raw["server.pty"]), + shell: adaptGroup21(raw["server.shell"]), + question: adaptGroup22(raw["server.question"]), + reference: adaptGroup23(raw["server.reference"]), + projectCopy: adaptGroup24(raw["server.projectCopy"]), + vcs: adaptGroup25(raw["server.vcs"]), + debug: adaptGroup26(raw["server.debug"]), +}) + +export const make = (options?: { readonly baseUrl?: URL | string }) => + HttpApiClient.make(ClientApi, options).pipe(Effect.map(adaptClient)) diff --git a/packages/client/src/generated-effect/index.ts b/packages/client/src/effect/generated/index.ts similarity index 100% rename from packages/client/src/generated-effect/index.ts rename to packages/client/src/effect/generated/index.ts diff --git a/packages/client/src/effect.ts b/packages/client/src/effect/index.ts similarity index 69% rename from packages/client/src/effect.ts rename to packages/client/src/effect/index.ts index b580c7f48a..27424aad82 100644 --- a/packages/client/src/effect.ts +++ b/packages/client/src/effect/index.ts @@ -1,10 +1,29 @@ // TODO: Keep additional network capabilities inside Schema and Protocol as the client grows; /effect must never import // Core or Server. Preserve these datatype exports so internal model reorganizations do not require caller migrations. -export * from "./generated-effect/index" +import type { Effect } from "effect" + +export * from "./generated/index" +export type { + AgentApi, + AppApi, + CatalogApi, + CommandApi, + EventApi, + IntegrationApi, + ModelApi, + PluginApi, + ProviderApi, + ReferenceApi, + SessionApi, + SkillApi, +} from "./api.js" export { Agent } from "@opencode-ai/schema/agent" export { Command } from "@opencode-ai/schema/command" export { Credential } from "@opencode-ai/schema/credential" +export { Event } from "@opencode-ai/schema/event" +export { EventLog } from "@opencode-ai/schema/event-log" export { FileSystem } from "@opencode-ai/schema/filesystem" +export { Form } from "@opencode-ai/schema/form" export { Integration } from "@opencode-ai/schema/integration" export { Location } from "@opencode-ai/schema/location" export { Model } from "@opencode-ai/schema/model" @@ -18,8 +37,10 @@ export { Question } from "@opencode-ai/schema/question" export { Reference } from "@opencode-ai/schema/reference" export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema" export { Session } from "@opencode-ai/schema/session" -export { SessionInput } from "@opencode-ai/schema/session-input" +export { SessionPending } from "@opencode-ai/schema/session-pending" export { SessionMessage } from "@opencode-ai/schema/session-message" export { Skill } from "@opencode-ai/schema/skill" export { Prompt } from "@opencode-ai/schema/prompt" +export { PromptInput } from "@opencode-ai/schema/prompt-input" export type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event" +export type OpenCodeClient = Effect.Success> diff --git a/packages/client/src/effect/service.ts b/packages/client/src/effect/service.ts new file mode 100644 index 0000000000..de452a2843 --- /dev/null +++ b/packages/client/src/effect/service.ts @@ -0,0 +1,294 @@ +import { ServiceStatus } from "@opencode-ai/protocol/groups/health" +import { Effect, FileSystem, Option, Schedule, Schema } from "effect" +import { spawn, type ChildProcess } from "node:child_process" +import { homedir } from "node:os" +import { join } from "node:path" +import type { DiscoverOptions, Endpoint, EnsureOptions, StopOptions } from "../service.js" + +export * from "../service.js" +/** Contents of the local service registration file. */ +export type Info = import("../service.js").Info + +// Find, start, and stop the local opencode background service. +// +// The service daemon advertises itself through a registration file in the +// user's state directory: url, pid, version, and the private password, with +// 0600 permissions. That file is the complete discovery contract — reading it +// is all a client needs to connect. The daemon's own configuration (port, +// persisted password) is CLI-owned and never read here. + +type Contender = { + readonly child: ChildProcess + readonly error: () => Error | undefined +} + +// Read-only lookup: registration file plus health check and version gate. +// Never spawns; escalation to ensure() is the caller's policy. +/** Discover a healthy, compatible local service without starting one. */ +export const discover = Effect.fn("service.discover")(function* (options: DiscoverOptions = {}) { + return (yield* discoverLocal(options))?.endpoint +}) + +/** Recognize an authenticated compatible service bound to an expected URL, including while it starts or fails. */ +export const incumbent = Effect.fn("service.incumbent")(function* ( + options: DiscoverOptions & { readonly url: string }, +) { + const info = yield* read(options.file) + const found = info === undefined ? undefined : yield* probe({ ...info, url: options.url }) + if (found === undefined || found.legacy) return undefined + if (options.version !== undefined && found.version !== options.version) return undefined + return { endpoint: found.endpoint, state: found.state } +}) + +const discoverLocal = Effect.fnUntraced(function* (options: DiscoverOptions) { + const found = (yield* registered(options.file)).service + if (found?.state !== "ready") return undefined + if (options.version !== undefined && found.version !== options.version) return undefined + return found +}) + +// Idempotent ensure-running: reuses a healthy compatible server, replaces a +// version-mismatched one, and otherwise spawns small contenders until a server +// becomes discoverable. A contender is never killed merely for slow startup. +/** Ensure a healthy, compatible local service is running. */ +export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOptions = {}) { + const contenders = new Set() + let announced = false + let lastSpawn = 0 + let spawnDelay = 5_000 + let ownerHeld = false + const announce = (reason: "missing" | "version-mismatch", previousVersion?: string) => + Effect.sync(() => { + if (announced) return + announced = true + options.onStart?.(reason, previousVersion) + }) + const spawnContender = Effect.gen(function* () { + const [command, ...args] = options.command ?? ["opencode", "serve", "--service"] + if (command === undefined) return yield* Effect.fail(new Error("Missing service command")) + return yield* Effect.try({ + try: () => { + const child = spawn(command, args, { detached: true, stdio: "ignore" }) + let error: Error | undefined + child.once("error", (cause) => { + error = new Error("Failed to start server", { cause }) + }) + child.unref() + return { child, error: () => error } + }, + catch: (cause) => new Error("Failed to start server", { cause }), + }) + }) + const found = yield* Effect.gen(function* () { + const registration = yield* registered(options.file, true) + const info = registration.info + const service = registration.service + if (service !== undefined) { + ownerHeld = false + spawnDelay = 5_000 + const compatible = !service.legacy && (options.version === undefined || service.version === options.version) + if (compatible && service.state === "ready") return Option.some(service) + if (compatible && service.state === "failed") + return yield* Effect.fail(new Error("Background service failed to start")) + if (compatible) return Option.none() + yield* announce("version-mismatch", service.version) + yield* kill(service, options).pipe(Effect.ignore) + lastSpawn = 0 + return Option.none() + } else if (lastSpawn === 0 && info !== undefined) lastSpawn = Date.now() + + const failure = [...contenders].map(contenderFailure).find((error): error is Error => error !== undefined) + if (failure !== undefined) return yield* Effect.fail(failure) + const finished = [...contenders].filter(contenderFinished) + if (finished.some((item) => item.child.exitCode === 0)) { + ownerHeld = true + spawnDelay = Math.min(spawnDelay * 2, 30_000) + } + finished.forEach((item) => contenders.delete(item)) + // Keep one candidate plus one lock probe so a pre-lock stall cannot block recovery. + if (contenders.size < 2 && Date.now() - lastSpawn >= spawnDelay) { + yield* announce("missing") + contenders.add(yield* spawnContender) + lastSpawn = Date.now() + } + return Option.none() + }).pipe( + Effect.repeat({ + until: Option.isSome, + schedule: Schedule.max([Schedule.spaced("1 second"), Schedule.recurs(120)]), + }), + ) + if (Option.isNone(found)) + return yield* Effect.fail(new Error("Timed out waiting for the background service to start")) + return found.value.endpoint +}) + +function contenderFailure(contender: Contender) { + const error = contender.error() + if (error !== undefined) return error + if (contender.child.exitCode !== null && contender.child.exitCode !== 0) + return new Error(`Server process exited with code ${contender.child.exitCode}`) + if (contender.child.signalCode !== null) + return new Error(`Server process terminated by ${contender.child.signalCode}`) + return undefined +} + +function contenderFinished(contender: Contender) { + return contender.error() !== undefined || contender.child.exitCode !== null || contender.child.signalCode !== null +} + +/** Stop the registered local service. */ +export const stop = Effect.fn("service.stop")(function* (options: StopOptions = {}) { + const existing = yield* find(options) + if (existing !== undefined) yield* kill(existing, options) +}) + +function fallback() { + const state = process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state") + return join(state, "opencode", "service.json") +} + +/** Create HTTP authentication headers for a service endpoint. */ +export function headers(endpoint: Endpoint) { + if (endpoint.auth === undefined) return undefined + return { authorization: "Basic " + btoa(endpoint.auth.username + ":" + endpoint.auth.password) } +} + +/** Schema for the local service registration file. */ +export const Info = Schema.Struct({ + id: Schema.optional(Schema.String), + version: Schema.optional(Schema.String), + url: Schema.String, + pid: Schema.Int.check(Schema.isGreaterThan(0)), + password: Schema.optional(Schema.String), +}) + +const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Info)) +const decodeHealth = Schema.decodeUnknownOption(ServiceStatus.Health) +const decodeLegacyHealth = Schema.decodeUnknownOption(Schema.Struct({ healthy: Schema.Literal(true) })) + +// A missing or corrupt file means no valid info; callers treat both +// the same (the registering server self-evicts, clients rediscover). +const read = Effect.fnUntraced(function* (file?: string) { + const fs = yield* FileSystem.FileSystem + const text = yield* fs.readFileString(file ?? fallback()).pipe(Effect.option) + if (Option.isNone(text)) return undefined + return yield* decode(text.value).pipe(Effect.option, Effect.map(Option.getOrUndefined)) +}) + +type LocalService = { + readonly info: Info + readonly endpoint: Endpoint + readonly version?: string + readonly state: "ready" | "waiting" | "failed" + readonly legacy: boolean +} + +const probe = Effect.fnUntraced(function* (info: Info, allowLegacy = false) { + const endpoint = { + url: info.url, + auth: + info.password === undefined + ? undefined + : { type: "basic" as const, username: "opencode", password: info.password }, + } satisfies Endpoint + const response = yield* Effect.tryPromise(() => + fetch(new URL("/api/health", info.url), { + headers: headers(endpoint), + signal: AbortSignal.timeout(2_000), + }), + ).pipe(Effect.option, Effect.map(Option.getOrUndefined)) + if (response === undefined) return undefined + const body = yield* Effect.tryPromise(() => response.json()).pipe(Effect.option, Effect.map(Option.getOrUndefined)) + const health = decodeHealth(body) + if (Option.isSome(health)) { + if (health.value.pid !== info.pid) return undefined + if (info.version !== undefined && health.value.version !== info.version) return undefined + return { + info, + endpoint, + version: health.value.version, + state: response.ok ? "ready" : response.status === 500 ? "failed" : "waiting", + legacy: false, + } satisfies LocalService + } + if ( + !allowLegacy || + Option.isNone(decodeLegacyHealth(body)) || + (typeof body === "object" && body !== null && ("version" in body || "pid" in body)) + ) + return undefined + return { info, endpoint, state: "ready", legacy: true } satisfies LocalService +}) + +const registered = Effect.fnUntraced(function* (file?: string, allowLegacy = false) { + const info = yield* read(file) + if (info === undefined) return { info: undefined, service: undefined } + return { info, service: yield* probe(info, allowLegacy) } +}) + +// Health-checked lookup without the version gate: lifecycle operations must be +// able to see (and replace or stop) a server from a different version. +const find = Effect.fnUntraced(function* (options: { readonly file?: string }) { + return (yield* registered(options.file, true)).service +}) + +// 50ms cadence bounded at ~5s, shared by stop escalation and each ensure +// discovery window. +const poll = Schedule.max([Schedule.spaced("50 millis"), Schedule.recurs(100)]) + +const signal = (pid: number, name: NodeJS.Signals) => + Effect.try({ try: () => process.kill(pid, name), catch: (cause) => cause }).pipe(Effect.ignore) + +const stopped = Effect.fnUntraced(function* (pid: number) { + const running = yield* Effect.try({ try: () => process.kill(pid, 0), catch: () => false }).pipe( + Effect.orElseSucceed(() => false), + ) + if (!running) return true + return yield* Effect.fail(new Error(`Server process ${pid} is still running`)) +}) + +function same(left: Info, right: Info) { + return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid +} + +const kill = Effect.fnUntraced(function* (service: LocalService, options: { readonly file?: string }) { + const requested = yield* requestStop(service) + if (requested === "rejected") return + if (requested === "unsupported") { + // A stale registration may point at a reused PID. Authenticate again + // immediately before the legacy signal fallback. + const current = yield* find(options) + if (current === undefined || !same(current.info, service.info)) return + yield* signal(service.info.pid, "SIGTERM") + } + const done = yield* stopped(service.info.pid).pipe(Effect.retry(poll), Effect.option) + if (Option.isSome(done)) return + + const latest = yield* find(options) + if (latest === undefined || !same(latest.info, service.info)) return + yield* signal(service.info.pid, "SIGKILL") + yield* stopped(service.info.pid).pipe(Effect.retry(poll)) +}) + +const decodeStopResponse = Schema.decodeUnknownOption(ServiceStatus.StopResponse) + +const requestStop = Effect.fnUntraced(function* (service: LocalService) { + if (service.info.id === undefined || service.legacy) return "unsupported" as const + const response = yield* Effect.tryPromise(() => + fetch(new URL("/api/service/stop", service.info.url), { + method: "POST", + headers: { ...headers(service.endpoint), "content-type": "application/json" }, + body: JSON.stringify({ instanceID: service.info.id }), + signal: AbortSignal.timeout(2_000), + }), + ).pipe(Effect.option, Effect.map(Option.getOrUndefined)) + if (response === undefined || response.status === 404 || response.status === 405) return "unsupported" as const + const body = yield* Effect.tryPromise(() => response.json()).pipe(Effect.option, Effect.map(Option.getOrUndefined)) + const decoded = decodeStopResponse(body) + if (!response.ok || Option.isNone(decoded) || !decoded.value.accepted) return "rejected" as const + return "accepted" as const +}) + +/** Effect-based local service lifecycle operations. */ +export const Service = { discover, incumbent, ensure, stop, headers, Info } diff --git a/packages/client/src/generated-effect/client.ts b/packages/client/src/generated-effect/client.ts deleted file mode 100644 index 024c978280..0000000000 --- a/packages/client/src/generated-effect/client.ts +++ /dev/null @@ -1,706 +0,0 @@ -// Generated by @opencode-ai/httpapi-codegen. Do not edit. -import { Effect, Stream, Schema } from "effect" -import { Sse } from "effect/unstable/encoding" -import { HttpClientError } from "effect/unstable/http" -import { HttpApiClient } from "effect/unstable/httpapi" -import { ClientApi } from "../contract" -import { ClientError } from "./client-error" - -type RawClient = HttpApiClient.ForApi - -const mapClientError = (error: E) => - HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) - ? new ClientError({ cause: error }) - : error - -const Endpoint0_0 = (raw: RawClient["server.health"]) => () => - raw["health.get"]({}).pipe(Effect.mapError(mapClientError)) - -const adaptGroup0 = (raw: RawClient["server.health"]) => ({ get: Endpoint0_0(raw) }) - -type Endpoint1_0Request = Parameters[0] -type Endpoint1_0Input = { readonly location?: Endpoint1_0Request["query"]["location"] } -const Endpoint1_0 = (raw: RawClient["server.location"]) => (input?: Endpoint1_0Input) => - raw["location.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup1 = (raw: RawClient["server.location"]) => ({ get: Endpoint1_0(raw) }) - -type Endpoint2_0Request = Parameters[0] -type Endpoint2_0Input = { readonly location?: Endpoint2_0Request["query"]["location"] } -const Endpoint2_0 = (raw: RawClient["server.agent"]) => (input?: Endpoint2_0Input) => - raw["agent.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup2 = (raw: RawClient["server.agent"]) => ({ list: Endpoint2_0(raw) }) - -type Endpoint3_0Request = Parameters[0] -type Endpoint3_0Input = { - readonly workspace?: Endpoint3_0Request["query"]["workspace"] - readonly limit?: Endpoint3_0Request["query"]["limit"] - readonly order?: Endpoint3_0Request["query"]["order"] - readonly search?: Endpoint3_0Request["query"]["search"] - readonly directory?: Endpoint3_0Request["query"]["directory"] - readonly project?: Endpoint3_0Request["query"]["project"] - readonly subpath?: Endpoint3_0Request["query"]["subpath"] - readonly cursor?: Endpoint3_0Request["query"]["cursor"] -} -const Endpoint3_0 = (raw: RawClient["server.session"]) => (input?: Endpoint3_0Input) => - raw["session.list"]({ - query: { - workspace: input?.["workspace"], - limit: input?.["limit"], - order: input?.["order"], - search: input?.["search"], - directory: input?.["directory"], - project: input?.["project"], - subpath: input?.["subpath"], - cursor: input?.["cursor"], - }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint3_1Request = Parameters[0] -type Endpoint3_1Input = { - readonly id?: Endpoint3_1Request["payload"]["id"] - readonly agent?: Endpoint3_1Request["payload"]["agent"] - readonly model?: Endpoint3_1Request["payload"]["model"] - readonly location?: Endpoint3_1Request["payload"]["location"] -} -const Endpoint3_1 = (raw: RawClient["server.session"]) => (input?: Endpoint3_1Input) => - raw["session.create"]({ - payload: { id: input?.["id"], agent: input?.["agent"], model: input?.["model"], location: input?.["location"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -const Endpoint3_2 = (raw: RawClient["server.session"]) => () => - raw["session.active"]({}).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint3_3Request = Parameters[0] -type Endpoint3_3Input = { readonly sessionID: Endpoint3_3Request["params"]["sessionID"] } -const Endpoint3_3 = (raw: RawClient["server.session"]) => (input: Endpoint3_3Input) => - raw["session.get"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint3_4Request = Parameters[0] -type Endpoint3_4Input = { - readonly sessionID: Endpoint3_4Request["params"]["sessionID"] - readonly agent: Endpoint3_4Request["payload"]["agent"] -} -const Endpoint3_4 = (raw: RawClient["server.session"]) => (input: Endpoint3_4Input) => - raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe( - Effect.mapError(mapClientError), - ) - -type Endpoint3_5Request = Parameters[0] -type Endpoint3_5Input = { - readonly sessionID: Endpoint3_5Request["params"]["sessionID"] - readonly model: Endpoint3_5Request["payload"]["model"] -} -const Endpoint3_5 = (raw: RawClient["server.session"]) => (input: Endpoint3_5Input) => - raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe( - Effect.mapError(mapClientError), - ) - -type Endpoint3_6Request = Parameters[0] -type Endpoint3_6Input = { - readonly sessionID: Endpoint3_6Request["params"]["sessionID"] - readonly id?: Endpoint3_6Request["payload"]["id"] - readonly prompt: Endpoint3_6Request["payload"]["prompt"] - readonly delivery?: Endpoint3_6Request["payload"]["delivery"] - readonly resume?: Endpoint3_6Request["payload"]["resume"] -} -const Endpoint3_6 = (raw: RawClient["server.session"]) => (input: Endpoint3_6Input) => - raw["session.prompt"]({ - params: { sessionID: input["sessionID"] }, - payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint3_7Request = Parameters[0] -type Endpoint3_7Input = { readonly sessionID: Endpoint3_7Request["params"]["sessionID"] } -const Endpoint3_7 = (raw: RawClient["server.session"]) => (input: Endpoint3_7Input) => - raw["session.compact"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint3_8Request = Parameters[0] -type Endpoint3_8Input = { readonly sessionID: Endpoint3_8Request["params"]["sessionID"] } -const Endpoint3_8 = (raw: RawClient["server.session"]) => (input: Endpoint3_8Input) => - raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint3_9Request = Parameters[0] -type Endpoint3_9Input = { - readonly sessionID: Endpoint3_9Request["params"]["sessionID"] - readonly messageID: Endpoint3_9Request["payload"]["messageID"] - readonly files?: Endpoint3_9Request["payload"]["files"] -} -const Endpoint3_9 = (raw: RawClient["server.session"]) => (input: Endpoint3_9Input) => - raw["session.revert.stage"]({ - params: { sessionID: input["sessionID"] }, - payload: { messageID: input["messageID"], files: input["files"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint3_10Request = Parameters[0] -type Endpoint3_10Input = { readonly sessionID: Endpoint3_10Request["params"]["sessionID"] } -const Endpoint3_10 = (raw: RawClient["server.session"]) => (input: Endpoint3_10Input) => - raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint3_11Request = Parameters[0] -type Endpoint3_11Input = { readonly sessionID: Endpoint3_11Request["params"]["sessionID"] } -const Endpoint3_11 = (raw: RawClient["server.session"]) => (input: Endpoint3_11Input) => - raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint3_12Request = Parameters[0] -type Endpoint3_12Input = { readonly sessionID: Endpoint3_12Request["params"]["sessionID"] } -const Endpoint3_12 = (raw: RawClient["server.session"]) => (input: Endpoint3_12Input) => - raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint3_13Request = Parameters[0] -type Endpoint3_13Input = { - readonly sessionID: Endpoint3_13Request["params"]["sessionID"] - readonly limit?: Endpoint3_13Request["query"]["limit"] - readonly after?: Endpoint3_13Request["query"]["after"] -} -const Endpoint3_13 = (raw: RawClient["server.session"]) => (input: Endpoint3_13Input) => - raw["session.history"]({ - params: { sessionID: input["sessionID"] }, - query: { limit: input["limit"], after: input["after"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint3_14Request = Parameters[0] -type Endpoint3_14Input = { - readonly sessionID: Endpoint3_14Request["params"]["sessionID"] - readonly after?: Endpoint3_14Request["query"]["after"] -} -const Endpoint3_14 = (raw: RawClient["server.session"]) => (input: Endpoint3_14Input) => - Stream.unwrap( - raw["session.events"]({ params: { sessionID: input["sessionID"] }, query: { after: input["after"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), - ), - ) - -type Endpoint3_15Request = Parameters[0] -type Endpoint3_15Input = { readonly sessionID: Endpoint3_15Request["params"]["sessionID"] } -const Endpoint3_15 = (raw: RawClient["server.session"]) => (input: Endpoint3_15Input) => - raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint3_16Request = Parameters[0] -type Endpoint3_16Input = { - readonly sessionID: Endpoint3_16Request["params"]["sessionID"] - readonly messageID: Endpoint3_16Request["params"]["messageID"] -} -const Endpoint3_16 = (raw: RawClient["server.session"]) => (input: Endpoint3_16Input) => - raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -const adaptGroup3 = (raw: RawClient["server.session"]) => ({ - list: Endpoint3_0(raw), - create: Endpoint3_1(raw), - active: Endpoint3_2(raw), - get: Endpoint3_3(raw), - switchAgent: Endpoint3_4(raw), - switchModel: Endpoint3_5(raw), - prompt: Endpoint3_6(raw), - compact: Endpoint3_7(raw), - wait: Endpoint3_8(raw), - stage: Endpoint3_9(raw), - clear: Endpoint3_10(raw), - commit: Endpoint3_11(raw), - context: Endpoint3_12(raw), - history: Endpoint3_13(raw), - events: Endpoint3_14(raw), - interrupt: Endpoint3_15(raw), - message: Endpoint3_16(raw), -}) - -type Endpoint4_0Request = Parameters[0] -type Endpoint4_0Input = { - readonly sessionID: Endpoint4_0Request["params"]["sessionID"] - readonly limit?: Endpoint4_0Request["query"]["limit"] - readonly order?: Endpoint4_0Request["query"]["order"] - readonly cursor?: Endpoint4_0Request["query"]["cursor"] -} -const Endpoint4_0 = (raw: RawClient["server.message"]) => (input: Endpoint4_0Input) => - raw["session.messages"]({ - params: { sessionID: input["sessionID"] }, - query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] }, - }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup4 = (raw: RawClient["server.message"]) => ({ list: Endpoint4_0(raw) }) - -type Endpoint5_0Request = Parameters[0] -type Endpoint5_0Input = { readonly location?: Endpoint5_0Request["query"]["location"] } -const Endpoint5_0 = (raw: RawClient["server.model"]) => (input?: Endpoint5_0Input) => - raw["model.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup5 = (raw: RawClient["server.model"]) => ({ list: Endpoint5_0(raw) }) - -type Endpoint6_0Request = Parameters[0] -type Endpoint6_0Input = { readonly location?: Endpoint6_0Request["query"]["location"] } -const Endpoint6_0 = (raw: RawClient["server.provider"]) => (input?: Endpoint6_0Input) => - raw["provider.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint6_1Request = Parameters[0] -type Endpoint6_1Input = { - readonly providerID: Endpoint6_1Request["params"]["providerID"] - readonly location?: Endpoint6_1Request["query"]["location"] -} -const Endpoint6_1 = (raw: RawClient["server.provider"]) => (input: Endpoint6_1Input) => - raw["provider.get"]({ params: { providerID: input["providerID"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), - ) - -const adaptGroup6 = (raw: RawClient["server.provider"]) => ({ list: Endpoint6_0(raw), get: Endpoint6_1(raw) }) - -type Endpoint7_0Request = Parameters[0] -type Endpoint7_0Input = { readonly location?: Endpoint7_0Request["query"]["location"] } -const Endpoint7_0 = (raw: RawClient["server.integration"]) => (input?: Endpoint7_0Input) => - raw["integration.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint7_1Request = Parameters[0] -type Endpoint7_1Input = { - readonly integrationID: Endpoint7_1Request["params"]["integrationID"] - readonly location?: Endpoint7_1Request["query"]["location"] -} -const Endpoint7_1 = (raw: RawClient["server.integration"]) => (input: Endpoint7_1Input) => - raw["integration.get"]({ - params: { integrationID: input["integrationID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint7_2Request = Parameters[0] -type Endpoint7_2Input = { - readonly integrationID: Endpoint7_2Request["params"]["integrationID"] - readonly location?: Endpoint7_2Request["query"]["location"] - readonly key: Endpoint7_2Request["payload"]["key"] - readonly label?: Endpoint7_2Request["payload"]["label"] -} -const Endpoint7_2 = (raw: RawClient["server.integration"]) => (input: Endpoint7_2Input) => - raw["integration.connect.key"]({ - params: { integrationID: input["integrationID"] }, - query: { location: input["location"] }, - payload: { key: input["key"], label: input["label"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint7_3Request = Parameters[0] -type Endpoint7_3Input = { - readonly integrationID: Endpoint7_3Request["params"]["integrationID"] - readonly location?: Endpoint7_3Request["query"]["location"] - readonly methodID: Endpoint7_3Request["payload"]["methodID"] - readonly inputs: Endpoint7_3Request["payload"]["inputs"] - readonly label?: Endpoint7_3Request["payload"]["label"] -} -const Endpoint7_3 = (raw: RawClient["server.integration"]) => (input: Endpoint7_3Input) => - raw["integration.connect.oauth"]({ - params: { integrationID: input["integrationID"] }, - query: { location: input["location"] }, - payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint7_4Request = Parameters[0] -type Endpoint7_4Input = { - readonly attemptID: Endpoint7_4Request["params"]["attemptID"] - readonly location?: Endpoint7_4Request["query"]["location"] -} -const Endpoint7_4 = (raw: RawClient["server.integration"]) => (input: Endpoint7_4Input) => - raw["integration.attempt.status"]({ - params: { attemptID: input["attemptID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint7_5Request = Parameters[0] -type Endpoint7_5Input = { - readonly attemptID: Endpoint7_5Request["params"]["attemptID"] - readonly location?: Endpoint7_5Request["query"]["location"] - readonly code?: Endpoint7_5Request["payload"]["code"] -} -const Endpoint7_5 = (raw: RawClient["server.integration"]) => (input: Endpoint7_5Input) => - raw["integration.attempt.complete"]({ - params: { attemptID: input["attemptID"] }, - query: { location: input["location"] }, - payload: { code: input["code"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint7_6Request = Parameters[0] -type Endpoint7_6Input = { - readonly attemptID: Endpoint7_6Request["params"]["attemptID"] - readonly location?: Endpoint7_6Request["query"]["location"] -} -const Endpoint7_6 = (raw: RawClient["server.integration"]) => (input: Endpoint7_6Input) => - raw["integration.attempt.cancel"]({ - params: { attemptID: input["attemptID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup7 = (raw: RawClient["server.integration"]) => ({ - list: Endpoint7_0(raw), - get: Endpoint7_1(raw), - connectKey: Endpoint7_2(raw), - connectOauth: Endpoint7_3(raw), - attemptStatus: Endpoint7_4(raw), - attemptComplete: Endpoint7_5(raw), - attemptCancel: Endpoint7_6(raw), -}) - -type Endpoint8_0Request = Parameters[0] -type Endpoint8_0Input = { - readonly credentialID: Endpoint8_0Request["params"]["credentialID"] - readonly location?: Endpoint8_0Request["query"]["location"] - readonly label: Endpoint8_0Request["payload"]["label"] -} -const Endpoint8_0 = (raw: RawClient["server.credential"]) => (input: Endpoint8_0Input) => - raw["credential.update"]({ - params: { credentialID: input["credentialID"] }, - query: { location: input["location"] }, - payload: { label: input["label"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint8_1Request = Parameters[0] -type Endpoint8_1Input = { - readonly credentialID: Endpoint8_1Request["params"]["credentialID"] - readonly location?: Endpoint8_1Request["query"]["location"] -} -const Endpoint8_1 = (raw: RawClient["server.credential"]) => (input: Endpoint8_1Input) => - raw["credential.remove"]({ - params: { credentialID: input["credentialID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup8 = (raw: RawClient["server.credential"]) => ({ update: Endpoint8_0(raw), remove: Endpoint8_1(raw) }) - -type Endpoint9_0Request = Parameters[0] -type Endpoint9_0Input = { readonly location?: Endpoint9_0Request["query"]["location"] } -const Endpoint9_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint9_0Input) => - raw["permission.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint9_1Request = Parameters[0] -type Endpoint9_1Input = { readonly projectID?: Endpoint9_1Request["query"]["projectID"] } -const Endpoint9_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint9_1Input) => - raw["permission.saved.list"]({ query: { projectID: input?.["projectID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint9_2Request = Parameters[0] -type Endpoint9_2Input = { readonly id: Endpoint9_2Request["params"]["id"] } -const Endpoint9_2 = (raw: RawClient["server.permission"]) => (input: Endpoint9_2Input) => - raw["permission.saved.remove"]({ params: { id: input["id"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint9_3Request = Parameters[0] -type Endpoint9_3Input = { - readonly sessionID: Endpoint9_3Request["params"]["sessionID"] - readonly id?: Endpoint9_3Request["payload"]["id"] - readonly action: Endpoint9_3Request["payload"]["action"] - readonly resources: Endpoint9_3Request["payload"]["resources"] - readonly save?: Endpoint9_3Request["payload"]["save"] - readonly metadata?: Endpoint9_3Request["payload"]["metadata"] - readonly source?: Endpoint9_3Request["payload"]["source"] - readonly agent?: Endpoint9_3Request["payload"]["agent"] -} -const Endpoint9_3 = (raw: RawClient["server.permission"]) => (input: Endpoint9_3Input) => - raw["session.permission.create"]({ - params: { sessionID: input["sessionID"] }, - payload: { - id: input["id"], - action: input["action"], - resources: input["resources"], - save: input["save"], - metadata: input["metadata"], - source: input["source"], - agent: input["agent"], - }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint9_4Request = Parameters[0] -type Endpoint9_4Input = { readonly sessionID: Endpoint9_4Request["params"]["sessionID"] } -const Endpoint9_4 = (raw: RawClient["server.permission"]) => (input: Endpoint9_4Input) => - raw["session.permission.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint9_5Request = Parameters[0] -type Endpoint9_5Input = { - readonly sessionID: Endpoint9_5Request["params"]["sessionID"] - readonly requestID: Endpoint9_5Request["params"]["requestID"] -} -const Endpoint9_5 = (raw: RawClient["server.permission"]) => (input: Endpoint9_5Input) => - raw["session.permission.get"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint9_6Request = Parameters[0] -type Endpoint9_6Input = { - readonly sessionID: Endpoint9_6Request["params"]["sessionID"] - readonly requestID: Endpoint9_6Request["params"]["requestID"] - readonly reply: Endpoint9_6Request["payload"]["reply"] - readonly message?: Endpoint9_6Request["payload"]["message"] -} -const Endpoint9_6 = (raw: RawClient["server.permission"]) => (input: Endpoint9_6Input) => - raw["session.permission.reply"]({ - params: { sessionID: input["sessionID"], requestID: input["requestID"] }, - payload: { reply: input["reply"], message: input["message"] }, - }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup9 = (raw: RawClient["server.permission"]) => ({ - listRequests: Endpoint9_0(raw), - listSaved: Endpoint9_1(raw), - removeSaved: Endpoint9_2(raw), - create: Endpoint9_3(raw), - list: Endpoint9_4(raw), - get: Endpoint9_5(raw), - reply: Endpoint9_6(raw), -}) - -type Endpoint10_0Request = Parameters[0] -type Endpoint10_0Input = { - readonly location?: Endpoint10_0Request["query"]["location"] - readonly path?: Endpoint10_0Request["query"]["path"] -} -const Endpoint10_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint10_0Input) => - raw["fs.list"]({ query: { location: input?.["location"], path: input?.["path"] } }).pipe( - Effect.mapError(mapClientError), - ) - -type Endpoint10_1Request = Parameters[0] -type Endpoint10_1Input = { - readonly location?: Endpoint10_1Request["query"]["location"] - readonly query: Endpoint10_1Request["query"]["query"] - readonly type?: Endpoint10_1Request["query"]["type"] - readonly limit?: Endpoint10_1Request["query"]["limit"] -} -const Endpoint10_1 = (raw: RawClient["server.fs"]) => (input: Endpoint10_1Input) => - raw["fs.find"]({ - query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] }, - }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup10 = (raw: RawClient["server.fs"]) => ({ list: Endpoint10_0(raw), find: Endpoint10_1(raw) }) - -type Endpoint11_0Request = Parameters[0] -type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] } -const Endpoint11_0 = (raw: RawClient["server.command"]) => (input?: Endpoint11_0Input) => - raw["command.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup11 = (raw: RawClient["server.command"]) => ({ list: Endpoint11_0(raw) }) - -type Endpoint12_0Request = Parameters[0] -type Endpoint12_0Input = { readonly location?: Endpoint12_0Request["query"]["location"] } -const Endpoint12_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint12_0Input) => - raw["skill.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup12 = (raw: RawClient["server.skill"]) => ({ list: Endpoint12_0(raw) }) - -const Endpoint13_0 = (raw: RawClient["server.event"]) => () => - Stream.unwrap( - raw["event.subscribe"]({}).pipe( - Effect.mapError(mapClientError), - Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), - ), - ) - -const adaptGroup13 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint13_0(raw) }) - -type Endpoint14_0Request = Parameters[0] -type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] } -const Endpoint14_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_0Input) => - raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint14_1Request = Parameters[0] -type Endpoint14_1Input = { - readonly location?: Endpoint14_1Request["query"]["location"] - readonly command?: Endpoint14_1Request["payload"]["command"] - readonly args?: Endpoint14_1Request["payload"]["args"] - readonly cwd?: Endpoint14_1Request["payload"]["cwd"] - readonly title?: Endpoint14_1Request["payload"]["title"] - readonly env?: Endpoint14_1Request["payload"]["env"] -} -const Endpoint14_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_1Input) => - raw["pty.create"]({ - query: { location: input?.["location"] }, - payload: { - command: input?.["command"], - args: input?.["args"], - cwd: input?.["cwd"], - title: input?.["title"], - env: input?.["env"], - }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint14_2Request = Parameters[0] -type Endpoint14_2Input = { - readonly ptyID: Endpoint14_2Request["params"]["ptyID"] - readonly location?: Endpoint14_2Request["query"]["location"] -} -const Endpoint14_2 = (raw: RawClient["server.pty"]) => (input: Endpoint14_2Input) => - raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), - ) - -type Endpoint14_3Request = Parameters[0] -type Endpoint14_3Input = { - readonly ptyID: Endpoint14_3Request["params"]["ptyID"] - readonly location?: Endpoint14_3Request["query"]["location"] - readonly title?: Endpoint14_3Request["payload"]["title"] - readonly size?: Endpoint14_3Request["payload"]["size"] -} -const Endpoint14_3 = (raw: RawClient["server.pty"]) => (input: Endpoint14_3Input) => - raw["pty.update"]({ - params: { ptyID: input["ptyID"] }, - query: { location: input["location"] }, - payload: { title: input["title"], size: input["size"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint14_4Request = Parameters[0] -type Endpoint14_4Input = { - readonly ptyID: Endpoint14_4Request["params"]["ptyID"] - readonly location?: Endpoint14_4Request["query"]["location"] -} -const Endpoint14_4 = (raw: RawClient["server.pty"]) => (input: Endpoint14_4Input) => - raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), - ) - -const adaptGroup14 = (raw: RawClient["server.pty"]) => ({ - list: Endpoint14_0(raw), - create: Endpoint14_1(raw), - get: Endpoint14_2(raw), - update: Endpoint14_3(raw), - remove: Endpoint14_4(raw), -}) - -type Endpoint15_0Request = Parameters[0] -type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] } -const Endpoint15_0 = (raw: RawClient["server.question"]) => (input?: Endpoint15_0Input) => - raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint15_1Request = Parameters[0] -type Endpoint15_1Input = { readonly sessionID: Endpoint15_1Request["params"]["sessionID"] } -const Endpoint15_1 = (raw: RawClient["server.question"]) => (input: Endpoint15_1Input) => - raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint15_2Request = Parameters[0] -type Endpoint15_2Input = { - readonly sessionID: Endpoint15_2Request["params"]["sessionID"] - readonly requestID: Endpoint15_2Request["params"]["requestID"] - readonly answers: Endpoint15_2Request["payload"]["answers"] -} -const Endpoint15_2 = (raw: RawClient["server.question"]) => (input: Endpoint15_2Input) => - raw["session.question.reply"]({ - params: { sessionID: input["sessionID"], requestID: input["requestID"] }, - payload: { answers: input["answers"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint15_3Request = Parameters[0] -type Endpoint15_3Input = { - readonly sessionID: Endpoint15_3Request["params"]["sessionID"] - readonly requestID: Endpoint15_3Request["params"]["requestID"] -} -const Endpoint15_3 = (raw: RawClient["server.question"]) => (input: Endpoint15_3Input) => - raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( - Effect.mapError(mapClientError), - ) - -const adaptGroup15 = (raw: RawClient["server.question"]) => ({ - listRequests: Endpoint15_0(raw), - list: Endpoint15_1(raw), - reply: Endpoint15_2(raw), - reject: Endpoint15_3(raw), -}) - -type Endpoint16_0Request = Parameters[0] -type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] } -const Endpoint16_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint16_0Input) => - raw["reference.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup16 = (raw: RawClient["server.reference"]) => ({ list: Endpoint16_0(raw) }) - -type Endpoint17_0Request = Parameters[0] -type Endpoint17_0Input = { - readonly projectID: Endpoint17_0Request["params"]["projectID"] - readonly location?: Endpoint17_0Request["query"]["location"] - readonly strategy: Endpoint17_0Request["payload"]["strategy"] - readonly directory: Endpoint17_0Request["payload"]["directory"] - readonly name?: Endpoint17_0Request["payload"]["name"] -} -const Endpoint17_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint17_0Input) => - raw["projectCopy.create"]({ - params: { projectID: input["projectID"] }, - query: { location: input["location"] }, - payload: { strategy: input["strategy"], directory: input["directory"], name: input["name"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint17_1Request = Parameters[0] -type Endpoint17_1Input = { - readonly projectID: Endpoint17_1Request["params"]["projectID"] - readonly location?: Endpoint17_1Request["query"]["location"] - readonly directory: Endpoint17_1Request["payload"]["directory"] - readonly force: Endpoint17_1Request["payload"]["force"] -} -const Endpoint17_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint17_1Input) => - raw["projectCopy.remove"]({ - params: { projectID: input["projectID"] }, - query: { location: input["location"] }, - payload: { directory: input["directory"], force: input["force"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint17_2Request = Parameters[0] -type Endpoint17_2Input = { - readonly projectID: Endpoint17_2Request["params"]["projectID"] - readonly location?: Endpoint17_2Request["query"]["location"] -} -const Endpoint17_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint17_2Input) => - raw["projectCopy.refresh"]({ - params: { projectID: input["projectID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup17 = (raw: RawClient["server.projectCopy"]) => ({ - create: Endpoint17_0(raw), - remove: Endpoint17_1(raw), - refresh: Endpoint17_2(raw), -}) - -const adaptClient = (raw: RawClient) => ({ - health: adaptGroup0(raw["server.health"]), - location: adaptGroup1(raw["server.location"]), - agents: adaptGroup2(raw["server.agent"]), - sessions: adaptGroup3(raw["server.session"]), - messages: adaptGroup4(raw["server.message"]), - models: adaptGroup5(raw["server.model"]), - providers: adaptGroup6(raw["server.provider"]), - integrations: adaptGroup7(raw["server.integration"]), - credentials: adaptGroup8(raw["server.credential"]), - permissions: adaptGroup9(raw["server.permission"]), - files: adaptGroup10(raw["server.fs"]), - commands: adaptGroup11(raw["server.command"]), - skills: adaptGroup12(raw["server.skill"]), - events: adaptGroup13(raw["server.event"]), - ptys: adaptGroup14(raw["server.pty"]), - questions: adaptGroup15(raw["server.question"]), - references: adaptGroup16(raw["server.reference"]), - projectCopies: adaptGroup17(raw["server.projectCopy"]), -}) - -export const make = (options?: { readonly baseUrl?: URL | string }) => - HttpApiClient.make(ClientApi, options).pipe(Effect.map(adaptClient)) diff --git a/packages/client/src/generated/client.ts b/packages/client/src/generated/client.ts deleted file mode 100644 index 27ec3d81ba..0000000000 --- a/packages/client/src/generated/client.ts +++ /dev/null @@ -1,1029 +0,0 @@ -import type { - HealthGetOutput, - LocationGetInput, - LocationGetOutput, - AgentsListInput, - AgentsListOutput, - SessionsListInput, - SessionsListOutput, - SessionsCreateInput, - SessionsCreateOutput, - SessionsActiveOutput, - SessionsGetInput, - SessionsGetOutput, - SessionsSwitchAgentInput, - SessionsSwitchAgentOutput, - SessionsSwitchModelInput, - SessionsSwitchModelOutput, - SessionsPromptInput, - SessionsPromptOutput, - SessionsCompactInput, - SessionsCompactOutput, - SessionsWaitInput, - SessionsWaitOutput, - SessionsStageInput, - SessionsStageOutput, - SessionsClearInput, - SessionsClearOutput, - SessionsCommitInput, - SessionsCommitOutput, - SessionsContextInput, - SessionsContextOutput, - SessionsHistoryInput, - SessionsHistoryOutput, - SessionsEventsInput, - SessionsEventsOutput, - SessionsInterruptInput, - SessionsInterruptOutput, - SessionsMessageInput, - SessionsMessageOutput, - MessagesListInput, - MessagesListOutput, - ModelsListInput, - ModelsListOutput, - ProvidersListInput, - ProvidersListOutput, - ProvidersGetInput, - ProvidersGetOutput, - IntegrationsListInput, - IntegrationsListOutput, - IntegrationsGetInput, - IntegrationsGetOutput, - IntegrationsConnectKeyInput, - IntegrationsConnectKeyOutput, - IntegrationsConnectOauthInput, - IntegrationsConnectOauthOutput, - IntegrationsAttemptStatusInput, - IntegrationsAttemptStatusOutput, - IntegrationsAttemptCompleteInput, - IntegrationsAttemptCompleteOutput, - IntegrationsAttemptCancelInput, - IntegrationsAttemptCancelOutput, - CredentialsUpdateInput, - CredentialsUpdateOutput, - CredentialsRemoveInput, - CredentialsRemoveOutput, - PermissionsListRequestsInput, - PermissionsListRequestsOutput, - PermissionsListSavedInput, - PermissionsListSavedOutput, - PermissionsRemoveSavedInput, - PermissionsRemoveSavedOutput, - PermissionsCreateInput, - PermissionsCreateOutput, - PermissionsListInput, - PermissionsListOutput, - PermissionsGetInput, - PermissionsGetOutput, - PermissionsReplyInput, - PermissionsReplyOutput, - FilesListInput, - FilesListOutput, - FilesFindInput, - FilesFindOutput, - CommandsListInput, - CommandsListOutput, - SkillsListInput, - SkillsListOutput, - EventsSubscribeOutput, - PtysListInput, - PtysListOutput, - PtysCreateInput, - PtysCreateOutput, - PtysGetInput, - PtysGetOutput, - PtysUpdateInput, - PtysUpdateOutput, - PtysRemoveInput, - PtysRemoveOutput, - QuestionsListRequestsInput, - QuestionsListRequestsOutput, - QuestionsListInput, - QuestionsListOutput, - QuestionsReplyInput, - QuestionsReplyOutput, - QuestionsRejectInput, - QuestionsRejectOutput, - ReferencesListInput, - ReferencesListOutput, - ProjectCopiesCreateInput, - ProjectCopiesCreateOutput, - ProjectCopiesRemoveInput, - ProjectCopiesRemoveOutput, - ProjectCopiesRefreshInput, - ProjectCopiesRefreshOutput, -} from "./types" -import { ClientError } from "./client-error" - -export interface ClientOptions { - readonly baseUrl: string - readonly fetch?: typeof globalThis.fetch - readonly headers?: HeadersInit -} - -export interface RequestOptions { - readonly signal?: AbortSignal - readonly headers?: HeadersInit -} - -interface RequestDescriptor { - readonly method: string - readonly path: string - readonly query?: Record - readonly headers?: Record - readonly body?: unknown - readonly successStatus: number - readonly declaredStatuses: ReadonlyArray - readonly empty: boolean -} - -export function make(options: ClientOptions) { - const fetch = options.fetch ?? globalThis.fetch - - const prepare = (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => { - const url = new URL(descriptor.path, options.baseUrl) - for (const [key, value] of Object.entries(descriptor.query ?? {})) appendQuery(url.searchParams, key, value) - const headers = new Headers(options.headers) - for (const [key, value] of Object.entries(descriptor.headers ?? {})) { - if (value !== undefined && value !== null) headers.set(key, String(value)) - } - for (const [key, value] of new Headers(requestOptions?.headers)) headers.set(key, value) - if (descriptor.body !== undefined && !headers.has("content-type")) headers.set("content-type", "application/json") - return { - url, - init: { - method: descriptor.method, - signal: requestOptions?.signal, - headers, - body: descriptor.body === undefined ? undefined : JSON.stringify(descriptor.body), - } satisfies RequestInit, - } - } - - const execute = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => { - try { - const prepared = prepare(descriptor, requestOptions) - return await fetch(prepared.url, prepared.init) - } catch (cause) { - throw new ClientError("Transport", { cause }) - } - } - - const responseError = async (response: Response, descriptor: RequestDescriptor): Promise => { - if (descriptor.declaredStatuses.includes(response.status)) throw await json(response) - try { - await response.body?.cancel() - } catch {} - throw new ClientError("UnexpectedStatus", { cause: { status: response.status } }) - } - - const request = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions): Promise => { - const response = await execute(descriptor, requestOptions) - if (response.status !== descriptor.successStatus) return responseError(response, descriptor) - if (descriptor.empty) { - try { - await response.body?.cancel() - } catch {} - return undefined as A - } - return (await json(response)) as A - } - - const sse = (descriptor: RequestDescriptor, requestOptions?: RequestOptions): AsyncIterable => ({ - async *[Symbol.asyncIterator]() { - const response = await execute(descriptor, requestOptions) - if (response.status !== descriptor.successStatus) await responseError(response, descriptor) - if (!isContentType(response, "text/event-stream")) { - try { - await response.body?.cancel() - } catch {} - throw new ClientError("UnsupportedContentType") - } - if (response.body === null) throw new ClientError("MalformedResponse") - const reader = response.body.getReader() - const decoder = new TextDecoder() - let buffer = "" - try { - while (true) { - let next - try { - next = await reader.read() - } catch (cause) { - throw new ClientError("Transport", { cause }) - } - buffer += decoder.decode(next.value, { stream: !next.done }) - if (buffer.length > 1_048_576) throw new ClientError("MalformedResponse") - const trailingCarriageReturn = !next.done && buffer.endsWith("\r") - if (trailingCarriageReturn) buffer = buffer.slice(0, -1) - buffer = buffer.replaceAll("\r\n", "\n").replaceAll("\r", "\n") - if (trailingCarriageReturn) buffer += "\r" - if (next.done && buffer !== "") buffer += "\n\n" - let boundary = buffer.indexOf("\n\n") - while (boundary >= 0) { - const block = buffer.slice(0, boundary) - buffer = buffer.slice(boundary + 2) - const data = block - .split("\n") - .flatMap((line) => (line.startsWith("data:") ? [line.slice(5).trimStart()] : [])) - .join("\n") - if (data !== "") { - try { - yield JSON.parse(data) as A - } catch (cause) { - throw new ClientError("MalformedResponse", { cause }) - } - } - boundary = buffer.indexOf("\n\n") - } - if (next.done) return - } - } finally { - try { - await reader.cancel() - } catch {} - reader.releaseLock() - } - }, - }) - - return { - health: { - get: (requestOptions?: RequestOptions) => - request( - { method: "GET", path: `/api/health`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, - requestOptions, - ), - }, - location: { - get: (input?: LocationGetInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/location`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - agents: { - list: (input?: AgentsListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/agent`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - sessions: { - list: (input?: SessionsListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/session`, - query: { - workspace: input?.["workspace"], - limit: input?.["limit"], - order: input?.["order"], - search: input?.["search"], - directory: input?.["directory"], - project: input?.["project"], - subpath: input?.["subpath"], - cursor: input?.["cursor"], - }, - successStatus: 200, - declaredStatuses: [400, 401], - empty: false, - }, - requestOptions, - ), - create: (input?: SessionsCreateInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionsCreateOutput }>( - { - method: "POST", - path: `/api/session`, - body: { - id: input?.["id"], - agent: input?.["agent"], - model: input?.["model"], - location: input?.["location"], - }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - active: (requestOptions?: RequestOptions) => - request<{ readonly data: SessionsActiveOutput }>( - { - method: "GET", - path: `/api/session/active`, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - get: (input: SessionsGetInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionsGetOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}`, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - switchAgent: (input: SessionsSwitchAgentInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/agent`, - body: { agent: input["agent"] }, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - switchModel: (input: SessionsSwitchModelInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/model`, - body: { model: input["model"] }, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - prompt: (input: SessionsPromptInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionsPromptOutput }>( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/prompt`, - body: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] }, - successStatus: 200, - declaredStatuses: [409, 404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - compact: (input: SessionsCompactInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/compact`, - successStatus: 204, - declaredStatuses: [404, 503, 400, 401], - empty: true, - }, - requestOptions, - ), - wait: (input: SessionsWaitInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/wait`, - successStatus: 204, - declaredStatuses: [404, 503, 400, 401], - empty: true, - }, - requestOptions, - ), - stage: (input: SessionsStageInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionsStageOutput }>( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`, - body: { messageID: input["messageID"], files: input["files"] }, - successStatus: 200, - declaredStatuses: [404, 500, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - clear: (input: SessionsClearInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`, - successStatus: 204, - declaredStatuses: [404, 500, 400, 401], - empty: true, - }, - requestOptions, - ), - commit: (input: SessionsCommitInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - context: (input: SessionsContextInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionsContextOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/context`, - successStatus: 200, - declaredStatuses: [404, 500, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - history: (input: SessionsHistoryInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/history`, - query: { limit: input["limit"], after: input["after"] }, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ), - events: (input: SessionsEventsInput, requestOptions?: RequestOptions): AsyncIterable => - sse( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/event`, - query: { after: input["after"] }, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ), - interrupt: (input: SessionsInterruptInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/interrupt`, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - message: (input: SessionsMessageInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionsMessageOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/message/${encodeURIComponent(input.messageID)}`, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - }, - messages: { - list: (input: MessagesListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/message`, - query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] }, - successStatus: 200, - declaredStatuses: [400, 404, 500, 401], - empty: false, - }, - requestOptions, - ), - }, - models: { - list: (input?: ModelsListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/model`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [503, 401, 400], - empty: false, - }, - requestOptions, - ), - }, - providers: { - list: (input?: ProvidersListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/provider`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [503, 401, 400], - empty: false, - }, - requestOptions, - ), - get: (input: ProvidersGetInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/provider/${encodeURIComponent(input.providerID)}`, - query: { location: input["location"] }, - successStatus: 200, - declaredStatuses: [404, 503, 401, 400], - empty: false, - }, - requestOptions, - ), - }, - integrations: { - list: (input?: IntegrationsListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/integration`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - get: (input: IntegrationsGetInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/integration/${encodeURIComponent(input.integrationID)}`, - query: { location: input["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - connectKey: (input: IntegrationsConnectKeyInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/key`, - query: { location: input["location"] }, - body: { key: input["key"], label: input["label"] }, - successStatus: 204, - declaredStatuses: [400, 401], - empty: true, - }, - requestOptions, - ), - connectOauth: (input: IntegrationsConnectOauthInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`, - query: { location: input["location"] }, - body: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] }, - successStatus: 200, - declaredStatuses: [400, 401], - empty: false, - }, - requestOptions, - ), - attemptStatus: (input: IntegrationsAttemptStatusInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`, - query: { location: input["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - attemptComplete: (input: IntegrationsAttemptCompleteInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`, - query: { location: input["location"] }, - body: { code: input["code"] }, - successStatus: 204, - declaredStatuses: [400, 401], - empty: true, - }, - requestOptions, - ), - attemptCancel: (input: IntegrationsAttemptCancelInput, requestOptions?: RequestOptions) => - request( - { - method: "DELETE", - path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`, - query: { location: input["location"] }, - successStatus: 204, - declaredStatuses: [401, 400], - empty: true, - }, - requestOptions, - ), - }, - credentials: { - update: (input: CredentialsUpdateInput, requestOptions?: RequestOptions) => - request( - { - method: "PATCH", - path: `/api/credential/${encodeURIComponent(input.credentialID)}`, - query: { location: input["location"] }, - body: { label: input["label"] }, - successStatus: 204, - declaredStatuses: [401, 400], - empty: true, - }, - requestOptions, - ), - remove: (input: CredentialsRemoveInput, requestOptions?: RequestOptions) => - request( - { - method: "DELETE", - path: `/api/credential/${encodeURIComponent(input.credentialID)}`, - query: { location: input["location"] }, - successStatus: 204, - declaredStatuses: [401, 400], - empty: true, - }, - requestOptions, - ), - }, - permissions: { - listRequests: (input?: PermissionsListRequestsInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/permission/request`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - listSaved: (input?: PermissionsListSavedInput, requestOptions?: RequestOptions) => - request<{ readonly data: PermissionsListSavedOutput }>( - { - method: "GET", - path: `/api/permission/saved`, - query: { projectID: input?.["projectID"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - removeSaved: (input: PermissionsRemoveSavedInput, requestOptions?: RequestOptions) => - request( - { - method: "DELETE", - path: `/api/permission/saved/${encodeURIComponent(input.id)}`, - successStatus: 204, - declaredStatuses: [401, 400], - empty: true, - }, - requestOptions, - ), - create: (input: PermissionsCreateInput, requestOptions?: RequestOptions) => - request<{ readonly data: PermissionsCreateOutput }>( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/permission`, - body: { - id: input["id"], - action: input["action"], - resources: input["resources"], - save: input["save"], - metadata: input["metadata"], - source: input["source"], - agent: input["agent"], - }, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - list: (input: PermissionsListInput, requestOptions?: RequestOptions) => - request<{ readonly data: PermissionsListOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/permission`, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - get: (input: PermissionsGetInput, requestOptions?: RequestOptions) => - request<{ readonly data: PermissionsGetOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}`, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - reply: (input: PermissionsReplyInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}/reply`, - body: { reply: input["reply"], message: input["message"] }, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - }, - files: { - list: (input?: FilesListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/fs/list`, - query: { location: input?.["location"], path: input?.["path"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - find: (input: FilesFindInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/fs/find`, - query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - commands: { - list: (input?: CommandsListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/command`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - skills: { - list: (input?: SkillsListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/skill`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - events: { - subscribe: (requestOptions?: RequestOptions): AsyncIterable => - sse( - { method: "GET", path: `/api/event`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, - requestOptions, - ), - }, - ptys: { - list: (input?: PtysListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/pty`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - create: (input?: PtysCreateInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/pty`, - query: { location: input?.["location"] }, - body: { - command: input?.["command"], - args: input?.["args"], - cwd: input?.["cwd"], - title: input?.["title"], - env: input?.["env"], - }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - get: (input: PtysGetInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/pty/${encodeURIComponent(input.ptyID)}`, - query: { location: input["location"] }, - successStatus: 200, - declaredStatuses: [404, 401, 400], - empty: false, - }, - requestOptions, - ), - update: (input: PtysUpdateInput, requestOptions?: RequestOptions) => - request( - { - method: "PUT", - path: `/api/pty/${encodeURIComponent(input.ptyID)}`, - query: { location: input["location"] }, - body: { title: input["title"], size: input["size"] }, - successStatus: 200, - declaredStatuses: [404, 401, 400], - empty: false, - }, - requestOptions, - ), - remove: (input: PtysRemoveInput, requestOptions?: RequestOptions) => - request( - { - method: "DELETE", - path: `/api/pty/${encodeURIComponent(input.ptyID)}`, - query: { location: input["location"] }, - successStatus: 204, - declaredStatuses: [404, 401, 400], - empty: true, - }, - requestOptions, - ), - }, - questions: { - listRequests: (input?: QuestionsListRequestsInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/question/request`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - list: (input: QuestionsListInput, requestOptions?: RequestOptions) => - request<{ readonly data: QuestionsListOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/question`, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - reply: (input: QuestionsReplyInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/question/${encodeURIComponent(input.requestID)}/reply`, - body: { answers: input["answers"] }, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - reject: (input: QuestionsRejectInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/question/${encodeURIComponent(input.requestID)}/reject`, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - }, - references: { - list: (input?: ReferencesListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/reference`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - projectCopies: { - create: (input: ProjectCopiesCreateInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy`, - query: { location: input["location"] }, - body: { strategy: input["strategy"], directory: input["directory"], name: input["name"] }, - successStatus: 200, - declaredStatuses: [400, 401], - empty: false, - }, - requestOptions, - ), - remove: (input: ProjectCopiesRemoveInput, requestOptions?: RequestOptions) => - request( - { - method: "DELETE", - path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy`, - query: { location: input["location"] }, - body: { directory: input["directory"], force: input["force"] }, - successStatus: 204, - declaredStatuses: [400, 401], - empty: true, - }, - requestOptions, - ), - refresh: (input: ProjectCopiesRefreshInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy/refresh`, - query: { location: input["location"] }, - successStatus: 204, - declaredStatuses: [400, 401], - empty: true, - }, - requestOptions, - ), - }, - } -} - -function appendQuery(params: URLSearchParams, key: string, value: unknown): void { - if (value === undefined || value === null) return - if (Array.isArray(value)) { - for (const item of value) appendQuery(params, key, item) - return - } - if (typeof value === "object") { - for (const [child, item] of Object.entries(value)) appendQuery(params, `${key}[${child}]`, item) - return - } - params.append(key, String(value)) -} - -async function json(response: Response): Promise { - if (!isContentType(response, "application/json") && !response.headers.get("content-type")?.includes("+json")) { - try { - await response.body?.cancel() - } catch {} - throw new ClientError("UnsupportedContentType") - } - let text: string - try { - text = await response.text() - } catch (cause) { - throw new ClientError("Transport", { cause }) - } - if (text === "") throw new ClientError("MalformedResponse") - try { - return JSON.parse(text) - } catch (cause) { - throw new ClientError("MalformedResponse", { cause }) - } -} - -function isContentType(response: Response, expected: string) { - return response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() === expected -} diff --git a/packages/client/src/generated/types.ts b/packages/client/src/generated/types.ts deleted file mode 100644 index 3b3188c874..0000000000 --- a/packages/client/src/generated/types.ts +++ /dev/null @@ -1,2807 +0,0 @@ -import type { OpenCodeEventEncoded } from "@opencode-ai/protocol/groups/event" - -export type JsonValue = - | null - | boolean - | number - | string - | ReadonlyArray - | { readonly [key: string]: JsonValue } - -export type UnauthorizedError = { readonly _tag: "UnauthorizedError"; readonly message: string } -export const isUnauthorizedError = (value: unknown): value is UnauthorizedError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnauthorizedError" - -export type InvalidRequestError = { - readonly _tag: "InvalidRequestError" - readonly message: string - readonly kind?: string | undefined - readonly field?: string | undefined -} -export const isInvalidRequestError = (value: unknown): value is InvalidRequestError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidRequestError" - -export type InvalidCursorError = { readonly _tag: "InvalidCursorError"; readonly message: string } -export const isInvalidCursorError = (value: unknown): value is InvalidCursorError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidCursorError" - -export type SessionNotFoundError = { - readonly _tag: "SessionNotFoundError" - readonly sessionID: string - readonly message: string -} -export const isSessionNotFoundError = (value: unknown): value is SessionNotFoundError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SessionNotFoundError" - -export type ConflictError = { - readonly _tag: "ConflictError" - readonly message: string - readonly resource?: string | undefined -} -export const isConflictError = (value: unknown): value is ConflictError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ConflictError" - -export type ServiceUnavailableError = { - readonly _tag: "ServiceUnavailableError" - readonly message: string - readonly service?: string | undefined -} -export const isServiceUnavailableError = (value: unknown): value is ServiceUnavailableError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ServiceUnavailableError" - -export type MessageNotFoundError = { - readonly _tag: "MessageNotFoundError" - readonly sessionID: string - readonly messageID: string - readonly message: string -} -export const isMessageNotFoundError = (value: unknown): value is MessageNotFoundError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "MessageNotFoundError" - -export type UnknownError = { - readonly _tag: "UnknownError" - readonly message: string - readonly ref?: string | undefined -} -export const isUnknownError = (value: unknown): value is UnknownError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnknownError" - -export type ProviderNotFoundError = { - readonly _tag: "ProviderNotFoundError" - readonly providerID: string - readonly message: string -} -export const isProviderNotFoundError = (value: unknown): value is ProviderNotFoundError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ProviderNotFoundError" - -export type PermissionNotFoundError = { - readonly _tag: "PermissionNotFoundError" - readonly requestID: string - readonly message: string -} -export const isPermissionNotFoundError = (value: unknown): value is PermissionNotFoundError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "PermissionNotFoundError" - -export type PtyNotFoundError = { readonly _tag: "PtyNotFoundError"; readonly ptyID: string; readonly message: string } -export const isPtyNotFoundError = (value: unknown): value is PtyNotFoundError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "PtyNotFoundError" - -export type QuestionNotFoundError = { - readonly _tag: "QuestionNotFoundError" - readonly requestID: string - readonly message: string -} -export const isQuestionNotFoundError = (value: unknown): value is QuestionNotFoundError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "QuestionNotFoundError" - -export type ProjectCopyError = { - readonly name: "ProjectCopyError" - readonly data: { readonly message: string; readonly forceRequired?: boolean | undefined } -} -export const isProjectCopyError = (value: unknown): value is ProjectCopyError => - typeof value === "object" && value !== null && "name" in value && value["name"] === "ProjectCopyError" - -export type HealthGetOutput = { readonly healthy: true } - -export type LocationGetInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type LocationGetOutput = { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } -} - -export type AgentsListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type AgentsListOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: ReadonlyArray<{ - readonly id: string - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } - readonly request: { - readonly headers: { readonly [x: string]: string } - readonly body: { readonly [x: string]: JsonValue } - } - readonly system?: string - readonly description?: string - readonly mode: "subagent" | "primary" | "all" - readonly hidden: boolean - readonly color?: string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info" - readonly steps?: number - readonly permissions: ReadonlyArray<{ - readonly action: string - readonly resource: string - readonly effect: "allow" | "deny" | "ask" - }> - }> -} - -export type SessionsListInput = { - readonly workspace?: { - readonly workspace?: string | undefined - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly search?: string | undefined - readonly directory?: string | undefined - readonly project?: string | undefined - readonly subpath?: string | undefined - readonly cursor?: string | undefined - }["workspace"] - readonly limit?: { - readonly workspace?: string | undefined - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly search?: string | undefined - readonly directory?: string | undefined - readonly project?: string | undefined - readonly subpath?: string | undefined - readonly cursor?: string | undefined - }["limit"] - readonly order?: { - readonly workspace?: string | undefined - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly search?: string | undefined - readonly directory?: string | undefined - readonly project?: string | undefined - readonly subpath?: string | undefined - readonly cursor?: string | undefined - }["order"] - readonly search?: { - readonly workspace?: string | undefined - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly search?: string | undefined - readonly directory?: string | undefined - readonly project?: string | undefined - readonly subpath?: string | undefined - readonly cursor?: string | undefined - }["search"] - readonly directory?: { - readonly workspace?: string | undefined - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly search?: string | undefined - readonly directory?: string | undefined - readonly project?: string | undefined - readonly subpath?: string | undefined - readonly cursor?: string | undefined - }["directory"] - readonly project?: { - readonly workspace?: string | undefined - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly search?: string | undefined - readonly directory?: string | undefined - readonly project?: string | undefined - readonly subpath?: string | undefined - readonly cursor?: string | undefined - }["project"] - readonly subpath?: { - readonly workspace?: string | undefined - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly search?: string | undefined - readonly directory?: string | undefined - readonly project?: string | undefined - readonly subpath?: string | undefined - readonly cursor?: string | undefined - }["subpath"] - readonly cursor?: { - readonly workspace?: string | undefined - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly search?: string | undefined - readonly directory?: string | undefined - readonly project?: string | undefined - readonly subpath?: string | undefined - readonly cursor?: string | undefined - }["cursor"] -} - -export type SessionsListOutput = { - readonly data: ReadonlyArray<{ - readonly id: string - readonly parentID?: string - readonly projectID: string - readonly agent?: string - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } - readonly cost: number - readonly tokens: { - readonly input: number - readonly output: number - readonly reasoning: number - readonly cache: { readonly read: number; readonly write: number } - } - readonly time: { readonly created: number; readonly updated: number; readonly archived?: number } - readonly title: string - readonly location: { readonly directory: string; readonly workspaceID?: string } - readonly subpath?: string - readonly revert?: { - readonly messageID: string - readonly partID?: string - readonly snapshot?: string - readonly diff?: string - readonly files?: ReadonlyArray<{ - readonly path: string - readonly status: "added" | "modified" | "deleted" - readonly additions: number - readonly deletions: number - readonly patch: string - }> - } - }> - readonly cursor: { readonly previous?: string | null; readonly next?: string | null } -} - -export type SessionsCreateInput = { - readonly id?: { - readonly id?: string | null - readonly agent?: string | null - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - readonly location?: { readonly directory: string; readonly workspaceID?: string } | null - }["id"] - readonly agent?: { - readonly id?: string | null - readonly agent?: string | null - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - readonly location?: { readonly directory: string; readonly workspaceID?: string } | null - }["agent"] - readonly model?: { - readonly id?: string | null - readonly agent?: string | null - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - readonly location?: { readonly directory: string; readonly workspaceID?: string } | null - }["model"] - readonly location?: { - readonly id?: string | null - readonly agent?: string | null - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - readonly location?: { readonly directory: string; readonly workspaceID?: string } | null - }["location"] -} - -export type SessionsCreateOutput = { - readonly data: { - readonly id: string - readonly parentID?: string - readonly projectID: string - readonly agent?: string - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } - readonly cost: number - readonly tokens: { - readonly input: number - readonly output: number - readonly reasoning: number - readonly cache: { readonly read: number; readonly write: number } - } - readonly time: { readonly created: number; readonly updated: number; readonly archived?: number } - readonly title: string - readonly location: { readonly directory: string; readonly workspaceID?: string } - readonly subpath?: string - readonly revert?: { - readonly messageID: string - readonly partID?: string - readonly snapshot?: string - readonly diff?: string - readonly files?: ReadonlyArray<{ - readonly path: string - readonly status: "added" | "modified" | "deleted" - readonly additions: number - readonly deletions: number - readonly patch: string - }> - } - } -}["data"] - -export type SessionsActiveOutput = { readonly data: { readonly [x: string]: { readonly type: "running" } } }["data"] - -export type SessionsGetInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionsGetOutput = { - readonly data: { - readonly id: string - readonly parentID?: string - readonly projectID: string - readonly agent?: string - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } - readonly cost: number - readonly tokens: { - readonly input: number - readonly output: number - readonly reasoning: number - readonly cache: { readonly read: number; readonly write: number } - } - readonly time: { readonly created: number; readonly updated: number; readonly archived?: number } - readonly title: string - readonly location: { readonly directory: string; readonly workspaceID?: string } - readonly subpath?: string - readonly revert?: { - readonly messageID: string - readonly partID?: string - readonly snapshot?: string - readonly diff?: string - readonly files?: ReadonlyArray<{ - readonly path: string - readonly status: "added" | "modified" | "deleted" - readonly additions: number - readonly deletions: number - readonly patch: string - }> - } - } -}["data"] - -export type SessionsSwitchAgentInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly agent: { readonly agent: string }["agent"] -} - -export type SessionsSwitchAgentOutput = void - -export type SessionsSwitchModelInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly model: { - readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } - }["model"] -} - -export type SessionsSwitchModelOutput = void - -export type SessionsPromptInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly id?: { - readonly id?: string | null - readonly prompt: { - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - } - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["id"] - readonly prompt: { - readonly id?: string | null - readonly prompt: { - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - } - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["prompt"] - readonly delivery?: { - readonly id?: string | null - readonly prompt: { - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - } - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["delivery"] - readonly resume?: { - readonly id?: string | null - readonly prompt: { - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - } - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["resume"] -} - -export type SessionsPromptOutput = { - readonly data: { - readonly admittedSeq: number - readonly id: string - readonly sessionID: string - readonly prompt: { - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly mime: string - readonly name?: string - readonly description?: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - } - readonly delivery: "steer" | "queue" - readonly timeCreated: number - readonly promotedSeq?: number - } -}["data"] - -export type SessionsCompactInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionsCompactOutput = void - -export type SessionsWaitInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionsWaitOutput = void - -export type SessionsStageInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly messageID: { readonly messageID: string; readonly files?: boolean | undefined }["messageID"] - readonly files?: { readonly messageID: string; readonly files?: boolean | undefined }["files"] -} - -export type SessionsStageOutput = { - readonly data: { - readonly messageID: string - readonly partID?: string - readonly snapshot?: string - readonly diff?: string - readonly files?: ReadonlyArray<{ - readonly path: string - readonly status: "added" | "modified" | "deleted" - readonly additions: number - readonly deletions: number - readonly patch: string - }> - } -}["data"] - -export type SessionsClearInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionsClearOutput = void - -export type SessionsCommitInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionsCommitOutput = void - -export type SessionsContextInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionsContextOutput = { - readonly data: ReadonlyArray< - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number } - readonly type: "agent-switched" - readonly agent: string - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number } - readonly type: "model-switched" - readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number } - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly mime: string - readonly name?: string - readonly description?: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly type: "user" - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number } - readonly sessionID: string - readonly text: string - readonly type: "synthetic" - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number } - readonly type: "system" - readonly text: string - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number; readonly completed?: number } - readonly type: "shell" - readonly callID: string - readonly command: string - readonly output: string - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number; readonly completed?: number } - readonly type: "assistant" - readonly agent: string - readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } - readonly content: ReadonlyArray< - | { readonly type: "text"; readonly id: string; readonly text: string } - | { - readonly type: "reasoning" - readonly id: string - readonly text: string - readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } - readonly time?: { readonly created: number; readonly completed?: number } - } - | { - readonly type: "tool" - readonly id: string - readonly name: string - readonly provider?: { - readonly executed: boolean - readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } - readonly resultMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } - } - readonly state: - | { readonly status: "pending"; readonly input: string } - | { - readonly status: "running" - readonly input: { readonly [x: string]: JsonValue } - readonly structured: { readonly [x: string]: JsonValue } - readonly content: ReadonlyArray< - | { readonly type: "text"; readonly text: string } - | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } - > - } - | { - readonly status: "completed" - readonly input: { readonly [x: string]: JsonValue } - readonly attachments?: ReadonlyArray<{ - readonly uri: string - readonly mime: string - readonly name?: string - readonly description?: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly content: ReadonlyArray< - | { readonly type: "text"; readonly text: string } - | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } - > - readonly outputPaths?: ReadonlyArray - readonly structured: { readonly [x: string]: JsonValue } - readonly result?: JsonValue - } - | { - readonly status: "error" - readonly input: { readonly [x: string]: JsonValue } - readonly content: ReadonlyArray< - | { readonly type: "text"; readonly text: string } - | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } - > - readonly structured: { readonly [x: string]: JsonValue } - readonly error: { readonly type: "unknown"; readonly message: string } - readonly result?: JsonValue - } - readonly time: { - readonly created: number - readonly ran?: number - readonly completed?: number - readonly pruned?: number - } - } - > - readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray } - readonly finish?: string - readonly cost?: number - readonly tokens?: { - readonly input: number - readonly output: number - readonly reasoning: number - readonly cache: { readonly read: number; readonly write: number } - } - readonly error?: { readonly type: "unknown"; readonly message: string } - } - | { - readonly type: "compaction" - readonly reason: "auto" | "manual" - readonly summary: string - readonly recent: string - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number } - } - > -}["data"] - -export type SessionsHistoryInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly limit?: { readonly limit?: number | undefined; readonly after?: number | undefined }["limit"] - readonly after?: { readonly limit?: number | undefined; readonly after?: number | undefined }["after"] -} - -export type SessionsHistoryOutput = { - readonly data: ReadonlyArray< - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.agent.switched" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly messageID: string - readonly agent: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.model.switched" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly messageID: string - readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.moved" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly location: { readonly directory: string; readonly workspaceID?: string } - readonly subdirectory?: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.prompted" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly messageID: string - readonly prompt: { - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly mime: string - readonly name?: string - readonly description?: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - } - readonly delivery: "steer" | "queue" - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.prompt.admitted" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly messageID: string - readonly prompt: { - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly mime: string - readonly name?: string - readonly description?: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - } - readonly delivery: "steer" | "queue" - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.context.updated" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly messageID: string - readonly text: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.synthetic" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly messageID: string - readonly text: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.shell.started" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly messageID: string - readonly callID: string - readonly command: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.shell.ended" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly callID: string - readonly output: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.step.started" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly agent: string - readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } - readonly snapshot?: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.step.ended" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly finish: string - readonly cost: number - readonly tokens: { - readonly input: number - readonly output: number - readonly reasoning: number - readonly cache: { readonly read: number; readonly write: number } - } - readonly snapshot?: string - readonly files?: ReadonlyArray - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.step.failed" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly error: { readonly type: "unknown"; readonly message: string } - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.text.started" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly textID: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.text.ended" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly textID: string - readonly text: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.tool.input.started" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly callID: string - readonly name: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.tool.input.ended" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly callID: string - readonly text: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.tool.called" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly callID: string - readonly tool: string - readonly input: { readonly [x: string]: JsonValue } - readonly provider: { - readonly executed: boolean - readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } - } - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.tool.progress" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly callID: string - readonly structured: { readonly [x: string]: JsonValue } - readonly content: ReadonlyArray< - | { readonly type: "text"; readonly text: string } - | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } - > - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.tool.success" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly callID: string - readonly structured: { readonly [x: string]: JsonValue } - readonly content: ReadonlyArray< - | { readonly type: "text"; readonly text: string } - | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } - > - readonly outputPaths?: ReadonlyArray - readonly result?: JsonValue - readonly provider: { - readonly executed: boolean - readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } - } - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.tool.failed" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly callID: string - readonly error: { readonly type: "unknown"; readonly message: string } - readonly result?: JsonValue - readonly provider: { - readonly executed: boolean - readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } - } - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.reasoning.started" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly reasoningID: string - readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.reasoning.ended" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly reasoningID: string - readonly text: string - readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.retried" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly attempt: number - readonly error: { - readonly message: string - readonly statusCode?: number - readonly isRetryable: boolean - readonly responseHeaders?: { readonly [x: string]: string } - readonly responseBody?: string - readonly metadata?: { readonly [x: string]: string } - } - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.compaction.started" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly messageID: string - readonly reason: "auto" | "manual" - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.compaction.ended" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly messageID: string - readonly reason: "auto" | "manual" - readonly text: string - readonly recent: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.revert.staged" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly revert: { - readonly messageID: string - readonly partID?: string - readonly snapshot?: string - readonly diff?: string - readonly files?: ReadonlyArray<{ - readonly path: string - readonly status: "added" | "modified" | "deleted" - readonly additions: number - readonly deletions: number - readonly patch: string - }> - } - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.revert.cleared" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { readonly timestamp: number; readonly sessionID: string } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly type: "session.next.revert.committed" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { readonly timestamp: number; readonly sessionID: string; readonly messageID: string } - } - > - readonly hasMore: boolean -} - -export type SessionsEventsInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly after?: { readonly after?: number | undefined }["after"] -} - -export type SessionsEventsOutput = - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.agent.switched" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly messageID: string - readonly agent: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.model.switched" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly messageID: string - readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.moved" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly location: { readonly directory: string; readonly workspaceID?: string } - readonly subdirectory?: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.prompted" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly messageID: string - readonly prompt: { - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly mime: string - readonly name?: string - readonly description?: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - } - readonly delivery: "steer" | "queue" - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.prompt.admitted" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly messageID: string - readonly prompt: { - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly mime: string - readonly name?: string - readonly description?: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - } - readonly delivery: "steer" | "queue" - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.context.updated" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly messageID: string - readonly text: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.synthetic" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly messageID: string - readonly text: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.shell.started" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly messageID: string - readonly callID: string - readonly command: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.shell.ended" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly callID: string - readonly output: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.step.started" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly agent: string - readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } - readonly snapshot?: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.step.ended" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly finish: string - readonly cost: number - readonly tokens: { - readonly input: number - readonly output: number - readonly reasoning: number - readonly cache: { readonly read: number; readonly write: number } - } - readonly snapshot?: string - readonly files?: ReadonlyArray - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.step.failed" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly error: { readonly type: "unknown"; readonly message: string } - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.text.started" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly textID: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.text.ended" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly textID: string - readonly text: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.tool.input.started" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly callID: string - readonly name: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.tool.input.ended" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly callID: string - readonly text: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.tool.called" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly callID: string - readonly tool: string - readonly input: { readonly [x: string]: unknown } - readonly provider: { - readonly executed: boolean - readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } } - } - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.tool.progress" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly callID: string - readonly structured: { readonly [x: string]: unknown } - readonly content: ReadonlyArray< - | { readonly type: "text"; readonly text: string } - | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } - > - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.tool.success" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly callID: string - readonly structured: { readonly [x: string]: unknown } - readonly content: ReadonlyArray< - | { readonly type: "text"; readonly text: string } - | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } - > - readonly outputPaths?: ReadonlyArray - readonly result?: unknown - readonly provider: { - readonly executed: boolean - readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } } - } - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.tool.failed" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly callID: string - readonly error: { readonly type: "unknown"; readonly message: string } - readonly result?: unknown - readonly provider: { - readonly executed: boolean - readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } } - } - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.reasoning.started" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly reasoningID: string - readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: unknown } } - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.reasoning.ended" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly assistantMessageID: string - readonly reasoningID: string - readonly text: string - readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: unknown } } - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.retried" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly attempt: number - readonly error: { - readonly message: string - readonly statusCode?: number - readonly isRetryable: boolean - readonly responseHeaders?: { readonly [x: string]: string } - readonly responseBody?: string - readonly metadata?: { readonly [x: string]: string } - } - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.compaction.started" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly messageID: string - readonly reason: "auto" | "manual" - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.compaction.ended" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly messageID: string - readonly reason: "auto" | "manual" - readonly text: string - readonly recent: string - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.revert.staged" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { - readonly timestamp: number - readonly sessionID: string - readonly revert: { - readonly messageID: string - readonly partID?: string - readonly snapshot?: string - readonly diff?: string - readonly files?: ReadonlyArray<{ - readonly path: string - readonly status: "added" | "modified" | "deleted" - readonly additions: number - readonly deletions: number - readonly patch: string - }> - } - } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.revert.cleared" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { readonly timestamp: number; readonly sessionID: string } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: unknown } - readonly type: "session.next.revert.committed" - readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } - readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { readonly timestamp: number; readonly sessionID: string; readonly messageID: string } - } - -export type SessionsInterruptInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionsInterruptOutput = void - -export type SessionsMessageInput = { - readonly sessionID: { readonly sessionID: string; readonly messageID: string }["sessionID"] - readonly messageID: { readonly sessionID: string; readonly messageID: string }["messageID"] -} - -export type SessionsMessageOutput = { - readonly data: - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number } - readonly type: "agent-switched" - readonly agent: string - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number } - readonly type: "model-switched" - readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number } - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly mime: string - readonly name?: string - readonly description?: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly type: "user" - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number } - readonly sessionID: string - readonly text: string - readonly type: "synthetic" - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number } - readonly type: "system" - readonly text: string - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number; readonly completed?: number } - readonly type: "shell" - readonly callID: string - readonly command: string - readonly output: string - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number; readonly completed?: number } - readonly type: "assistant" - readonly agent: string - readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } - readonly content: ReadonlyArray< - | { readonly type: "text"; readonly id: string; readonly text: string } - | { - readonly type: "reasoning" - readonly id: string - readonly text: string - readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } - readonly time?: { readonly created: number; readonly completed?: number } - } - | { - readonly type: "tool" - readonly id: string - readonly name: string - readonly provider?: { - readonly executed: boolean - readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } - readonly resultMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } - } - readonly state: - | { readonly status: "pending"; readonly input: string } - | { - readonly status: "running" - readonly input: { readonly [x: string]: JsonValue } - readonly structured: { readonly [x: string]: JsonValue } - readonly content: ReadonlyArray< - | { readonly type: "text"; readonly text: string } - | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } - > - } - | { - readonly status: "completed" - readonly input: { readonly [x: string]: JsonValue } - readonly attachments?: ReadonlyArray<{ - readonly uri: string - readonly mime: string - readonly name?: string - readonly description?: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly content: ReadonlyArray< - | { readonly type: "text"; readonly text: string } - | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } - > - readonly outputPaths?: ReadonlyArray - readonly structured: { readonly [x: string]: JsonValue } - readonly result?: JsonValue - } - | { - readonly status: "error" - readonly input: { readonly [x: string]: JsonValue } - readonly content: ReadonlyArray< - | { readonly type: "text"; readonly text: string } - | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } - > - readonly structured: { readonly [x: string]: JsonValue } - readonly error: { readonly type: "unknown"; readonly message: string } - readonly result?: JsonValue - } - readonly time: { - readonly created: number - readonly ran?: number - readonly completed?: number - readonly pruned?: number - } - } - > - readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray } - readonly finish?: string - readonly cost?: number - readonly tokens?: { - readonly input: number - readonly output: number - readonly reasoning: number - readonly cache: { readonly read: number; readonly write: number } - } - readonly error?: { readonly type: "unknown"; readonly message: string } - } - | { - readonly type: "compaction" - readonly reason: "auto" | "manual" - readonly summary: string - readonly recent: string - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number } - } -}["data"] - -export type MessagesListInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly limit?: { - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly cursor?: string | undefined - }["limit"] - readonly order?: { - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly cursor?: string | undefined - }["order"] - readonly cursor?: { - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly cursor?: string | undefined - }["cursor"] -} - -export type MessagesListOutput = { - readonly data: ReadonlyArray< - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number } - readonly type: "agent-switched" - readonly agent: string - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number } - readonly type: "model-switched" - readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number } - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly mime: string - readonly name?: string - readonly description?: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly type: "user" - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number } - readonly sessionID: string - readonly text: string - readonly type: "synthetic" - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number } - readonly type: "system" - readonly text: string - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number; readonly completed?: number } - readonly type: "shell" - readonly callID: string - readonly command: string - readonly output: string - } - | { - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number; readonly completed?: number } - readonly type: "assistant" - readonly agent: string - readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } - readonly content: ReadonlyArray< - | { readonly type: "text"; readonly id: string; readonly text: string } - | { - readonly type: "reasoning" - readonly id: string - readonly text: string - readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } - readonly time?: { readonly created: number; readonly completed?: number } - } - | { - readonly type: "tool" - readonly id: string - readonly name: string - readonly provider?: { - readonly executed: boolean - readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } - readonly resultMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } - } - readonly state: - | { readonly status: "pending"; readonly input: string } - | { - readonly status: "running" - readonly input: { readonly [x: string]: JsonValue } - readonly structured: { readonly [x: string]: JsonValue } - readonly content: ReadonlyArray< - | { readonly type: "text"; readonly text: string } - | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } - > - } - | { - readonly status: "completed" - readonly input: { readonly [x: string]: JsonValue } - readonly attachments?: ReadonlyArray<{ - readonly uri: string - readonly mime: string - readonly name?: string - readonly description?: string - readonly source?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly content: ReadonlyArray< - | { readonly type: "text"; readonly text: string } - | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } - > - readonly outputPaths?: ReadonlyArray - readonly structured: { readonly [x: string]: JsonValue } - readonly result?: JsonValue - } - | { - readonly status: "error" - readonly input: { readonly [x: string]: JsonValue } - readonly content: ReadonlyArray< - | { readonly type: "text"; readonly text: string } - | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } - > - readonly structured: { readonly [x: string]: JsonValue } - readonly error: { readonly type: "unknown"; readonly message: string } - readonly result?: JsonValue - } - readonly time: { - readonly created: number - readonly ran?: number - readonly completed?: number - readonly pruned?: number - } - } - > - readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray } - readonly finish?: string - readonly cost?: number - readonly tokens?: { - readonly input: number - readonly output: number - readonly reasoning: number - readonly cache: { readonly read: number; readonly write: number } - } - readonly error?: { readonly type: "unknown"; readonly message: string } - } - | { - readonly type: "compaction" - readonly reason: "auto" | "manual" - readonly summary: string - readonly recent: string - readonly id: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly time: { readonly created: number } - } - > - readonly cursor: { readonly previous?: string | null; readonly next?: string | null } -} - -export type ModelsListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type ModelsListOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: ReadonlyArray<{ - readonly id: string - readonly providerID: string - readonly family?: string - readonly name: string - readonly api: - | { - readonly id: string - readonly type: "aisdk" - readonly package: string - readonly url?: string - readonly settings?: { readonly [x: string]: JsonValue } - } - | { - readonly id: string - readonly type: "native" - readonly url?: string - readonly settings: { readonly [x: string]: JsonValue } - } - readonly capabilities: { - readonly tools: boolean - readonly input: ReadonlyArray - readonly output: ReadonlyArray - } - readonly request: { - readonly headers: { readonly [x: string]: string } - readonly body: { readonly [x: string]: JsonValue } - readonly variant?: string - } - readonly variants: ReadonlyArray<{ - readonly id: string - readonly headers: { readonly [x: string]: string } - readonly body: { readonly [x: string]: JsonValue } - }> - readonly time: { readonly released: number } - readonly cost: ReadonlyArray<{ - readonly tier?: { readonly type: "context"; readonly size: number } - readonly input: number - readonly output: number - readonly cache: { readonly read: number; readonly write: number } - }> - readonly status: "alpha" | "beta" | "deprecated" | "active" - readonly enabled: boolean - readonly limit: { readonly context: number; readonly input?: number; readonly output: number } - }> -} - -export type ProvidersListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type ProvidersListOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: ReadonlyArray<{ - readonly id: string - readonly integrationID?: string - readonly name: string - readonly disabled?: boolean - readonly api: - | { - readonly type: "aisdk" - readonly package: string - readonly url?: string - readonly settings?: { readonly [x: string]: JsonValue } - } - | { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } } - readonly request: { - readonly headers: { readonly [x: string]: string } - readonly body: { readonly [x: string]: JsonValue } - } - }> -} - -export type ProvidersGetInput = { - readonly providerID: { readonly providerID: string }["providerID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type ProvidersGetOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: { - readonly id: string - readonly integrationID?: string - readonly name: string - readonly disabled?: boolean - readonly api: - | { - readonly type: "aisdk" - readonly package: string - readonly url?: string - readonly settings?: { readonly [x: string]: JsonValue } - } - | { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } } - readonly request: { - readonly headers: { readonly [x: string]: string } - readonly body: { readonly [x: string]: JsonValue } - } - } -} - -export type IntegrationsListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type IntegrationsListOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: ReadonlyArray<{ - readonly id: string - readonly name: string - readonly methods: ReadonlyArray< - | { - readonly id: string - readonly type: "oauth" - readonly label: string - readonly prompts?: ReadonlyArray< - | { - readonly type: "text" - readonly key: string - readonly message: string - readonly placeholder?: string - readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string } - } - | { - readonly type: "select" - readonly key: string - readonly message: string - readonly options: ReadonlyArray<{ - readonly label: string - readonly value: string - readonly hint?: string - }> - readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string } - } - > - } - | { readonly type: "key"; readonly label?: string } - | { readonly type: "env"; readonly names: ReadonlyArray } - > - readonly connections: ReadonlyArray< - | { readonly type: "credential"; readonly id: string; readonly label: string } - | { readonly type: "env"; readonly name: string } - > - }> -} - -export type IntegrationsGetInput = { - readonly integrationID: { readonly integrationID: string }["integrationID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type IntegrationsGetOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: { - readonly id: string - readonly name: string - readonly methods: ReadonlyArray< - | { - readonly id: string - readonly type: "oauth" - readonly label: string - readonly prompts?: ReadonlyArray< - | { - readonly type: "text" - readonly key: string - readonly message: string - readonly placeholder?: string - readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string } - } - | { - readonly type: "select" - readonly key: string - readonly message: string - readonly options: ReadonlyArray<{ - readonly label: string - readonly value: string - readonly hint?: string - }> - readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string } - } - > - } - | { readonly type: "key"; readonly label?: string } - | { readonly type: "env"; readonly names: ReadonlyArray } - > - readonly connections: ReadonlyArray< - | { readonly type: "credential"; readonly id: string; readonly label: string } - | { readonly type: "env"; readonly name: string } - > - } | null -} - -export type IntegrationsConnectKeyInput = { - readonly integrationID: { readonly integrationID: string }["integrationID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly key: { readonly key: string; readonly label?: string | undefined }["key"] - readonly label?: { readonly key: string; readonly label?: string | undefined }["label"] -} - -export type IntegrationsConnectKeyOutput = void - -export type IntegrationsConnectOauthInput = { - readonly integrationID: { readonly integrationID: string }["integrationID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly methodID: { - readonly methodID: string - readonly inputs: { readonly [x: string]: string } - readonly label?: string | undefined - }["methodID"] - readonly inputs: { - readonly methodID: string - readonly inputs: { readonly [x: string]: string } - readonly label?: string | undefined - }["inputs"] - readonly label?: { - readonly methodID: string - readonly inputs: { readonly [x: string]: string } - readonly label?: string | undefined - }["label"] -} - -export type IntegrationsConnectOauthOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: { - readonly attemptID: string - readonly url: string - readonly instructions: string - readonly mode: "auto" | "code" - readonly time: { - readonly created: number | "Infinity" | "-Infinity" | "NaN" - readonly expires: number | "Infinity" | "-Infinity" | "NaN" - } - } -} - -export type IntegrationsAttemptStatusInput = { - readonly attemptID: { readonly attemptID: string }["attemptID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type IntegrationsAttemptStatusOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: - | { - readonly status: "pending" - readonly time: { - readonly created: number | "Infinity" | "-Infinity" | "NaN" - readonly expires: number | "Infinity" | "-Infinity" | "NaN" - } - } - | { - readonly status: "complete" - readonly time: { - readonly created: number | "Infinity" | "-Infinity" | "NaN" - readonly expires: number | "Infinity" | "-Infinity" | "NaN" - } - } - | { - readonly status: "failed" - readonly message: string - readonly time: { - readonly created: number | "Infinity" | "-Infinity" | "NaN" - readonly expires: number | "Infinity" | "-Infinity" | "NaN" - } - } - | { - readonly status: "expired" - readonly time: { - readonly created: number | "Infinity" | "-Infinity" | "NaN" - readonly expires: number | "Infinity" | "-Infinity" | "NaN" - } - } -} - -export type IntegrationsAttemptCompleteInput = { - readonly attemptID: { readonly attemptID: string }["attemptID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly code?: { readonly code?: string | undefined }["code"] -} - -export type IntegrationsAttemptCompleteOutput = void - -export type IntegrationsAttemptCancelInput = { - readonly attemptID: { readonly attemptID: string }["attemptID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type IntegrationsAttemptCancelOutput = void - -export type CredentialsUpdateInput = { - readonly credentialID: { readonly credentialID: string }["credentialID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly label: { readonly label: string }["label"] -} - -export type CredentialsUpdateOutput = void - -export type CredentialsRemoveInput = { - readonly credentialID: { readonly credentialID: string }["credentialID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type CredentialsRemoveOutput = void - -export type PermissionsListRequestsInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type PermissionsListRequestsOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: ReadonlyArray<{ - readonly id: string - readonly sessionID: string - readonly action: string - readonly resources: ReadonlyArray - readonly save?: ReadonlyArray - readonly metadata?: { readonly [x: string]: JsonValue } - readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } - }> -} - -export type PermissionsListSavedInput = { - readonly projectID?: { readonly projectID?: string | undefined }["projectID"] -} - -export type PermissionsListSavedOutput = { - readonly data: ReadonlyArray<{ - readonly id: string - readonly projectID: string - readonly action: string - readonly resource: string - }> -}["data"] - -export type PermissionsRemoveSavedInput = { readonly id: { readonly id: string }["id"] } - -export type PermissionsRemoveSavedOutput = void - -export type PermissionsCreateInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly id?: { - readonly id?: string | null - readonly action: string - readonly resources: ReadonlyArray - readonly save?: ReadonlyArray - readonly metadata?: { readonly [x: string]: JsonValue } - readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } - readonly agent?: string | null - }["id"] - readonly action: { - readonly id?: string | null - readonly action: string - readonly resources: ReadonlyArray - readonly save?: ReadonlyArray - readonly metadata?: { readonly [x: string]: JsonValue } - readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } - readonly agent?: string | null - }["action"] - readonly resources: { - readonly id?: string | null - readonly action: string - readonly resources: ReadonlyArray - readonly save?: ReadonlyArray - readonly metadata?: { readonly [x: string]: JsonValue } - readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } - readonly agent?: string | null - }["resources"] - readonly save?: { - readonly id?: string | null - readonly action: string - readonly resources: ReadonlyArray - readonly save?: ReadonlyArray - readonly metadata?: { readonly [x: string]: JsonValue } - readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } - readonly agent?: string | null - }["save"] - readonly metadata?: { - readonly id?: string | null - readonly action: string - readonly resources: ReadonlyArray - readonly save?: ReadonlyArray - readonly metadata?: { readonly [x: string]: JsonValue } - readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } - readonly agent?: string | null - }["metadata"] - readonly source?: { - readonly id?: string | null - readonly action: string - readonly resources: ReadonlyArray - readonly save?: ReadonlyArray - readonly metadata?: { readonly [x: string]: JsonValue } - readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } - readonly agent?: string | null - }["source"] - readonly agent?: { - readonly id?: string | null - readonly action: string - readonly resources: ReadonlyArray - readonly save?: ReadonlyArray - readonly metadata?: { readonly [x: string]: JsonValue } - readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } - readonly agent?: string | null - }["agent"] -} - -export type PermissionsCreateOutput = { - readonly data: { readonly id: string; readonly effect: "allow" | "deny" | "ask" } -}["data"] - -export type PermissionsListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type PermissionsListOutput = { - readonly data: ReadonlyArray<{ - readonly id: string - readonly sessionID: string - readonly action: string - readonly resources: ReadonlyArray - readonly save?: ReadonlyArray - readonly metadata?: { readonly [x: string]: JsonValue } - readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } - }> -}["data"] - -export type PermissionsGetInput = { - readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] - readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] -} - -export type PermissionsGetOutput = { - readonly data: { - readonly id: string - readonly sessionID: string - readonly action: string - readonly resources: ReadonlyArray - readonly save?: ReadonlyArray - readonly metadata?: { readonly [x: string]: JsonValue } - readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } - } -}["data"] - -export type PermissionsReplyInput = { - readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] - readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] - readonly reply: { readonly reply: "once" | "always" | "reject"; readonly message?: string | undefined }["reply"] - readonly message?: { readonly reply: "once" | "always" | "reject"; readonly message?: string | undefined }["message"] -} - -export type PermissionsReplyOutput = void - -export type FilesListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - readonly path?: string | undefined - }["location"] - readonly path?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - readonly path?: string | undefined - }["path"] -} - -export type FilesListOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: ReadonlyArray<{ readonly path: string; readonly type: "file" | "directory" }> -} - -export type FilesFindInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - readonly query: string - readonly type?: "file" | "directory" | undefined - readonly limit?: number | undefined - }["location"] - readonly query: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - readonly query: string - readonly type?: "file" | "directory" | undefined - readonly limit?: number | undefined - }["query"] - readonly type?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - readonly query: string - readonly type?: "file" | "directory" | undefined - readonly limit?: number | undefined - }["type"] - readonly limit?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - readonly query: string - readonly type?: "file" | "directory" | undefined - readonly limit?: number | undefined - }["limit"] -} - -export type FilesFindOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: ReadonlyArray<{ readonly path: string; readonly type: "file" | "directory" }> -} - -export type CommandsListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type CommandsListOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: ReadonlyArray<{ - readonly name: string - readonly template: string - readonly description?: string - readonly agent?: string - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } - readonly subtask?: boolean - }> -} - -export type SkillsListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type SkillsListOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: ReadonlyArray<{ - readonly name: string - readonly description?: string - readonly slash?: boolean - readonly location: string - readonly content: string - }> -} - -export type EventsSubscribeOutput = OpenCodeEventEncoded - -export type PtysListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type PtysListOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: ReadonlyArray<{ - readonly id: string - readonly title: string - readonly command: string - readonly args: ReadonlyArray - readonly cwd: string - readonly status: "running" | "exited" - readonly pid: number - readonly exitCode?: number - }> -} - -export type PtysCreateInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly command?: { - readonly command?: string - readonly args?: ReadonlyArray - readonly cwd?: string - readonly title?: string - readonly env?: { readonly [x: string]: string } - }["command"] - readonly args?: { - readonly command?: string - readonly args?: ReadonlyArray - readonly cwd?: string - readonly title?: string - readonly env?: { readonly [x: string]: string } - }["args"] - readonly cwd?: { - readonly command?: string - readonly args?: ReadonlyArray - readonly cwd?: string - readonly title?: string - readonly env?: { readonly [x: string]: string } - }["cwd"] - readonly title?: { - readonly command?: string - readonly args?: ReadonlyArray - readonly cwd?: string - readonly title?: string - readonly env?: { readonly [x: string]: string } - }["title"] - readonly env?: { - readonly command?: string - readonly args?: ReadonlyArray - readonly cwd?: string - readonly title?: string - readonly env?: { readonly [x: string]: string } - }["env"] -} - -export type PtysCreateOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: { - readonly id: string - readonly title: string - readonly command: string - readonly args: ReadonlyArray - readonly cwd: string - readonly status: "running" | "exited" - readonly pid: number - readonly exitCode?: number - } -} - -export type PtysGetInput = { - readonly ptyID: { readonly ptyID: string }["ptyID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type PtysGetOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: { - readonly id: string - readonly title: string - readonly command: string - readonly args: ReadonlyArray - readonly cwd: string - readonly status: "running" | "exited" - readonly pid: number - readonly exitCode?: number - } -} - -export type PtysUpdateInput = { - readonly ptyID: { readonly ptyID: string }["ptyID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly title?: { - readonly title?: string - readonly size?: { readonly rows: number; readonly cols: number } - }["title"] - readonly size?: { readonly title?: string; readonly size?: { readonly rows: number; readonly cols: number } }["size"] -} - -export type PtysUpdateOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: { - readonly id: string - readonly title: string - readonly command: string - readonly args: ReadonlyArray - readonly cwd: string - readonly status: "running" | "exited" - readonly pid: number - readonly exitCode?: number - } -} - -export type PtysRemoveInput = { - readonly ptyID: { readonly ptyID: string }["ptyID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type PtysRemoveOutput = void - -export type QuestionsListRequestsInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type QuestionsListRequestsOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: ReadonlyArray<{ - readonly id: string - readonly sessionID: string - readonly questions: ReadonlyArray<{ - readonly question: string - readonly header: string - readonly options: ReadonlyArray<{ readonly label: string; readonly description: string }> - readonly multiple?: boolean - readonly custom?: boolean - }> - readonly tool?: { readonly messageID: string; readonly callID: string } - }> -} - -export type QuestionsListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type QuestionsListOutput = { - readonly data: ReadonlyArray<{ - readonly id: string - readonly sessionID: string - readonly questions: ReadonlyArray<{ - readonly question: string - readonly header: string - readonly options: ReadonlyArray<{ readonly label: string; readonly description: string }> - readonly multiple?: boolean - readonly custom?: boolean - }> - readonly tool?: { readonly messageID: string; readonly callID: string } - }> -}["data"] - -export type QuestionsReplyInput = { - readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] - readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] - readonly answers: { readonly answers: ReadonlyArray> }["answers"] -} - -export type QuestionsReplyOutput = void - -export type QuestionsRejectInput = { - readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] - readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] -} - -export type QuestionsRejectOutput = void - -export type ReferencesListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type ReferencesListOutput = { - readonly location: { - readonly directory: string - readonly workspaceID?: string - readonly project: { readonly id: string; readonly directory: string } - } - readonly data: ReadonlyArray<{ - readonly name: string - readonly path: string - readonly description?: string - readonly hidden?: boolean - readonly source: - | { readonly type: "local"; readonly path: string; readonly description?: string; readonly hidden?: boolean } - | { - readonly type: "git" - readonly repository: string - readonly branch?: string - readonly description?: string - readonly hidden?: boolean - } - }> -} - -export type ProjectCopiesCreateInput = { - readonly projectID: { readonly projectID: string }["projectID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly strategy: { readonly strategy: string; readonly directory: string; readonly name?: string }["strategy"] - readonly directory: { readonly strategy: string; readonly directory: string; readonly name?: string }["directory"] - readonly name?: { readonly strategy: string; readonly directory: string; readonly name?: string }["name"] -} - -export type ProjectCopiesCreateOutput = { readonly directory: string } - -export type ProjectCopiesRemoveInput = { - readonly projectID: { readonly projectID: string }["projectID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly directory: { readonly directory: string; readonly force: boolean }["directory"] - readonly force: { readonly directory: string; readonly force: boolean }["force"] -} - -export type ProjectCopiesRemoveOutput = void - -export type ProjectCopiesRefreshInput = { - readonly projectID: { readonly projectID: string }["projectID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type ProjectCopiesRefreshOutput = void diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts deleted file mode 100644 index 6955d7d8c5..0000000000 --- a/packages/client/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./generated/index" -export type { EventsSubscribeOutput as OpenCodeEvent } from "./generated/types" diff --git a/packages/client/src/promise/api.ts b/packages/client/src/promise/api.ts new file mode 100644 index 0000000000..b4bc8635b2 --- /dev/null +++ b/packages/client/src/promise/api.ts @@ -0,0 +1,17 @@ +type Client = ReturnType + +export type AgentApi = Client["agent"] +export type CommandApi = Client["command"] +export type EventApi = Client["event"] +export type IntegrationApi = Client["integration"] +export type ModelApi = Client["model"] +export type PluginApi = Client["plugin"] +export type ProviderApi = Client["provider"] +export type ReferenceApi = Client["reference"] +export type SessionApi = Client["session"] +export type SkillApi = Client["skill"] + +export interface CatalogApi { + readonly provider: ProviderApi + readonly model: ModelApi +} diff --git a/packages/client/src/generated/.httpapi-codegen.json b/packages/client/src/promise/generated/.httpapi-codegen.json similarity index 100% rename from packages/client/src/generated/.httpapi-codegen.json rename to packages/client/src/promise/generated/.httpapi-codegen.json diff --git a/packages/client/src/generated/client-error.ts b/packages/client/src/promise/generated/client-error.ts similarity index 58% rename from packages/client/src/generated/client-error.ts rename to packages/client/src/promise/generated/client-error.ts index c278f0ddc8..930b612383 100644 --- a/packages/client/src/generated/client-error.ts +++ b/packages/client/src/promise/generated/client-error.ts @@ -1,4 +1,9 @@ -export type ClientErrorReason = "Transport" | "UnexpectedStatus" | "UnsupportedContentType" | "MalformedResponse" +export type ClientErrorReason = + | "Transport" + | "UnexpectedStatus" + | "UnsupportedContentType" + | "MalformedResponse" + | "SseEventTooLarge" export class ClientError extends Error { override readonly name = "ClientError" diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts new file mode 100644 index 0000000000..ea5e790ea6 --- /dev/null +++ b/packages/client/src/promise/generated/client.ts @@ -0,0 +1,1728 @@ +import type { + HealthGetOutput, + HealthStopInput, + HealthStopOutput, + ServerGetOutput, + LocationGetInput, + LocationGetOutput, + AgentListInput, + AgentListOutput, + PluginListInput, + PluginListOutput, + SessionListInput, + SessionListOutput, + SessionCreateInput, + SessionCreateOutput, + SessionActiveOutput, + SessionGetInput, + SessionGetOutput, + SessionRemoveInput, + SessionRemoveOutput, + SessionForkInput, + SessionForkOutput, + SessionSwitchAgentInput, + SessionSwitchAgentOutput, + SessionSwitchModelInput, + SessionSwitchModelOutput, + SessionRenameInput, + SessionRenameOutput, + SessionMoveInput, + SessionMoveOutput, + SessionPromptInput, + SessionPromptOutput, + SessionCommandInput, + SessionCommandOutput, + SessionSkillInput, + SessionSkillOutput, + SessionSyntheticInput, + SessionSyntheticOutput, + SessionShellInput, + SessionShellOutput, + SessionCompactInput, + SessionCompactOutput, + SessionWaitInput, + SessionWaitOutput, + SessionRevertStageInput, + SessionRevertStageOutput, + SessionRevertClearInput, + SessionRevertClearOutput, + SessionRevertCommitInput, + SessionRevertCommitOutput, + SessionContextInput, + SessionContextOutput, + SessionPendingListInput, + SessionPendingListOutput, + SessionInstructionsEntryListInput, + SessionInstructionsEntryListOutput, + SessionInstructionsEntryPutInput, + SessionInstructionsEntryPutOutput, + SessionInstructionsEntryRemoveInput, + SessionInstructionsEntryRemoveOutput, + SessionGenerateInput, + SessionGenerateOutput, + SessionLogInput, + SessionLogOutput, + SessionInterruptInput, + SessionInterruptOutput, + SessionBackgroundInput, + SessionBackgroundOutput, + SessionMessageInput, + SessionMessageOutput, + MessageListInput, + MessageListOutput, + ModelListInput, + ModelListOutput, + ModelDefaultInput, + ModelDefaultOutput, + GenerateTextInput, + GenerateTextOutput, + ProviderListInput, + ProviderListOutput, + ProviderGetInput, + ProviderGetOutput, + IntegrationListInput, + IntegrationListOutput, + IntegrationGetInput, + IntegrationGetOutput, + IntegrationWellknownAddInput, + IntegrationWellknownAddOutput, + IntegrationConnectKeyInput, + IntegrationConnectKeyOutput, + IntegrationOauthConnectInput, + IntegrationOauthConnectOutput, + IntegrationOauthStatusInput, + IntegrationOauthStatusOutput, + IntegrationOauthCompleteInput, + IntegrationOauthCompleteOutput, + IntegrationOauthCancelInput, + IntegrationOauthCancelOutput, + IntegrationCommandConnectInput, + IntegrationCommandConnectOutput, + IntegrationCommandStatusInput, + IntegrationCommandStatusOutput, + IntegrationCommandCancelInput, + IntegrationCommandCancelOutput, + McpListInput, + McpListOutput, + McpResourceCatalogInput, + McpResourceCatalogOutput, + CredentialUpdateInput, + CredentialUpdateOutput, + CredentialRemoveInput, + CredentialRemoveOutput, + ProjectListOutput, + ProjectCurrentInput, + ProjectCurrentOutput, + ProjectDirectoriesInput, + ProjectDirectoriesOutput, + FormRequestListInput, + FormRequestListOutput, + FormListInput, + FormListOutput, + FormCreateInput, + FormCreateOutput, + FormGetInput, + FormGetOutput, + FormStateInput, + FormStateOutput, + FormReplyInput, + FormReplyOutput, + FormCancelInput, + FormCancelOutput, + PermissionRequestListInput, + PermissionRequestListOutput, + PermissionSavedListInput, + PermissionSavedListOutput, + PermissionSavedRemoveInput, + PermissionSavedRemoveOutput, + PermissionCreateInput, + PermissionCreateOutput, + PermissionListInput, + PermissionListOutput, + PermissionGetInput, + PermissionGetOutput, + PermissionReplyInput, + PermissionReplyOutput, + FileReadInput, + FileReadOutput, + FileListInput, + FileListOutput, + FileFindInput, + FileFindOutput, + CommandListInput, + CommandListOutput, + SkillListInput, + SkillListOutput, + EventSubscribeOutput, + PtyListInput, + PtyListOutput, + PtyCreateInput, + PtyCreateOutput, + PtyGetInput, + PtyGetOutput, + PtyUpdateInput, + PtyUpdateOutput, + PtyRemoveInput, + PtyRemoveOutput, + ShellListInput, + ShellListOutput, + ShellCreateInput, + ShellCreateOutput, + ShellGetInput, + ShellGetOutput, + ShellTimeoutInput, + ShellTimeoutOutput, + ShellOutputInput, + ShellOutputOutput, + ShellRemoveInput, + ShellRemoveOutput, + QuestionRequestListInput, + QuestionRequestListOutput, + QuestionListInput, + QuestionListOutput, + QuestionReplyInput, + QuestionReplyOutput, + QuestionRejectInput, + QuestionRejectOutput, + ReferenceListInput, + ReferenceListOutput, + ProjectCopyCreateInput, + ProjectCopyCreateOutput, + ProjectCopyRemoveInput, + ProjectCopyRemoveOutput, + ProjectCopyRefreshInput, + ProjectCopyRefreshOutput, + VcsStatusInput, + VcsStatusOutput, + VcsDiffInput, + VcsDiffOutput, + DebugLocationListOutput, + DebugLocationEvictInput, + DebugLocationEvictOutput, +} from "./types" +import { ClientError } from "./client-error" + +export interface ClientOptions { + readonly baseUrl: string + readonly fetch?: typeof globalThis.fetch + readonly headers?: RequestInit["headers"] +} + +export interface RequestOptions { + readonly signal?: AbortSignal + readonly headers?: RequestInit["headers"] +} + +interface RequestDescriptor { + readonly method: string + readonly path: string + readonly query?: Record + readonly headers?: Record + readonly body?: unknown + readonly successStatus: number + readonly declaredStatuses: ReadonlyArray + readonly empty: boolean + readonly binary?: true +} + +const maxSseEventBytes = 16 * 1024 * 1024 + +export function make(options: ClientOptions) { + const fetch = options.fetch ?? globalThis.fetch + + const prepare = (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => { + const url = new URL(descriptor.path, options.baseUrl) + for (const [key, value] of Object.entries(descriptor.query ?? {})) appendQuery(url.searchParams, key, value) + const headers = new Headers(options.headers) + for (const [key, value] of Object.entries(descriptor.headers ?? {})) { + if (value !== undefined && value !== null) headers.set(key, String(value)) + } + for (const [key, value] of new Headers(requestOptions?.headers)) headers.set(key, value) + if (descriptor.body !== undefined && !headers.has("content-type")) headers.set("content-type", "application/json") + return { + url, + init: { + method: descriptor.method, + signal: requestOptions?.signal, + headers, + body: descriptor.body === undefined ? undefined : JSON.stringify(descriptor.body), + } satisfies RequestInit, + } + } + + const execute = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => { + try { + const prepared = prepare(descriptor, requestOptions) + return await fetch(prepared.url, prepared.init) + } catch (cause) { + throw new ClientError("Transport", { cause }) + } + } + + const responseError = async (response: Response, descriptor: RequestDescriptor): Promise => { + if (descriptor.declaredStatuses.includes(response.status)) throw await json(response) + try { + await response.body?.cancel() + } catch {} + throw new ClientError("UnexpectedStatus", { cause: { status: response.status } }) + } + + const request = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions): Promise => { + const response = await execute(descriptor, requestOptions) + if (response.status !== descriptor.successStatus) return responseError(response, descriptor) + if (descriptor.binary) return new Uint8Array(await response.arrayBuffer()) as A + if (descriptor.empty) { + try { + await response.body?.cancel() + } catch {} + return undefined as A + } + return (await json(response)) as A + } + + const sse = (descriptor: RequestDescriptor, requestOptions?: RequestOptions): AsyncIterable => ({ + async *[Symbol.asyncIterator]() { + const response = await execute(descriptor, requestOptions) + if (response.status !== descriptor.successStatus) await responseError(response, descriptor) + if (!isContentType(response, "text/event-stream")) { + try { + await response.body?.cancel() + } catch {} + throw new ClientError("UnsupportedContentType") + } + if (response.body === null) throw new ClientError("MalformedResponse") + const reader = response.body.getReader() + const decoder = new TextDecoder() + let buffer = "" + try { + while (true) { + let next + try { + next = await reader.read() + } catch (cause) { + throw new ClientError("Transport", { cause }) + } + buffer += decoder.decode(next.value, { stream: !next.done }) + if (buffer.length > maxSseEventBytes) throw new ClientError("SseEventTooLarge") + const trailingCarriageReturn = !next.done && buffer.endsWith("\r") + if (trailingCarriageReturn) buffer = buffer.slice(0, -1) + buffer = buffer.replaceAll("\r\n", "\n").replaceAll("\r", "\n") + if (trailingCarriageReturn) buffer += "\r" + if (next.done && buffer !== "") buffer += "\n\n" + let boundary = buffer.indexOf("\n\n") + while (boundary >= 0) { + const block = buffer.slice(0, boundary) + buffer = buffer.slice(boundary + 2) + const data = block + .split("\n") + .flatMap((line) => (line.startsWith("data:") ? [line.slice(5).trimStart()] : [])) + .join("\n") + if (data !== "") { + try { + yield JSON.parse(data) as A + } catch (cause) { + throw new ClientError("MalformedResponse", { cause }) + } + } + boundary = buffer.indexOf("\n\n") + } + if (next.done) return + } + } finally { + try { + await reader.cancel() + } catch {} + reader.releaseLock() + } + }, + }) + + return { + health: { + get: (requestOptions?: RequestOptions) => + request( + { method: "GET", path: `/api/health`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, + requestOptions, + ), + stop: (input: HealthStopInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/service/stop`, + body: { instanceID: input["instanceID"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + server: { + get: (requestOptions?: RequestOptions) => + request( + { method: "GET", path: `/api/server`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, + requestOptions, + ), + }, + location: { + get: (input?: LocationGetInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/location`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + agent: { + list: (input?: AgentListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/agent`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + plugin: { + list: (input?: PluginListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/plugin`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + session: { + list: (input?: SessionListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/session`, + query: { + workspace: input?.["workspace"], + limit: input?.["limit"], + order: input?.["order"], + search: input?.["search"], + parentID: input?.["parentID"], + directory: input?.["directory"], + project: input?.["project"], + subpath: input?.["subpath"], + cursor: input?.["cursor"], + }, + successStatus: 200, + declaredStatuses: [400, 401], + empty: false, + }, + requestOptions, + ), + create: (input?: SessionCreateInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionCreateOutput }>( + { + method: "POST", + path: `/api/session`, + body: { + id: input?.["id"], + agent: input?.["agent"], + model: input?.["model"], + location: input?.["location"], + }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + active: (requestOptions?: RequestOptions) => + request<{ readonly data: SessionActiveOutput }>( + { + method: "GET", + path: `/api/session/active`, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + get: (input: SessionGetInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionGetOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}`, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + remove: (input: SessionRemoveInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/session/${encodeURIComponent(input.sessionID)}`, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + fork: (input: SessionForkInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionForkOutput }>( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/fork`, + body: { messageID: input["messageID"] }, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + switchAgent: (input: SessionSwitchAgentInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/agent`, + body: { agent: input["agent"] }, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + switchModel: (input: SessionSwitchModelInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/model`, + body: { model: input["model"] }, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + rename: (input: SessionRenameInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/rename`, + body: { title: input["title"] }, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + move: (input: SessionMoveInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/move`, + body: { directory: input["directory"], workspaceID: input["workspaceID"] }, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + prompt: (input: SessionPromptInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionPromptOutput }>( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/prompt`, + body: { + id: input["id"], + text: input["text"], + files: input["files"], + agents: input["agents"], + metadata: input["metadata"], + delivery: input["delivery"], + resume: input["resume"], + }, + successStatus: 200, + declaredStatuses: [409, 400, 404, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + command: (input: SessionCommandInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionCommandOutput }>( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/command`, + body: { + id: input["id"], + command: input["command"], + arguments: input["arguments"], + agent: input["agent"], + model: input["model"], + files: input["files"], + agents: input["agents"], + delivery: input["delivery"], + resume: input["resume"], + }, + successStatus: 200, + declaredStatuses: [409, 400, 404, 500, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + skill: (input: SessionSkillInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/skill`, + body: { id: input["id"], skill: input["skill"], resume: input["resume"] }, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + synthetic: (input: SessionSyntheticInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionSyntheticOutput }>( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/synthetic`, + body: { + id: input["id"], + text: input["text"], + description: input["description"], + metadata: input["metadata"], + delivery: input["delivery"], + resume: input["resume"], + }, + successStatus: 200, + declaredStatuses: [409, 404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + shell: (input: SessionShellInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/shell`, + body: { id: input["id"], command: input["command"] }, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + compact: (input: SessionCompactInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionCompactOutput }>( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/compact`, + body: { id: input["id"] }, + successStatus: 200, + declaredStatuses: [409, 404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + wait: (input: SessionWaitInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/wait`, + successStatus: 204, + declaredStatuses: [404, 503, 400, 401], + empty: true, + }, + requestOptions, + ), + revert: { + stage: (input: SessionRevertStageInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionRevertStageOutput }>( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`, + body: { messageID: input["messageID"], files: input["files"] }, + successStatus: 200, + declaredStatuses: [404, 409, 500, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + clear: (input: SessionRevertClearInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`, + successStatus: 204, + declaredStatuses: [404, 409, 500, 400, 401], + empty: true, + }, + requestOptions, + ), + commit: (input: SessionRevertCommitInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`, + successStatus: 204, + declaredStatuses: [404, 409, 400, 401], + empty: true, + }, + requestOptions, + ), + }, + context: (input: SessionContextInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionContextOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/context`, + successStatus: 200, + declaredStatuses: [404, 500, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + pending: { + list: (input: SessionPendingListInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionPendingListOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/pending`, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + }, + instructions: { + entry: { + list: (input: SessionInstructionsEntryListInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionInstructionsEntryListOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/instructions/entries`, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + put: (input: SessionInstructionsEntryPutInput, requestOptions?: RequestOptions) => + request( + { + method: "PUT", + path: `/api/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`, + body: { value: input["value"] }, + successStatus: 204, + declaredStatuses: [404, 413, 400, 401], + empty: true, + }, + requestOptions, + ), + remove: (input: SessionInstructionsEntryRemoveInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + }, + }, + generate: (input: SessionGenerateInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionGenerateOutput }>( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/generate`, + body: { prompt: input["prompt"] }, + successStatus: 200, + declaredStatuses: [404, 503, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + log: (input: SessionLogInput, requestOptions?: RequestOptions): AsyncIterable => + sse( + { + method: "GET", + path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/log`, + query: { after: input["after"], follow: input["follow"] }, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ), + interrupt: (input: SessionInterruptInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/interrupt`, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + background: (input: SessionBackgroundInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/background`, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + message: (input: SessionMessageInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionMessageOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/message/${encodeURIComponent(input.messageID)}`, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + }, + message: { + list: (input: MessageListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/message`, + query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] }, + successStatus: 200, + declaredStatuses: [400, 404, 500, 401], + empty: false, + }, + requestOptions, + ), + }, + model: { + list: (input?: ModelListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/model`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [503, 401, 400], + empty: false, + }, + requestOptions, + ), + default: (input?: ModelDefaultInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/model/default`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [503, 401, 400], + empty: false, + }, + requestOptions, + ), + }, + generate: { + text: (input: GenerateTextInput, requestOptions?: RequestOptions) => + request<{ readonly data: GenerateTextOutput }>( + { + method: "POST", + path: `/api/generate`, + query: { location: input["location"] }, + body: { prompt: input["prompt"], model: input["model"] }, + successStatus: 200, + declaredStatuses: [400, 503, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + }, + provider: { + list: (input?: ProviderListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/provider`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [503, 401, 400], + empty: false, + }, + requestOptions, + ), + get: (input: ProviderGetInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/provider/${encodeURIComponent(input.providerID)}`, + query: { location: input["location"] }, + successStatus: 200, + declaredStatuses: [404, 503, 401, 400], + empty: false, + }, + requestOptions, + ), + }, + integration: { + list: (input?: IntegrationListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/integration`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + get: (input: IntegrationGetInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/integration/${encodeURIComponent(input.integrationID)}`, + query: { location: input["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + wellknown: { + add: (input: IntegrationWellknownAddInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/experimental/integration/wellknown`, + query: { location: input["location"] }, + body: { url: input["url"] }, + successStatus: 204, + declaredStatuses: [400, 401], + empty: true, + }, + requestOptions, + ), + }, + connect: { + key: (input: IntegrationConnectKeyInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/key`, + query: { location: input["location"] }, + body: { key: input["key"], label: input["label"] }, + successStatus: 204, + declaredStatuses: [400, 401], + empty: true, + }, + requestOptions, + ), + }, + oauth: { + connect: (input: IntegrationOauthConnectInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`, + query: { location: input["location"] }, + body: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] }, + successStatus: 200, + declaredStatuses: [400, 401], + empty: false, + }, + requestOptions, + ), + status: (input: IntegrationOauthStatusInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}`, + query: { location: input["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + complete: (input: IntegrationOauthCompleteInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}/complete`, + query: { location: input["location"] }, + body: { code: input["code"] }, + successStatus: 204, + declaredStatuses: [400, 401], + empty: true, + }, + requestOptions, + ), + cancel: (input: IntegrationOauthCancelInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}`, + query: { location: input["location"] }, + successStatus: 204, + declaredStatuses: [401, 400], + empty: true, + }, + requestOptions, + ), + }, + command: { + connect: (input: IntegrationCommandConnectInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/command`, + query: { location: input["location"] }, + body: { methodID: input["methodID"], label: input["label"] }, + successStatus: 200, + declaredStatuses: [400, 401], + empty: false, + }, + requestOptions, + ), + status: (input: IntegrationCommandStatusInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/command/${encodeURIComponent(input.attemptID)}`, + query: { location: input["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + cancel: (input: IntegrationCommandCancelInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/command/${encodeURIComponent(input.attemptID)}`, + query: { location: input["location"] }, + successStatus: 204, + declaredStatuses: [401, 400], + empty: true, + }, + requestOptions, + ), + }, + }, + mcp: { + list: (input?: McpListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/mcp`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + resource: { + catalog: (input?: McpResourceCatalogInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/mcp/resource`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + }, + credential: { + update: (input: CredentialUpdateInput, requestOptions?: RequestOptions) => + request( + { + method: "PATCH", + path: `/api/credential/${encodeURIComponent(input.credentialID)}`, + query: { location: input["location"] }, + body: { label: input["label"] }, + successStatus: 204, + declaredStatuses: [401, 400], + empty: true, + }, + requestOptions, + ), + remove: (input: CredentialRemoveInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/credential/${encodeURIComponent(input.credentialID)}`, + query: { location: input["location"] }, + successStatus: 204, + declaredStatuses: [401, 400], + empty: true, + }, + requestOptions, + ), + }, + project: { + list: (requestOptions?: RequestOptions) => + request( + { method: "GET", path: `/api/project`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, + requestOptions, + ), + current: (input?: ProjectCurrentInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/project/current`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + directories: (input: ProjectDirectoriesInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/project/${encodeURIComponent(input.projectID)}/directories`, + query: { location: input["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + form: { + request: { + list: (input?: FormRequestListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/form/request`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + list: (input: FormListInput, requestOptions?: RequestOptions) => + request<{ readonly data: FormListOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/form`, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + create: (input: FormCreateInput, requestOptions?: RequestOptions) => + request<{ readonly data: FormCreateOutput }>( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/form`, + body: { id: input["id"], title: input["title"], metadata: input["metadata"], fields: input["fields"] }, + successStatus: 200, + declaredStatuses: [404, 409, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + get: (input: FormGetInput, requestOptions?: RequestOptions) => + request<{ readonly data: FormGetOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}`, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + state: (input: FormStateInput, requestOptions?: RequestOptions) => + request<{ readonly data: FormStateOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/state`, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + reply: (input: FormReplyInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/reply`, + body: { answer: input["answer"] }, + successStatus: 204, + declaredStatuses: [404, 409, 400, 401], + empty: true, + }, + requestOptions, + ), + cancel: (input: FormCancelInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/cancel`, + successStatus: 204, + declaredStatuses: [404, 409, 400, 401], + empty: true, + }, + requestOptions, + ), + }, + permission: { + request: { + list: (input?: PermissionRequestListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/permission/request`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + saved: { + list: (input?: PermissionSavedListInput, requestOptions?: RequestOptions) => + request<{ readonly data: PermissionSavedListOutput }>( + { + method: "GET", + path: `/api/permission/saved`, + query: { projectID: input?.["projectID"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + remove: (input: PermissionSavedRemoveInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/permission/saved/${encodeURIComponent(input.id)}`, + successStatus: 204, + declaredStatuses: [401, 400], + empty: true, + }, + requestOptions, + ), + }, + create: (input: PermissionCreateInput, requestOptions?: RequestOptions) => + request<{ readonly data: PermissionCreateOutput }>( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/permission`, + body: { + id: input["id"], + action: input["action"], + resources: input["resources"], + save: input["save"], + metadata: input["metadata"], + source: input["source"], + agent: input["agent"], + }, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + list: (input: PermissionListInput, requestOptions?: RequestOptions) => + request<{ readonly data: PermissionListOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/permission`, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + get: (input: PermissionGetInput, requestOptions?: RequestOptions) => + request<{ readonly data: PermissionGetOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}`, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + reply: (input: PermissionReplyInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}/reply`, + body: { reply: input["reply"], message: input["message"] }, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + }, + file: { + read: (input: FileReadInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/fs/read/${encodePath(input.path)}`, + query: { location: input["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + binary: true, + }, + requestOptions, + ), + list: (input?: FileListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/fs/list`, + query: { location: input?.["location"], path: input?.["path"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + find: (input: FileFindInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/fs/find`, + query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + command: { + list: (input?: CommandListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/command`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + skill: { + list: (input?: SkillListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/skill`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + event: { + subscribe: (requestOptions?: RequestOptions): AsyncIterable => + sse( + { method: "GET", path: `/api/event`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, + requestOptions, + ), + }, + pty: { + list: (input?: PtyListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/pty`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + create: (input?: PtyCreateInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/pty`, + query: { location: input?.["location"] }, + body: { + command: input?.["command"], + args: input?.["args"], + cwd: input?.["cwd"], + title: input?.["title"], + env: input?.["env"], + }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + get: (input: PtyGetInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/pty/${encodeURIComponent(input.ptyID)}`, + query: { location: input["location"] }, + successStatus: 200, + declaredStatuses: [404, 401, 400], + empty: false, + }, + requestOptions, + ), + update: (input: PtyUpdateInput, requestOptions?: RequestOptions) => + request( + { + method: "PUT", + path: `/api/pty/${encodeURIComponent(input.ptyID)}`, + query: { location: input["location"] }, + body: { title: input["title"], size: input["size"] }, + successStatus: 200, + declaredStatuses: [404, 401, 400], + empty: false, + }, + requestOptions, + ), + remove: (input: PtyRemoveInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/pty/${encodeURIComponent(input.ptyID)}`, + query: { location: input["location"] }, + successStatus: 204, + declaredStatuses: [404, 401, 400], + empty: true, + }, + requestOptions, + ), + }, + shell: { + list: (input?: ShellListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/shell`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + create: (input: ShellCreateInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/shell`, + query: { location: input["location"] }, + body: { + command: input["command"], + cwd: input["cwd"], + timeout: input["timeout"], + metadata: input["metadata"], + }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + get: (input: ShellGetInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/shell/${encodeURIComponent(input.id)}`, + query: { location: input["location"] }, + successStatus: 200, + declaredStatuses: [404, 401, 400], + empty: false, + }, + requestOptions, + ), + timeout: (input: ShellTimeoutInput, requestOptions?: RequestOptions) => + request( + { + method: "PATCH", + path: `/api/shell/${encodeURIComponent(input.id)}/timeout`, + query: { location: input["location"] }, + body: { timeout: input["timeout"] }, + successStatus: 200, + declaredStatuses: [404, 401, 400], + empty: false, + }, + requestOptions, + ), + output: (input: ShellOutputInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/shell/${encodeURIComponent(input.id)}/output`, + query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] }, + successStatus: 200, + declaredStatuses: [404, 401, 400], + empty: false, + }, + requestOptions, + ), + remove: (input: ShellRemoveInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/shell/${encodeURIComponent(input.id)}`, + query: { location: input["location"] }, + successStatus: 204, + declaredStatuses: [404, 401, 400], + empty: true, + }, + requestOptions, + ), + }, + question: { + request: { + list: (input?: QuestionRequestListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/question/request`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + list: (input: QuestionListInput, requestOptions?: RequestOptions) => + request<{ readonly data: QuestionListOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/question`, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + reply: (input: QuestionReplyInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/question/${encodeURIComponent(input.requestID)}/reply`, + body: { answers: input["answers"] }, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + reject: (input: QuestionRejectInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/question/${encodeURIComponent(input.requestID)}/reject`, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + }, + reference: { + list: (input?: ReferenceListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/reference`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + projectCopy: { + create: (input: ProjectCopyCreateInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy`, + query: { location: input["location"] }, + body: { strategy: input["strategy"], directory: input["directory"], name: input["name"] }, + successStatus: 200, + declaredStatuses: [400, 401], + empty: false, + }, + requestOptions, + ), + remove: (input: ProjectCopyRemoveInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy`, + query: { location: input["location"] }, + body: { directory: input["directory"], force: input["force"] }, + successStatus: 204, + declaredStatuses: [400, 401], + empty: true, + }, + requestOptions, + ), + refresh: (input: ProjectCopyRefreshInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy/refresh`, + query: { location: input["location"] }, + successStatus: 204, + declaredStatuses: [400, 401], + empty: true, + }, + requestOptions, + ), + }, + vcs: { + status: (input?: VcsStatusInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/vcs/status`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + diff: (input: VcsDiffInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/vcs/diff`, + query: { location: input["location"], mode: input["mode"], context: input["context"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + debug: { + location: { + list: (requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/debug/location`, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + evict: (input?: DebugLocationEvictInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/debug/location`, + query: { location: input?.["location"] }, + successStatus: 204, + declaredStatuses: [401, 400], + empty: true, + }, + requestOptions, + ), + }, + }, + } +} + +function encodePath(value: string): string { + return value.split("/").map(encodeURIComponent).join("/") +} + +function appendQuery(params: URLSearchParams, key: string, value: unknown): void { + if (value === undefined) return + if (value === null) { + params.append(key, "null") + return + } + if (Array.isArray(value)) { + for (const item of value) appendQuery(params, key, item) + return + } + if (typeof value === "object") { + for (const [child, item] of Object.entries(value)) appendQuery(params, `${key}[${child}]`, item) + return + } + params.append(key, String(value)) +} + +async function json(response: Response): Promise { + if (!isContentType(response, "application/json") && !response.headers.get("content-type")?.includes("+json")) { + try { + await response.body?.cancel() + } catch {} + throw new ClientError("UnsupportedContentType") + } + let text: string + try { + text = await response.text() + } catch (cause) { + throw new ClientError("Transport", { cause }) + } + if (text === "") throw new ClientError("MalformedResponse") + try { + return JSON.parse(text) + } catch (cause) { + throw new ClientError("MalformedResponse", { cause }) + } +} + +function isContentType(response: Response, expected: string) { + return response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() === expected +} diff --git a/packages/client/src/generated/index.ts b/packages/client/src/promise/generated/index.ts similarity index 100% rename from packages/client/src/generated/index.ts rename to packages/client/src/promise/generated/index.ts diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts new file mode 100644 index 0000000000..3ae799a87e --- /dev/null +++ b/packages/client/src/promise/generated/types.ts @@ -0,0 +1,4881 @@ +export type JsonValue = null | boolean | number | string | Array | { [key: string]: JsonValue } + +export type ServiceHealth = { healthy: true; version: string; pid: number } + +export type ServiceStopResponse = { accepted: boolean } + +export type ModelRef = { id: string; providerID: string; variant?: string } + +export type ProviderSettings = { [x: string]: JsonValue } + +export type AgentColor = string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info" + +export type PermissionV2Effect = "allow" | "deny" | "ask" + +export type PluginInfo = { id: string } + +export type MoneyUSD = number + +export type TokenUsageInfo = { + input: number + output: number + reasoning: number + cache: { read: number; write: number } +} + +export type LocationRef = { directory: string; workspaceID?: string } + +export type FileDiffInfo = { + file: string + patch: string + additions: number + deletions: number + status: "added" | "deleted" | "modified" +} + +export type SessionActive = { type: "running" } + +export type PromptBase64 = string + +export type PromptFileSource = { type: "inline" } | { type: "uri"; uri: string } + +export type PromptMention = { start: number; end: number; text: string } + +export type SessionPendingSyntheticData = { text: string; description?: string; metadata?: { [x: string]: JsonValue } } + +export type SessionPendingCompaction = { + admittedSeq: number + id: string + sessionID: string + timeCreated: number + type: "compaction" +} + +export type SessionMessageAgentSelected = { + id: string + metadata?: { [x: string]: JsonValue } + time: { created: number } + type: "agent-switched" + agent: string +} + +export type SessionMessageSynthetic = { + id: string + metadata?: { [x: string]: JsonValue } + time: { created: number } + text: string + description?: string + type: "synthetic" +} + +export type SessionMessageSystem = { + id: string + metadata?: { [x: string]: JsonValue } + time: { created: number } + type: "system" + text: string +} + +export type SessionMessageSkill = { + id: string + metadata?: { [x: string]: JsonValue } + time: { created: number } + type: "skill" + skill: string + name: string + text: string +} + +export type SessionMessageShell = { + id: string + metadata?: { [x: string]: JsonValue } + time: { created: number; completed?: number } + type: "shell" + shellID: string + command: string + status: "running" | "exited" | "timeout" | "killed" + exit?: number | "Infinity" | "-Infinity" | "NaN" + output?: { output: string; cursor: number; size: number; truncated: boolean } +} + +export type SessionMessageAssistantText = { type: "text"; text: string } + +export type SessionMessageProviderState = { [x: string]: JsonValue } + +export type SessionMessageToolStateStreaming = { status: "streaming"; input: string } + +export type ToolTextContent = { type: "text"; text: string } + +export type ToolFileContent = { type: "file"; uri: string; mime: string; name?: string } + +export type SessionStructuredError = { type: string; message: string } + +export type SessionMessageCompactionRunning = { + type: "compaction" + id: string + metadata?: { [x: string]: JsonValue } + time: { created: number } + status: "running" + reason: "auto" | "manual" + summary: string + recent: string +} + +export type SessionMessageCompactionCompleted = { + type: "compaction" + id: string + metadata?: { [x: string]: JsonValue } + time: { created: number } + status: "completed" + reason: "auto" | "manual" + summary: string + recent: string +} + +export type InstructionEntryKey = string + +export type SessionGenerateResponse = { data: { text: string } } + +export type SessionPendingSyntheticData1 = { text: string; description?: string; metadata?: { [x: string]: any } } + +export type ShellInfo = { + id: string + status: "running" | "exited" | "timeout" | "killed" + command: string + cwd: string + shell: string + file: string + pid?: number + exit?: number + metadata: { [x: string]: any } + time: { started: number; completed?: number } +} + +export type SessionMessageProviderState3 = { [x: string]: any } + +export type SessionMessageProviderState4 = { [x: string]: any } + +export type SessionMessageProviderState5 = { [x: string]: any } + +export type SessionMessageProviderState6 = { [x: string]: any } + +export type SessionMessageProviderState7 = { [x: string]: any } + +export type SessionMessageProviderState8 = { [x: string]: any } + +export type EventLogSynced = { type: "log.synced"; aggregateID: string; seq?: number } + +export type ModelCapabilities = { tools: boolean; input: Array; output: Array } + +export type ModelVariant = { + id: string + settings?: { [x: string]: JsonValue } + headers?: { [x: string]: string } + body?: { [x: string]: JsonValue } +} + +export type MoneyUSDPerMillionTokens = number + +export type GenerateTextResponse = { data: { text: string } } + +export type ProviderV2Info = { + id: string + integrationID?: string + name: string + disabled?: boolean + package: string + settings?: { [x: string]: JsonValue } + headers?: { [x: string]: string } + body?: { [x: string]: JsonValue } +} + +export type IntegrationWhen = { key: string; op: "eq" | "neq"; value: string } + +export type IntegrationCommandMethod = { id: string; type: "command"; label: string; command: Array } + +export type IntegrationKeyMethod = { type: "key"; label?: string } + +export type IntegrationEnvMethod = { type: "env"; names: Array } + +export type ConnectionCredentialInfo = { type: "credential"; id: string; label: string } + +export type ConnectionEnvInfo = { type: "env"; name: string } + +export type IntegrationAttemptStatus = + | { + status: "pending" + time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" } + } + | { + status: "complete" + time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" } + } + | { + status: "failed" + message: string + time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" } + } + | { + status: "expired" + time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" } + } + +export type IntegrationCommandAttempt = { + attemptID: string + time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" } +} + +export type IntegrationCommandAttemptStatus = + | { + status: "pending" + message?: string + time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" } + } + | { + status: "complete" + time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" } + } + | { + status: "failed" + message: string + time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" } + } + | { + status: "expired" + time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" } + } + +export type McpStatusConnected = { status: "connected" } + +export type McpStatusPending = { status: "pending" } + +export type McpStatusDisabled = { status: "disabled" } + +export type McpStatusFailed = { status: "failed"; error: string } + +export type McpStatusNeedsAuth = { status: "needs_auth" } + +export type McpStatusNeedsClientRegistration = { status: "needs_client_registration"; error: string } + +export type McpResource = { server: string; name: string; uri: string; description?: string; mimeType?: string } + +export type McpResourceTemplate = { + server: string + name: string + uriTemplate: string + description?: string + mimeType?: string +} + +export type ProjectVcs = "git" | "hg" + +export type ProjectIcon = { url?: string; override?: string; color?: string } + +export type ProjectCommands = { start?: string } + +export type ProjectTime = { created: number; updated: number; initialized?: number } + +export type ProjectCurrent = { id: string; directory: string } + +export type ProjectDirectory = { directory: string; strategy?: string } + +export type FormMetadata = { [x: string]: JsonValue } + +export type FormWhen = { + key: string + op: "eq" | "neq" + value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean +} + +export type FormOption = { value: string; label: string; description?: string } + +export type FormExternalField = { key: string; type: "external"; url: string; title?: string; description?: string } + +export type FormValue = string | number | boolean | Array + +export type PermissionV2Source = { type: "tool"; messageID: string; callID: string } + +export type PermissionSavedInfo = { id: string; projectID: string; action: string; resource: string } + +export type FileSystemEntry = { path: string; type: "file" | "directory" } + +export type SkillInfo = { + id: string + name: string + description?: string + slash?: boolean + autoinvoke?: boolean + location: string + content: string +} + +export type FileDiffLegacyInfo = { + file?: string + patch?: string + additions: number + deletions: number + status?: "added" | "deleted" | "modified" +} + +export type PermissionAction = "allow" | "deny" | "ask" + +export type JSONSchema = { [x: string]: any } + +export type ProviderAuthError = { name: "ProviderAuthError"; data: { providerID: string; message: string } } + +export type UnknownError2 = { name: "UnknownError"; data: { message: string; ref?: string | undefined } } + +export type MessageOutputLengthError = { name: "MessageOutputLengthError"; data: {} } + +export type MessageAbortedError = { name: "MessageAbortedError"; data: { message: string } } + +export type StructuredOutputError = { name: "StructuredOutputError"; data: { message: string; retries: number } } + +export type ContextOverflowError = { + name: "ContextOverflowError" + data: { message: string; responseBody?: string | undefined } +} + +export type ContentFilterError = { name: "ContentFilterError"; data: { message: string } } + +export type APIError = { + name: "APIError" + data: { + message: string + statusCode?: number | undefined + isRetryable: boolean + responseHeaders?: { [x: string]: string } | undefined + responseBody?: string | undefined + metadata?: { [x: string]: string } | undefined + } +} + +export type TextPart = { + id: string + sessionID: string + messageID: string + type: "text" + text: string + synthetic?: boolean | undefined + ignored?: boolean | undefined + time?: { start: number; end?: number | undefined } | undefined + metadata?: { [x: string]: any } | undefined +} + +export type SubtaskPart = { + id: string + sessionID: string + messageID: string + type: "subtask" + prompt: string + description: string + agent: string + model?: { providerID: string; modelID: string } | undefined + command?: string | undefined +} + +export type ReasoningPart = { + id: string + sessionID: string + messageID: string + type: "reasoning" + text: string + metadata?: { [x: string]: any } | undefined + time: { start: number; end?: number | undefined } +} + +export type FilePartSourceText = { value: string; start: number; end: number } + +export type Range = { start: { line: number; character: number }; end: { line: number; character: number } } + +export type ToolStatePending = { status: "pending"; input: { [x: string]: any }; raw: string } + +export type ToolStateRunning = { + status: "running" + input: { [x: string]: any } + title?: string | undefined + metadata?: { [x: string]: any } | undefined + time: { start: number } +} + +export type ToolStateError = { + status: "error" + input: { [x: string]: any } + error: string + metadata?: { [x: string]: any } | undefined + time: { start: number; end: number } +} + +export type StepStartPart = { + id: string + sessionID: string + messageID: string + type: "step-start" + snapshot?: string | undefined +} + +export type StepFinishPart = { + id: string + sessionID: string + messageID: string + type: "step-finish" + reason: string + snapshot?: string | undefined + cost: number + tokens: { + total?: number | undefined + input: number + output: number + reasoning: number + cache: { read: number; write: number } + } +} + +export type SnapshotPart = { id: string; sessionID: string; messageID: string; type: "snapshot"; snapshot: string } + +export type PatchPart = { + id: string + sessionID: string + messageID: string + type: "patch" + hash: string + files: Array +} + +export type AgentPart = { + id: string + sessionID: string + messageID: string + type: "agent" + name: string + source?: { value: string; start: number; end: number } | undefined +} + +export type CompactionPart = { + id: string + sessionID: string + messageID: string + type: "compaction" + auto: boolean + overflow?: boolean | undefined + tail_start_id?: string | undefined +} + +export type PermissionV2Reply = "once" | "always" | "reject" + +export type Pty = { + id: string + title: string + command: string + args: Array + cwd: string + status: "running" | "exited" + pid: number + exitCode?: number +} + +export type QuestionV2Option = { label: string; description: string } + +export type QuestionV2Tool = { messageID: string; callID: string } + +export type QuestionV2Answer = Array + +export type FormMetadata1 = { [x: string]: any } + +export type FormWhen1 = { key: string; op: "eq" | "neq"; value: string | number | boolean } + +export type SessionStatus = + | { type: "idle" } + | { + type: "retry" + attempt: number + message: string + action?: { reason: string; provider: string; title: string; message: string; label: string; link?: string } + next: number + } + | { type: "busy" } + +export type QuestionOption = { label: string; description: string } + +export type QuestionTool = { messageID: string; callID: string } + +export type QuestionAnswer = Array + +export type ShellInfo1 = { + id: string + status: "running" | "exited" | "timeout" | "killed" + command: string + cwd: string + shell: string + file: string + pid?: number + exit?: number + metadata: { [x: string]: JsonValue } + time: { started: number; completed?: number } +} + +export type ReferenceLocalSource = { type: "local"; path: string; description?: string; hidden?: boolean } + +export type ReferenceGitSource = { + type: "git" + repository: string + branch?: string + description?: string + hidden?: boolean +} + +export type ProjectCopyCopy = { directory: string } + +export type VcsFileStatus = { + file: string + additions: number + deletions: number + status: "added" | "deleted" | "modified" +} + +export type SessionMessageModelSelected = { + id: string + metadata?: { [x: string]: JsonValue } + time: { created: number } + type: "model-switched" + model: ModelRef + previous?: ModelRef +} + +export type CommandInfo = { + name: string + template: string + description?: string + agent?: string + model?: ModelRef + subtask?: boolean +} + +export type ProviderRequest = { + settings: ProviderSettings + headers: { [x: string]: string } + body: { [x: string]: JsonValue } +} + +export type PermissionV2Rule = { action: string; resource: string; effect: PermissionV2Effect } + +export type SessionAgentSelected = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.agent.selected" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; agent: string } +} + +export type SessionModelSelected = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.model.selected" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; model: ModelRef } +} + +export type SessionMoved = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.moved" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; location: LocationRef; projectID?: string; subpath?: string } +} + +export type SessionRenamed = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.renamed" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; title: string } +} + +export type SessionDeleted = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.deleted" + durable: { aggregateID: string; seq: number; version: 2 } + location?: LocationRef + data: { sessionID: string } +} + +export type SessionForked = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.forked" + durable: { aggregateID: string; seq: number; version: 2 } + location?: LocationRef + data: { sessionID: string; parentID: string; parentSeq: number; from?: string } +} + +export type SessionInputPromoted = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.input.promoted" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; inputID: string } +} + +export type SessionExecutionStarted = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.execution.started" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string } +} + +export type SessionExecutionSucceeded = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.execution.succeeded" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string } +} + +export type SessionExecutionInterrupted = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.execution.interrupted" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; reason: "user" | "shutdown" | "superseded" } +} + +export type SessionInstructionsUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.instructions.updated" + durable: { aggregateID: string; seq: number; version: 2 } + location?: LocationRef + data: { sessionID: string; delta: { [x: string]: string | "removed" } } +} + +export type SessionSynthetic = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.synthetic" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; text: string; description?: string; metadata?: { [x: string]: any } } +} + +export type SessionSkillActivated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.skill.activated" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; id: string; name: string; text: string } +} + +export type SessionStepStarted = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.step.started" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; assistantMessageID: string; agent: string; model: ModelRef; snapshot?: string } +} + +export type SessionStepEnded = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.step.ended" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { + sessionID: string + assistantMessageID: string + finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" + cost: MoneyUSD + tokens: TokenUsageInfo + snapshot?: string + files?: Array + } +} + +export type SessionTextStarted = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.text.started" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; assistantMessageID: string; ordinal: number } +} + +export type SessionTextEnded = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.text.ended" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; assistantMessageID: string; ordinal: number; text: string } +} + +export type SessionToolInputEnded = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.tool.input.ended" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; assistantMessageID: string; callID: string; text: string } +} + +export type SessionCompactionAdmitted = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.compaction.admitted" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; inputID: string } +} + +export type SessionCompactionStarted = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.compaction.started" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; reason: "auto" | "manual"; recent: string; inputID?: string } +} + +export type SessionCompactionEnded = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.compaction.ended" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; reason: "auto" | "manual"; text: string; recent: string } +} + +export type SessionRevertCleared = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.revert.cleared" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string } +} + +export type SessionRevertCommitted = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.revert.committed" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; to: string } +} + +export type SessionUsageRecorded = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.usage.recorded" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; source: "title" | "compaction"; cost: MoneyUSD; tokens: TokenUsageInfo } +} + +export type ModelsDevRefreshed = { + id: string + created: number + metadata?: { [x: string]: any } + type: "models-dev.refreshed" + location?: LocationRef + data: {} +} + +export type IntegrationUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "integration.updated" + location?: LocationRef + data: {} +} + +export type IntegrationConnectionUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "integration.connection.updated" + location?: LocationRef + data: { integrationID: string } +} + +export type CatalogUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "catalog.updated" + location?: LocationRef + data: {} +} + +export type AgentUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "agent.updated" + location?: LocationRef + data: {} +} + +export type MessageRemoved = { + id: string + created: number + metadata?: { [x: string]: any } + type: "message.removed" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; messageID: string } +} + +export type MessagePartRemoved = { + id: string + created: number + metadata?: { [x: string]: any } + type: "message.part.removed" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; messageID: string; partID: string } +} + +export type SessionUsageUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.usage.updated" + location?: LocationRef + data: { sessionID: string; cost: MoneyUSD; tokens: TokenUsageInfo } +} + +export type SessionTextDelta = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.text.delta" + location?: LocationRef + data: { sessionID: string; assistantMessageID: string; ordinal: number; delta: string } +} + +export type SessionReasoningDelta = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.reasoning.delta" + location?: LocationRef + data: { sessionID: string; assistantMessageID: string; ordinal: number; delta: string } +} + +export type SessionToolInputDelta = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.tool.input.delta" + location?: LocationRef + data: { sessionID: string; assistantMessageID: string; callID: string; delta: string } +} + +export type SessionCompactionDelta = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.compaction.delta" + location?: LocationRef + data: { sessionID: string; text: string } +} + +export type FilesystemChanged = { + id: string + created: number + metadata?: { [x: string]: any } + type: "filesystem.changed" + location?: LocationRef + data: { file: string; event: "add" | "change" | "unlink" } +} + +export type ReferenceUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "reference.updated" + location?: LocationRef + data: {} +} + +export type PluginAdded = { + id: string + created: number + metadata?: { [x: string]: any } + type: "plugin.added" + location?: LocationRef + data: { id: string } +} + +export type PluginUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "plugin.updated" + location?: LocationRef + data: {} +} + +export type ProjectDirectoriesUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "project.directories.updated" + location?: LocationRef + data: { projectID: string } +} + +export type CommandUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "command.updated" + location?: LocationRef + data: {} +} + +export type ConfigUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "config.updated" + location?: LocationRef + data: {} +} + +export type SkillUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "skill.updated" + location?: LocationRef + data: {} +} + +export type PtyExited = { + id: string + created: number + metadata?: { [x: string]: any } + type: "pty.exited" + location?: LocationRef + data: { id: string; exitCode: number } +} + +export type PtyDeleted = { + id: string + created: number + metadata?: { [x: string]: any } + type: "pty.deleted" + location?: LocationRef + data: { id: string } +} + +export type ShellExited = { + id: string + created: number + metadata?: { [x: string]: any } + type: "shell.exited" + location?: LocationRef + data: { id: string; exit?: number; status: "running" | "exited" | "timeout" | "killed" } +} + +export type ShellDeleted = { + id: string + created: number + metadata?: { [x: string]: any } + type: "shell.deleted" + location?: LocationRef + data: { id: string } +} + +export type QuestionV2Rejected = { + id: string + created: number + metadata?: { [x: string]: any } + type: "question.v2.rejected" + location?: LocationRef + data: { sessionID: string; requestID: string } +} + +export type FormCancelled = { + id: string + created: number + metadata?: { [x: string]: any } + type: "form.cancelled" + location?: LocationRef + data: { id: string; sessionID: string } +} + +export type SessionIdle = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.idle" + location?: LocationRef + data: { sessionID: string } +} + +export type TuiPromptAppend = { + id: string + created: number + metadata?: { [x: string]: any } + type: "tui.prompt.append" + location?: LocationRef + data: { text: string } +} + +export type TuiCommandExecute = { + id: string + created: number + metadata?: { [x: string]: any } + type: "tui.command.execute" + location?: LocationRef + data: { + command: + | "session.list" + | "session.new" + | "session.share" + | "session.interrupt" + | "session.background" + | "session.compact" + | "session.page.up" + | "session.page.down" + | "session.line.up" + | "session.line.down" + | "session.half.page.up" + | "session.half.page.down" + | "session.first" + | "session.last" + | "prompt.clear" + | "prompt.submit" + | "agent.cycle" + | string + } +} + +export type TuiToastShow = { + id: string + created: number + metadata?: { [x: string]: any } + type: "tui.toast.show" + location?: LocationRef + data: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + duration?: number | undefined + } +} + +export type TuiSessionSelect = { + id: string + created: number + metadata?: { [x: string]: any } + type: "tui.session.select" + location?: LocationRef + data: { sessionID: string } +} + +export type InstallationUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "installation.updated" + location?: LocationRef + data: { version: string } +} + +export type InstallationUpdateAvailable = { + id: string + created: number + metadata?: { [x: string]: any } + type: "installation.update-available" + location?: LocationRef + data: { version: string } +} + +export type VcsBranchUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "vcs.branch.updated" + location?: LocationRef + data: { branch?: string } +} + +export type McpStatusChanged = { + id: string + created: number + metadata?: { [x: string]: any } + type: "mcp.status.changed" + location?: LocationRef + data: { server: string } +} + +export type McpResourcesChanged = { + id: string + created: number + metadata?: { [x: string]: any } + type: "mcp.resources.changed" + location?: LocationRef + data: { server: string } +} + +export type PermissionAsked = { + id: string + created: number + metadata?: { [x: string]: any } + type: "permission.asked" + location?: LocationRef + data: { + id: string + sessionID: string + permission: string + patterns: Array + metadata: { [x: string]: any } + always: Array + tool?: { messageID: string; callID: string } | undefined + } +} + +export type PermissionReplied = { + id: string + created: number + metadata?: { [x: string]: any } + type: "permission.replied" + location?: LocationRef + data: { sessionID: string; requestID: string; reply: "once" | "always" | "reject" } +} + +export type QuestionRejected = { + id: string + created: number + metadata?: { [x: string]: any } + type: "question.rejected" + location?: LocationRef + data: { sessionID: string; requestID: string } +} + +export type V2EventServerConnected = { + id: string + metadata?: { [x: string]: any } | undefined + location?: LocationRef | undefined + type: "server.connected" + data: {} +} + +export type SessionRevert = { messageID: string; partID?: string; snapshot?: string; files?: Array } + +export type PromptFileAttachment = { + data: PromptBase64 + mime: string + source: PromptFileSource + name?: string + description?: string + mention?: PromptMention +} + +export type PromptAgentAttachment = { name: string; mention?: PromptMention } + +export type SessionPendingSynthetic = { + admittedSeq: number + id: string + sessionID: string + timeCreated: number + type: "synthetic" + data: SessionPendingSyntheticData + delivery: "steer" | "queue" +} + +export type SessionMessageAssistantReasoning = { + type: "reasoning" + text: string + state?: SessionMessageProviderState + time?: { created: number; completed?: number } +} + +export type LLMToolContent = ToolTextContent | ToolFileContent + +export type SessionMessageAssistantRetry = { attempt: number; at: number; error: SessionStructuredError } + +export type SessionMessageCompactionFailed = { + type: "compaction" + id: string + metadata?: { [x: string]: JsonValue } + time: { created: number } + status: "failed" + reason: "auto" | "manual" + error: SessionStructuredError +} + +export type SessionExecutionFailed = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.execution.failed" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; error: SessionStructuredError } +} + +export type SessionStepFailed = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.step.failed" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { + sessionID: string + assistantMessageID: string + error: SessionStructuredError + cost?: MoneyUSD + tokens?: TokenUsageInfo + } +} + +export type SessionRetryScheduled = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.retry.scheduled" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; assistantMessageID: string; attempt: number; at: number; error: SessionStructuredError } +} + +export type SessionCompactionFailed = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.compaction.failed" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; reason: "auto" | "manual"; error: SessionStructuredError; inputID?: string } +} + +export type InstructionEntryInfo = { key: InstructionEntryKey; value: JsonValue } + +export type SessionPendingSyntheticMessage = { + type: "synthetic" + data: SessionPendingSyntheticData1 + delivery: "steer" | "queue" +} + +export type SessionShellStarted = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.shell.started" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; shell: ShellInfo } +} + +export type SessionShellEnded = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.shell.ended" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { + sessionID: string + shell: ShellInfo + output: { output: string; cursor: number; size: number; truncated: boolean } + } +} + +export type ShellCreated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "shell.created" + location?: LocationRef + data: { info: ShellInfo } +} + +export type SessionReasoningStarted = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.reasoning.started" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; assistantMessageID: string; ordinal: number; state?: SessionMessageProviderState3 } +} + +export type SessionReasoningEnded = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.reasoning.ended" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { + sessionID: string + assistantMessageID: string + ordinal: number + text: string + state?: SessionMessageProviderState4 + } +} + +export type SessionToolInputStarted = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.tool.input.started" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { + sessionID: string + assistantMessageID: string + callID: string + name: string + executed?: boolean + state?: SessionMessageProviderState5 + } +} + +export type SessionToolCalled = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.tool.called" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { + sessionID: string + assistantMessageID: string + callID: string + input: { [x: string]: any } + executed: boolean + state?: SessionMessageProviderState6 + } +} + +export type SessionToolFailed = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.tool.failed" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { + sessionID: string + assistantMessageID: string + callID: string + error: SessionStructuredError + raw?: string + result?: any + executed: boolean + resultState?: SessionMessageProviderState8 + } +} + +export type ModelCost = { + tier?: { type: "context"; size: number } + input: MoneyUSDPerMillionTokens + output: MoneyUSDPerMillionTokens + cache: { read: MoneyUSDPerMillionTokens; write: MoneyUSDPerMillionTokens } +} + +export type IntegrationTextPrompt = { + type: "text" + key: string + message: string + placeholder?: string + when?: IntegrationWhen +} + +export type IntegrationSelectPrompt = { + type: "select" + key: string + message: string + options: Array<{ label: string; value: string; hint?: string }> + when?: IntegrationWhen +} + +export type ConnectionInfo = ConnectionCredentialInfo | ConnectionEnvInfo + +export type McpServer = { + name: string + status: + | McpStatusConnected + | McpStatusPending + | McpStatusDisabled + | McpStatusFailed + | McpStatusNeedsAuth + | McpStatusNeedsClientRegistration + integrationID?: string +} + +export type McpResourceCatalog = { resources: Array; templates: Array } + +export type Project = { + id: string + worktree: string + vcs?: ProjectVcs + name?: string + icon?: ProjectIcon + commands?: ProjectCommands + time: ProjectTime + sandboxes: Array +} + +export type ProjectDirectories = Array + +export type FormNumberField = { + key: string + title?: string + description?: string + required?: boolean + when?: Array + type: "number" + minimum?: number | "Infinity" | "-Infinity" | "NaN" + maximum?: number | "Infinity" | "-Infinity" | "NaN" + default?: number | "Infinity" | "-Infinity" | "NaN" +} + +export type FormIntegerField = { + key: string + title?: string + description?: string + required?: boolean + when?: Array + type: "integer" + minimum?: number | "Infinity" | "-Infinity" | "NaN" + maximum?: number | "Infinity" | "-Infinity" | "NaN" + default?: number | "Infinity" | "-Infinity" | "NaN" +} + +export type FormBooleanField = { + key: string + title?: string + description?: string + required?: boolean + when?: Array + type: "boolean" + default?: boolean +} + +export type FormStringField = { + key: string + title?: string + description?: string + required?: boolean + when?: Array + type: "string" + format?: "email" | "uri" | "date" | "date-time" + minLength?: number + maxLength?: number + pattern?: string + placeholder?: string + default?: string + options?: Array + custom?: boolean +} + +export type FormMultiselectField = { + key: string + title?: string + description?: string + required?: boolean + when?: Array + type: "multiselect" + options: Array + minItems?: number + maxItems?: number + custom?: boolean + default?: Array +} + +export type FormAnswer = { [x: string]: FormValue } + +export type PermissionV2Request = { + id: string + sessionID: string + action: string + resources: Array + save?: Array + metadata?: { [x: string]: JsonValue } + source?: PermissionV2Source +} + +export type PermissionV2Asked = { + id: string + created: number + metadata?: { [x: string]: any } + type: "permission.v2.asked" + location?: LocationRef + data: { + id: string + sessionID: string + action: string + resources: Array + save?: Array + metadata?: { [x: string]: any } + source?: PermissionV2Source + } +} + +export type PermissionRule = { permission: string; pattern: string; action: PermissionAction } + +export type OutputFormat = + | { type: "text" } + | { type: "json_schema"; schema: JSONSchema; retryCount?: number | undefined | undefined } + +export type AssistantMessage = { + id: string + sessionID: string + role: "assistant" + time: { created: number; completed?: number | undefined } + error?: + | ProviderAuthError + | UnknownError2 + | MessageOutputLengthError + | MessageAbortedError + | StructuredOutputError + | ContextOverflowError + | ContentFilterError + | APIError + | undefined + parentID: string + modelID: string + providerID: string + mode: string + agent: string + path: { cwd: string; root: string } + summary?: boolean | undefined + cost: number + tokens: { + total?: number | undefined + input: number + output: number + reasoning: number + cache: { read: number; write: number } + } + structured?: any | undefined + variant?: string | undefined + finish?: string | undefined +} + +export type RetryPart = { + id: string + sessionID: string + messageID: string + type: "retry" + attempt: number + error: APIError + time: { created: number } +} + +export type SessionError = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.error" + location?: LocationRef + data: { + sessionID?: string | undefined + error?: + | ProviderAuthError + | UnknownError2 + | MessageOutputLengthError + | MessageAbortedError + | StructuredOutputError + | ContextOverflowError + | ContentFilterError + | APIError + | undefined + } +} + +export type FileSource = { text: FilePartSourceText; type: "file"; path: string } + +export type ResourceSource = { text: FilePartSourceText; type: "resource"; clientName: string; uri: string } + +export type SymbolSource = { + text: FilePartSourceText + type: "symbol" + path: string + range: Range + name: string + kind: number +} + +export type PermissionV2Replied = { + id: string + created: number + metadata?: { [x: string]: any } + type: "permission.v2.replied" + location?: LocationRef + data: { sessionID: string; requestID: string; reply: PermissionV2Reply } +} + +export type PtyCreated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "pty.created" + location?: LocationRef + data: { info: Pty } +} + +export type PtyUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "pty.updated" + location?: LocationRef + data: { info: Pty } +} + +export type QuestionV2Info = { + question: string + header: string + options: Array + multiple?: boolean + custom?: boolean +} + +export type QuestionV2Replied = { + id: string + created: number + metadata?: { [x: string]: any } + type: "question.v2.replied" + location?: LocationRef + data: { sessionID: string; requestID: string; answers: Array } +} + +export type FormStringField1 = { + key: string + title?: string + description?: string + required?: boolean + when?: Array + type: "string" + format?: "email" | "uri" | "date" | "date-time" + minLength?: number + maxLength?: number + pattern?: string + placeholder?: string + default?: string + options?: Array + custom?: boolean +} + +export type FormNumberField1 = { + key: string + title?: string + description?: string + required?: boolean + when?: Array + type: "number" + minimum?: number + maximum?: number + default?: number +} + +export type FormIntegerField1 = { + key: string + title?: string + description?: string + required?: boolean + when?: Array + type: "integer" + minimum?: number + maximum?: number + default?: number +} + +export type FormBooleanField1 = { + key: string + title?: string + description?: string + required?: boolean + when?: Array + type: "boolean" + default?: boolean +} + +export type FormMultiselectField1 = { + key: string + title?: string + description?: string + required?: boolean + when?: Array + type: "multiselect" + options: Array + minItems?: number + maxItems?: number + custom?: boolean + default?: Array +} + +export type SessionStatus2 = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.status" + location?: LocationRef + data: { sessionID: string; status: SessionStatus } +} + +export type QuestionInfo = { + question: string + header: string + options: Array + multiple?: boolean | undefined + custom?: boolean | undefined +} + +export type QuestionReplied = { + id: string + created: number + metadata?: { [x: string]: any } + type: "question.replied" + location?: LocationRef + data: { sessionID: string; requestID: string; answers: Array } +} + +export type ReferenceSource = ReferenceLocalSource | ReferenceGitSource + +export type PermissionV2Ruleset = Array + +export type SessionInfo = { + id: string + parentID?: string + fork?: { sessionID: string; messageID?: string } + projectID: string + agent?: string + model?: ModelRef + cost: MoneyUSD + tokens: TokenUsageInfo + time: { created: number; updated: number; archived?: number } + title: string + location: LocationRef + subpath?: string + revert?: SessionRevert +} + +export type SessionRevertStaged = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.revert.staged" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; revert: SessionRevert } +} + +export type SessionPendingUserData = { + text: string + files?: Array + agents?: Array + metadata?: { [x: string]: JsonValue } +} + +export type SessionMessageUser = { + id: string + metadata?: { [x: string]: JsonValue } + time: { created: number } + text: string + files?: Array + agents?: Array + type: "user" +} + +export type SessionPendingUserData1 = { + text: string + files?: Array + agents?: Array + metadata?: { [x: string]: any } +} + +export type SessionMessageToolStateRunning = { + status: "running" + input: { [x: string]: JsonValue } + structured: { [x: string]: JsonValue } + content: Array +} + +export type SessionMessageToolStateCompleted = { + status: "completed" + input: { [x: string]: JsonValue } + content: Array + structured: { [x: string]: JsonValue } + result?: JsonValue +} + +export type SessionMessageToolStateError = { + status: "error" + input: { [x: string]: JsonValue } + raw?: string + content: Array + structured: { [x: string]: JsonValue } + error: SessionStructuredError + result?: JsonValue +} + +export type SessionToolProgress = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.tool.progress" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { + sessionID: string + assistantMessageID: string + callID: string + structured: { [x: string]: any } + content: Array + } +} + +export type SessionToolSuccess = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.tool.success" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { + sessionID: string + assistantMessageID: string + callID: string + structured: { [x: string]: any } + content: Array + result?: any + executed: boolean + resultState?: SessionMessageProviderState7 + } +} + +export type SessionMessageCompaction = + | SessionMessageCompactionRunning + | SessionMessageCompactionCompleted + | SessionMessageCompactionFailed + +export type ModelInfo = { + id: string + modelID: string + providerID: string + family?: string + name: string + package?: string + settings?: { [x: string]: JsonValue } + headers?: { [x: string]: string } + body?: { [x: string]: JsonValue } + capabilities: ModelCapabilities + variants: Array + time: { released: number } + cost: Array + status: "alpha" | "beta" | "deprecated" | "active" + enabled: boolean + limit: { context: number; input?: number; output: number } +} + +export type IntegrationOAuthMethod = { + id: string + type: "oauth" + label: string + prompts?: Array +} + +export type FormField = + | FormStringField + | FormNumberField + | FormIntegerField + | FormBooleanField + | FormMultiselectField + | FormExternalField + +export type FormState = { status: "pending" } | { status: "answered"; answer: FormAnswer } | { status: "cancelled" } + +export type FormReplied = { + id: string + created: number + metadata?: { [x: string]: any } + type: "form.replied" + location?: LocationRef + data: { id: string; sessionID: string; answer: FormAnswer } +} + +export type PermissionRuleset = Array + +export type UserMessage = { + id: string + sessionID: string + role: "user" + time: { created: number } + format?: OutputFormat | undefined + summary?: { title?: string | undefined; body?: string | undefined; diffs: Array } | undefined + agent: string + model: { providerID: string; modelID: string; variant?: string | undefined } + system?: string | undefined + tools?: { [x: string]: boolean } | undefined +} + +export type FilePartSource = FileSource | SymbolSource | ResourceSource + +export type QuestionV2Asked = { + id: string + created: number + metadata?: { [x: string]: any } + type: "question.v2.asked" + location?: LocationRef + data: { id: string; sessionID: string; questions: Array; tool?: QuestionV2Tool } +} + +export type QuestionV2Request = { + id: string + sessionID: string + questions: Array + tool?: QuestionV2Tool +} + +export type FormField1 = + | FormStringField1 + | FormNumberField1 + | FormIntegerField1 + | FormBooleanField1 + | FormMultiselectField1 + | FormExternalField + +export type QuestionAsked = { + id: string + created: number + metadata?: { [x: string]: any } + type: "question.asked" + location?: LocationRef + data: { id: string; sessionID: string; questions: Array; tool?: QuestionTool | undefined } +} + +export type ReferenceInfo = { + name: string + path: string + description?: string + hidden?: boolean + source: ReferenceSource +} + +export type AgentInfo = { + id: string + name: string + model?: ModelRef + request: ProviderRequest + system?: string + description?: string + mode: "subagent" | "primary" | "all" + hidden: boolean + color?: AgentColor + steps?: number + permissions: PermissionV2Ruleset +} + +export type SessionsResponse = { data: Array; cursor: { previous?: string | null; next?: string | null } } + +export type SessionPendingUser = { + admittedSeq: number + id: string + sessionID: string + timeCreated: number + type: "user" + data: SessionPendingUserData + delivery: "steer" | "queue" +} + +export type SessionPendingUserMessage = { type: "user"; data: SessionPendingUserData1; delivery: "steer" | "queue" } + +export type SessionMessageAssistantTool = { + type: "tool" + id: string + name: string + executed?: boolean + providerState?: SessionMessageProviderState + providerResultState?: SessionMessageProviderState + state: + | SessionMessageToolStateStreaming + | SessionMessageToolStateRunning + | SessionMessageToolStateCompleted + | SessionMessageToolStateError + time: { created: number; ran?: number; completed?: number } +} + +export type IntegrationMethod = + | IntegrationOAuthMethod + | IntegrationCommandMethod + | IntegrationKeyMethod + | IntegrationEnvMethod + +export type FormFields = [FormField, ...Array] + +export type SessionV1Info = { + id: string + slug: string + projectID: string + workspaceID?: string + directory: string + path?: string + parentID?: string + summary?: { additions: number; deletions: number; files: number; diffs?: Array } + cost?: number + tokens?: { input: number; output: number; reasoning: number; cache: { read: number; write: number } } + share?: { url: string } + title: string + agent?: string + model?: { id: string; providerID: string; variant?: string } + version: string + metadata?: { [x: string]: any } + time: { created: number; updated: number; compacting?: number; archived?: number } + permission?: PermissionRuleset + revert?: { messageID: string; partID?: string; snapshot?: string; diff?: string } +} + +export type Message = UserMessage | AssistantMessage + +export type FilePart = { + id: string + sessionID: string + messageID: string + type: "file" + mime: string + filename?: string | undefined + url: string + source?: FilePartSource | undefined +} + +export type FormFields1 = [FormField1, ...Array] + +export type SessionPendingInfo = SessionPendingUser | SessionPendingSynthetic | SessionPendingCompaction + +export type SessionPendingMessage = SessionPendingUserMessage | SessionPendingSyntheticMessage + +export type SessionMessageAssistant = { + id: string + metadata?: { [x: string]: JsonValue } + time: { created: number; completed?: number } + type: "assistant" + agent: string + model: ModelRef + content: Array + snapshot?: { start?: string; end?: string; files?: Array } + finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" + cost?: MoneyUSD + tokens?: TokenUsageInfo + error?: SessionStructuredError + retry?: SessionMessageAssistantRetry +} + +export type IntegrationInfo = { + id: string + name: string + methods: Array + connections: Array +} + +export type FormInfo = { id: string; sessionID: string; title: string; metadata?: FormMetadata; fields: FormFields } + +export type SessionCreated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.created" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; info: SessionV1Info } +} + +export type SessionUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.updated" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; info: SessionV1Info } +} + +export type SessionDeleted1 = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.deleted" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; info: SessionV1Info } +} + +export type MessageUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "message.updated" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; info: Message } +} + +export type ToolStateCompleted = { + status: "completed" + input: { [x: string]: any } + output: string + title: string + metadata: { [x: string]: any } + time: { start: number; end: number; compacted?: number | undefined } + attachments?: Array | undefined +} + +export type FormInfo1 = { id: string; sessionID: string; title: string; metadata?: FormMetadata1; fields: FormFields1 } + +export type SessionInputAdmitted = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.input.admitted" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; inputID: string; input: SessionPendingMessage } +} + +export type SessionMessageInfo = + | SessionMessageAgentSelected + | SessionMessageModelSelected + | SessionMessageUser + | SessionMessageSynthetic + | SessionMessageSystem + | SessionMessageSkill + | SessionMessageShell + | SessionMessageAssistant + | SessionMessageCompaction + +export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError + +export type FormCreated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "form.created" + location?: LocationRef + data: { form: FormInfo1 } +} + +export type SessionEventDurable = + | SessionAgentSelected + | SessionModelSelected + | SessionMoved + | SessionRenamed + | SessionDeleted + | SessionForked + | SessionInputPromoted + | SessionInputAdmitted + | SessionExecutionStarted + | SessionExecutionSucceeded + | SessionExecutionFailed + | SessionExecutionInterrupted + | SessionInstructionsUpdated + | SessionSynthetic + | SessionSkillActivated + | SessionShellStarted + | SessionShellEnded + | SessionStepStarted + | SessionStepEnded + | SessionStepFailed + | SessionTextStarted + | SessionTextEnded + | SessionReasoningStarted + | SessionReasoningEnded + | SessionToolInputStarted + | SessionToolInputEnded + | SessionToolCalled + | SessionToolProgress + | SessionToolSuccess + | SessionToolFailed + | SessionRetryScheduled + | SessionCompactionAdmitted + | SessionCompactionStarted + | SessionCompactionEnded + | SessionCompactionFailed + | SessionRevertStaged + | SessionRevertCleared + | SessionRevertCommitted + | SessionUsageRecorded + +export type SessionMessagesResponse = { + data: Array + cursor: { previous?: string | null; next?: string | null } +} + +export type ToolPart = { + id: string + sessionID: string + messageID: string + type: "tool" + callID: string + tool: string + state: ToolState + metadata?: { [x: string]: any } | undefined +} + +export type SessionLogItem = SessionEventDurable | EventLogSynced + +export type Part = + | TextPart + | SubtaskPart + | ReasoningPart + | FilePart + | ToolPart + | StepStartPart + | StepFinishPart + | SnapshotPart + | PatchPart + | AgentPart + | RetryPart + | CompactionPart + +export type MessagePartUpdated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "message.part.updated" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; part: Part; time: number } +} + +export type V2Event = + | ModelsDevRefreshed + | IntegrationUpdated + | IntegrationConnectionUpdated + | CatalogUpdated + | AgentUpdated + | SessionCreated + | SessionUpdated + | SessionDeleted1 + | MessageUpdated + | MessageRemoved + | MessagePartUpdated + | MessagePartRemoved + | SessionAgentSelected + | SessionModelSelected + | SessionMoved + | SessionRenamed + | SessionUsageUpdated + | SessionDeleted + | SessionForked + | SessionInputPromoted + | SessionInputAdmitted + | SessionExecutionStarted + | SessionExecutionSucceeded + | SessionExecutionFailed + | SessionExecutionInterrupted + | SessionInstructionsUpdated + | SessionSynthetic + | SessionSkillActivated + | SessionShellStarted + | SessionShellEnded + | SessionStepStarted + | SessionStepEnded + | SessionStepFailed + | SessionTextStarted + | SessionTextDelta + | SessionTextEnded + | SessionReasoningStarted + | SessionReasoningDelta + | SessionReasoningEnded + | SessionToolInputStarted + | SessionToolInputDelta + | SessionToolInputEnded + | SessionToolCalled + | SessionToolProgress + | SessionToolSuccess + | SessionToolFailed + | SessionRetryScheduled + | SessionCompactionAdmitted + | SessionCompactionStarted + | SessionCompactionDelta + | SessionCompactionEnded + | SessionCompactionFailed + | SessionRevertStaged + | SessionRevertCleared + | SessionRevertCommitted + | FilesystemChanged + | ReferenceUpdated + | PermissionV2Asked + | PermissionV2Replied + | PluginAdded + | PluginUpdated + | ProjectDirectoriesUpdated + | CommandUpdated + | ConfigUpdated + | SkillUpdated + | PtyCreated + | PtyUpdated + | PtyExited + | PtyDeleted + | ShellCreated + | ShellExited + | ShellDeleted + | QuestionV2Asked + | QuestionV2Replied + | QuestionV2Rejected + | FormCreated + | FormReplied + | FormCancelled + | SessionStatus2 + | SessionIdle + | TuiPromptAppend + | TuiCommandExecute + | TuiToastShow + | TuiSessionSelect + | InstallationUpdated + | InstallationUpdateAvailable + | VcsBranchUpdated + | McpStatusChanged + | McpResourcesChanged + | PermissionAsked + | PermissionReplied + | QuestionAsked + | QuestionReplied + | QuestionRejected + | SessionError + | V2EventServerConnected + +export type UnauthorizedError = { readonly _tag: "UnauthorizedError"; readonly message: string } +export const isUnauthorizedError = (value: unknown): value is UnauthorizedError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnauthorizedError" + +export type InvalidRequestError = { + readonly _tag: "InvalidRequestError" + readonly message: string + readonly kind?: string | undefined + readonly field?: string | undefined +} +export const isInvalidRequestError = (value: unknown): value is InvalidRequestError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidRequestError" + +export type InvalidCursorError = { readonly _tag: "InvalidCursorError"; readonly message: string } +export const isInvalidCursorError = (value: unknown): value is InvalidCursorError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidCursorError" + +export type SessionNotFoundError = { + readonly _tag: "SessionNotFoundError" + readonly sessionID: string + readonly message: string +} +export const isSessionNotFoundError = (value: unknown): value is SessionNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SessionNotFoundError" + +export type MessageNotFoundError = { + readonly _tag: "MessageNotFoundError" + readonly sessionID: string + readonly messageID: string + readonly message: string +} +export const isMessageNotFoundError = (value: unknown): value is MessageNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "MessageNotFoundError" + +export type ConflictError = { + readonly _tag: "ConflictError" + readonly message: string + readonly resource?: string | undefined +} +export const isConflictError = (value: unknown): value is ConflictError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ConflictError" + +export type CommandNotFoundError = { + readonly _tag: "CommandNotFoundError" + readonly command: string + readonly message: string +} +export const isCommandNotFoundError = (value: unknown): value is CommandNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "CommandNotFoundError" + +export type CommandEvaluationError = { + readonly _tag: "CommandEvaluationError" + readonly command: string + readonly message: string +} +export const isCommandEvaluationError = (value: unknown): value is CommandEvaluationError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "CommandEvaluationError" + +export type SkillNotFoundError = { + readonly _tag: "SkillNotFoundError" + readonly skill: string + readonly message: string +} +export const isSkillNotFoundError = (value: unknown): value is SkillNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SkillNotFoundError" + +export type ServiceUnavailableError = { + readonly _tag: "ServiceUnavailableError" + readonly message: string + readonly service?: string | undefined +} +export const isServiceUnavailableError = (value: unknown): value is ServiceUnavailableError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ServiceUnavailableError" + +export type SessionBusyError = { + readonly _tag: "SessionBusyError" + readonly sessionID: string + readonly message: string +} +export const isSessionBusyError = (value: unknown): value is SessionBusyError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SessionBusyError" + +export type UnknownError = { + readonly _tag: "UnknownError" + readonly message: string + readonly ref?: string | undefined +} +export const isUnknownError = (value: unknown): value is UnknownError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnknownError" + +export type InstructionEntryValueTooLargeError = { + readonly _tag: "InstructionEntryValueTooLargeError" + readonly actualBytes: number + readonly maxBytes: number + readonly message: string +} +export const isInstructionEntryValueTooLargeError = (value: unknown): value is InstructionEntryValueTooLargeError => + typeof value === "object" && + value !== null && + "_tag" in value && + value["_tag"] === "InstructionEntryValueTooLargeError" + +export type ProviderNotFoundError = { + readonly _tag: "ProviderNotFoundError" + readonly providerID: string + readonly message: string +} +export const isProviderNotFoundError = (value: unknown): value is ProviderNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ProviderNotFoundError" + +export type FormNotFoundError = { readonly _tag: "FormNotFoundError"; readonly id: string; readonly message: string } +export const isFormNotFoundError = (value: unknown): value is FormNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormNotFoundError" + +export type FormAlreadySettledError = { + readonly _tag: "FormAlreadySettledError" + readonly id: string + readonly message: string +} +export const isFormAlreadySettledError = (value: unknown): value is FormAlreadySettledError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormAlreadySettledError" + +export type FormInvalidAnswerError = { + readonly _tag: "FormInvalidAnswerError" + readonly id: string + readonly message: string +} +export const isFormInvalidAnswerError = (value: unknown): value is FormInvalidAnswerError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormInvalidAnswerError" + +export type PermissionNotFoundError = { + readonly _tag: "PermissionNotFoundError" + readonly requestID: string + readonly message: string +} +export const isPermissionNotFoundError = (value: unknown): value is PermissionNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "PermissionNotFoundError" + +export type PtyNotFoundError = { readonly _tag: "PtyNotFoundError"; readonly ptyID: string; readonly message: string } +export const isPtyNotFoundError = (value: unknown): value is PtyNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "PtyNotFoundError" + +export type ShellNotFoundError = { readonly _tag: "ShellNotFoundError"; readonly id: string; readonly message: string } +export const isShellNotFoundError = (value: unknown): value is ShellNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ShellNotFoundError" + +export type QuestionNotFoundError = { + readonly _tag: "QuestionNotFoundError" + readonly requestID: string + readonly message: string +} +export const isQuestionNotFoundError = (value: unknown): value is QuestionNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "QuestionNotFoundError" + +export type ProjectCopyError = { + readonly name: "ProjectCopyError" + readonly data: { readonly message: string; readonly forceRequired?: boolean | undefined } +} +export const isProjectCopyError = (value: unknown): value is ProjectCopyError => + typeof value === "object" && value !== null && "name" in value && value["name"] === "ProjectCopyError" + +export type HealthGetOutput = ServiceHealth + +export type HealthStopInput = { readonly instanceID: { readonly instanceID: string }["instanceID"] } + +export type HealthStopOutput = ServiceStopResponse + +export type ServerGetOutput = { urls: Array } + +export type LocationGetInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type LocationGetOutput = { directory: string; workspaceID?: string; project: { id: string; directory: string } } + +export type AgentListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type AgentListOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type PluginListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type PluginListOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type SessionListInput = { + readonly workspace?: { + readonly workspace?: string | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly parentID?: string | null | undefined + readonly directory?: string | undefined + readonly project?: string | undefined + readonly subpath?: string | undefined + readonly cursor?: string | undefined + }["workspace"] + readonly limit?: { + readonly workspace?: string | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly parentID?: string | null | undefined + readonly directory?: string | undefined + readonly project?: string | undefined + readonly subpath?: string | undefined + readonly cursor?: string | undefined + }["limit"] + readonly order?: { + readonly workspace?: string | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly parentID?: string | null | undefined + readonly directory?: string | undefined + readonly project?: string | undefined + readonly subpath?: string | undefined + readonly cursor?: string | undefined + }["order"] + readonly search?: { + readonly workspace?: string | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly parentID?: string | null | undefined + readonly directory?: string | undefined + readonly project?: string | undefined + readonly subpath?: string | undefined + readonly cursor?: string | undefined + }["search"] + readonly parentID?: { + readonly workspace?: string | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly parentID?: string | null | undefined + readonly directory?: string | undefined + readonly project?: string | undefined + readonly subpath?: string | undefined + readonly cursor?: string | undefined + }["parentID"] + readonly directory?: { + readonly workspace?: string | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly parentID?: string | null | undefined + readonly directory?: string | undefined + readonly project?: string | undefined + readonly subpath?: string | undefined + readonly cursor?: string | undefined + }["directory"] + readonly project?: { + readonly workspace?: string | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly parentID?: string | null | undefined + readonly directory?: string | undefined + readonly project?: string | undefined + readonly subpath?: string | undefined + readonly cursor?: string | undefined + }["project"] + readonly subpath?: { + readonly workspace?: string | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly parentID?: string | null | undefined + readonly directory?: string | undefined + readonly project?: string | undefined + readonly subpath?: string | undefined + readonly cursor?: string | undefined + }["subpath"] + readonly cursor?: { + readonly workspace?: string | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly parentID?: string | null | undefined + readonly directory?: string | undefined + readonly project?: string | undefined + readonly subpath?: string | undefined + readonly cursor?: string | undefined + }["cursor"] +} + +export type SessionListOutput = SessionsResponse + +export type SessionCreateInput = { + readonly id?: { + readonly id?: string | null + readonly agent?: string | null + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + readonly location?: { readonly directory: string; readonly workspaceID?: string } | null + }["id"] + readonly agent?: { + readonly id?: string | null + readonly agent?: string | null + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + readonly location?: { readonly directory: string; readonly workspaceID?: string } | null + }["agent"] + readonly model?: { + readonly id?: string | null + readonly agent?: string | null + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + readonly location?: { readonly directory: string; readonly workspaceID?: string } | null + }["model"] + readonly location?: { + readonly id?: string | null + readonly agent?: string | null + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + readonly location?: { readonly directory: string; readonly workspaceID?: string } | null + }["location"] +} + +export type SessionCreateOutput = { data: SessionInfo }["data"] + +export type SessionActiveOutput = { data: { [x: string]: SessionActive } }["data"] + +export type SessionGetInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type SessionGetOutput = { data: SessionInfo }["data"] + +export type SessionRemoveInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type SessionRemoveOutput = void + +export type SessionForkInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly messageID?: { readonly messageID?: string | undefined }["messageID"] +} + +export type SessionForkOutput = { data: SessionInfo }["data"] + +export type SessionSwitchAgentInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly agent: { readonly agent: string }["agent"] +} + +export type SessionSwitchAgentOutput = void + +export type SessionSwitchModelInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly model: { + readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } + }["model"] +} + +export type SessionSwitchModelOutput = void + +export type SessionRenameInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly title: { readonly title: string }["title"] +} + +export type SessionRenameOutput = void + +export type SessionMoveInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly directory: { readonly directory: string; readonly workspaceID?: string }["directory"] + readonly workspaceID?: { readonly directory: string; readonly workspaceID?: string }["workspaceID"] +} + +export type SessionMoveOutput = void + +export type SessionPromptInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly id?: { + readonly id?: string | null + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly metadata?: { readonly [x: string]: JsonValue } + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["id"] + readonly text: { + readonly id?: string | null + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly metadata?: { readonly [x: string]: JsonValue } + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["text"] + readonly files?: { + readonly id?: string | null + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly metadata?: { readonly [x: string]: JsonValue } + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["files"] + readonly agents?: { + readonly id?: string | null + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly metadata?: { readonly [x: string]: JsonValue } + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["agents"] + readonly metadata?: { + readonly id?: string | null + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly metadata?: { readonly [x: string]: JsonValue } + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["metadata"] + readonly delivery?: { + readonly id?: string | null + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly metadata?: { readonly [x: string]: JsonValue } + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["delivery"] + readonly resume?: { + readonly id?: string | null + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly metadata?: { readonly [x: string]: JsonValue } + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["resume"] +} + +export type SessionPromptOutput = { data: SessionPendingUser }["data"] + +export type SessionCommandInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly id?: { + readonly id?: string | null + readonly command: string + readonly arguments?: string | null + readonly agent?: string | null + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["id"] + readonly command: { + readonly id?: string | null + readonly command: string + readonly arguments?: string | null + readonly agent?: string | null + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["command"] + readonly arguments?: { + readonly id?: string | null + readonly command: string + readonly arguments?: string | null + readonly agent?: string | null + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["arguments"] + readonly agent?: { + readonly id?: string | null + readonly command: string + readonly arguments?: string | null + readonly agent?: string | null + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["agent"] + readonly model?: { + readonly id?: string | null + readonly command: string + readonly arguments?: string | null + readonly agent?: string | null + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["model"] + readonly files?: { + readonly id?: string | null + readonly command: string + readonly arguments?: string | null + readonly agent?: string | null + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["files"] + readonly agents?: { + readonly id?: string | null + readonly command: string + readonly arguments?: string | null + readonly agent?: string | null + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["agents"] + readonly delivery?: { + readonly id?: string | null + readonly command: string + readonly arguments?: string | null + readonly agent?: string | null + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["delivery"] + readonly resume?: { + readonly id?: string | null + readonly command: string + readonly arguments?: string | null + readonly agent?: string | null + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly mention?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["resume"] +} + +export type SessionCommandOutput = { data: SessionPendingUser }["data"] + +export type SessionSkillInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly id?: { + readonly id?: string | undefined + readonly skill: string + readonly resume?: boolean | undefined + }["id"] + readonly skill: { + readonly id?: string | undefined + readonly skill: string + readonly resume?: boolean | undefined + }["skill"] + readonly resume?: { + readonly id?: string | undefined + readonly skill: string + readonly resume?: boolean | undefined + }["resume"] +} + +export type SessionSkillOutput = void + +export type SessionSyntheticInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly id?: { + readonly id?: string | null + readonly text: string + readonly description?: string | null + readonly metadata?: { readonly [x: string]: JsonValue } + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["id"] + readonly text: { + readonly id?: string | null + readonly text: string + readonly description?: string | null + readonly metadata?: { readonly [x: string]: JsonValue } + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["text"] + readonly description?: { + readonly id?: string | null + readonly text: string + readonly description?: string | null + readonly metadata?: { readonly [x: string]: JsonValue } + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["description"] + readonly metadata?: { + readonly id?: string | null + readonly text: string + readonly description?: string | null + readonly metadata?: { readonly [x: string]: JsonValue } + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["metadata"] + readonly delivery?: { + readonly id?: string | null + readonly text: string + readonly description?: string | null + readonly metadata?: { readonly [x: string]: JsonValue } + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["delivery"] + readonly resume?: { + readonly id?: string | null + readonly text: string + readonly description?: string | null + readonly metadata?: { readonly [x: string]: JsonValue } + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["resume"] +} + +export type SessionSyntheticOutput = { data: SessionPendingSynthetic }["data"] + +export type SessionShellInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly id?: { readonly id?: string | undefined; readonly command: string }["id"] + readonly command: { readonly id?: string | undefined; readonly command: string }["command"] +} + +export type SessionShellOutput = void + +export type SessionCompactInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly id?: { readonly id?: string | undefined }["id"] +} + +export type SessionCompactOutput = { data: SessionPendingCompaction }["data"] + +export type SessionWaitInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type SessionWaitOutput = void + +export type SessionRevertStageInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly messageID: { readonly messageID: string; readonly files?: boolean | undefined }["messageID"] + readonly files?: { readonly messageID: string; readonly files?: boolean | undefined }["files"] +} + +export type SessionRevertStageOutput = { data: SessionRevert }["data"] + +export type SessionRevertClearInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type SessionRevertClearOutput = void + +export type SessionRevertCommitInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type SessionRevertCommitOutput = void + +export type SessionContextInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type SessionContextOutput = { data: Array }["data"] + +export type SessionPendingListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type SessionPendingListOutput = { data: Array }["data"] + +export type SessionInstructionsEntryListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type SessionInstructionsEntryListOutput = { data: Array }["data"] + +export type SessionInstructionsEntryPutInput = { + readonly sessionID: { readonly sessionID: string; readonly key: string }["sessionID"] + readonly key: { readonly sessionID: string; readonly key: string }["key"] + readonly value: { readonly value: JsonValue }["value"] +} + +export type SessionInstructionsEntryPutOutput = void + +export type SessionInstructionsEntryRemoveInput = { + readonly sessionID: { readonly sessionID: string; readonly key: string }["sessionID"] + readonly key: { readonly sessionID: string; readonly key: string }["key"] +} + +export type SessionInstructionsEntryRemoveOutput = void + +export type SessionGenerateInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly prompt: { readonly prompt: string }["prompt"] +} + +export type SessionGenerateOutput = SessionGenerateResponse["data"] + +export type SessionLogInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly after?: { readonly after?: number | undefined; readonly follow?: boolean | undefined }["after"] + readonly follow?: { readonly after?: number | undefined; readonly follow?: boolean | undefined }["follow"] +} + +export type SessionLogOutput = SessionLogItem + +export type SessionInterruptInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type SessionInterruptOutput = void + +export type SessionBackgroundInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type SessionBackgroundOutput = void + +export type SessionMessageInput = { + readonly sessionID: { readonly sessionID: string; readonly messageID: string }["sessionID"] + readonly messageID: { readonly sessionID: string; readonly messageID: string }["messageID"] +} + +export type SessionMessageOutput = { data: SessionMessageInfo }["data"] + +export type MessageListInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly limit?: { + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly cursor?: string | undefined + }["limit"] + readonly order?: { + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly cursor?: string | undefined + }["order"] + readonly cursor?: { + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly cursor?: string | undefined + }["cursor"] +} + +export type MessageListOutput = SessionMessagesResponse + +export type ModelListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ModelListOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type ModelDefaultInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ModelDefaultOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: ModelInfo | null +} + +export type GenerateTextInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly prompt: { + readonly prompt: string + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + }["prompt"] + readonly model?: { + readonly prompt: string + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + }["model"] +} + +export type GenerateTextOutput = GenerateTextResponse["data"] + +export type ProviderListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ProviderListOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type ProviderGetInput = { + readonly providerID: { readonly providerID: string }["providerID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ProviderGetOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: ProviderV2Info +} + +export type IntegrationListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type IntegrationListOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type IntegrationGetInput = { + readonly integrationID: { readonly integrationID: string }["integrationID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type IntegrationGetOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: IntegrationInfo | null +} + +export type IntegrationWellknownAddInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly url: { readonly url: string }["url"] +} + +export type IntegrationWellknownAddOutput = void + +export type IntegrationConnectKeyInput = { + readonly integrationID: { readonly integrationID: string }["integrationID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly key: { readonly key: string; readonly label?: string | undefined }["key"] + readonly label?: { readonly key: string; readonly label?: string | undefined }["label"] +} + +export type IntegrationConnectKeyOutput = void + +export type IntegrationOauthConnectInput = { + readonly integrationID: { readonly integrationID: string }["integrationID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly methodID: { + readonly methodID: string + readonly inputs: { readonly [x: string]: string } + readonly label?: string | undefined + }["methodID"] + readonly inputs: { + readonly methodID: string + readonly inputs: { readonly [x: string]: string } + readonly label?: string | undefined + }["inputs"] + readonly label?: { + readonly methodID: string + readonly inputs: { readonly [x: string]: string } + readonly label?: string | undefined + }["label"] +} + +export type IntegrationOauthConnectOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: { + attemptID: string + url: string + instructions: string + mode: "auto" | "code" + time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" } + } +} + +export type IntegrationOauthStatusInput = { + readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] + readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type IntegrationOauthStatusOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: IntegrationAttemptStatus +} + +export type IntegrationOauthCompleteInput = { + readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] + readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly code?: { readonly code?: string | undefined }["code"] +} + +export type IntegrationOauthCompleteOutput = void + +export type IntegrationOauthCancelInput = { + readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] + readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type IntegrationOauthCancelOutput = void + +export type IntegrationCommandConnectInput = { + readonly integrationID: { readonly integrationID: string }["integrationID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly methodID: { readonly methodID: string; readonly label?: string | undefined }["methodID"] + readonly label?: { readonly methodID: string; readonly label?: string | undefined }["label"] +} + +export type IntegrationCommandConnectOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: IntegrationCommandAttempt +} + +export type IntegrationCommandStatusInput = { + readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] + readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type IntegrationCommandStatusOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: IntegrationCommandAttemptStatus +} + +export type IntegrationCommandCancelInput = { + readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] + readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type IntegrationCommandCancelOutput = void + +export type McpListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type McpListOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type McpResourceCatalogInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type McpResourceCatalogOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: McpResourceCatalog +} + +export type CredentialUpdateInput = { + readonly credentialID: { readonly credentialID: string }["credentialID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly label: { readonly label: string }["label"] +} + +export type CredentialUpdateOutput = void + +export type CredentialRemoveInput = { + readonly credentialID: { readonly credentialID: string }["credentialID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type CredentialRemoveOutput = void + +export type ProjectListOutput = Array + +export type ProjectCurrentInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ProjectCurrentOutput = ProjectCurrent + +export type ProjectDirectoriesInput = { + readonly projectID: { readonly projectID: string }["projectID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ProjectDirectoriesOutput = ProjectDirectories + +export type FormRequestListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type FormRequestListOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type FormListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type FormListOutput = { data: Array }["data"] + +export type FormCreateInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly id?: { + readonly id?: string | null + readonly title: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly fields: readonly [ + ( + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "string" + readonly format?: "email" | "uri" | "date" | "date-time" + readonly minLength?: number + readonly maxLength?: number + readonly pattern?: string + readonly placeholder?: string + readonly default?: string + readonly options?: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly custom?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "number" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "integer" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "boolean" + readonly default?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "multiselect" + readonly options: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly minItems?: number + readonly maxItems?: number + readonly custom?: boolean + readonly default?: ReadonlyArray + } + | { + readonly key: string + readonly type: "external" + readonly url: string + readonly title?: string + readonly description?: string + } + ), + ...Array< + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "string" + readonly format?: "email" | "uri" | "date" | "date-time" + readonly minLength?: number + readonly maxLength?: number + readonly pattern?: string + readonly placeholder?: string + readonly default?: string + readonly options?: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly custom?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "number" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "integer" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "boolean" + readonly default?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "multiselect" + readonly options: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly minItems?: number + readonly maxItems?: number + readonly custom?: boolean + readonly default?: ReadonlyArray + } + | { + readonly key: string + readonly type: "external" + readonly url: string + readonly title?: string + readonly description?: string + } + >, + ] + }["id"] + readonly title: { + readonly id?: string | null + readonly title: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly fields: readonly [ + ( + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "string" + readonly format?: "email" | "uri" | "date" | "date-time" + readonly minLength?: number + readonly maxLength?: number + readonly pattern?: string + readonly placeholder?: string + readonly default?: string + readonly options?: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly custom?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "number" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "integer" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "boolean" + readonly default?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "multiselect" + readonly options: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly minItems?: number + readonly maxItems?: number + readonly custom?: boolean + readonly default?: ReadonlyArray + } + | { + readonly key: string + readonly type: "external" + readonly url: string + readonly title?: string + readonly description?: string + } + ), + ...Array< + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "string" + readonly format?: "email" | "uri" | "date" | "date-time" + readonly minLength?: number + readonly maxLength?: number + readonly pattern?: string + readonly placeholder?: string + readonly default?: string + readonly options?: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly custom?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "number" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "integer" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "boolean" + readonly default?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "multiselect" + readonly options: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly minItems?: number + readonly maxItems?: number + readonly custom?: boolean + readonly default?: ReadonlyArray + } + | { + readonly key: string + readonly type: "external" + readonly url: string + readonly title?: string + readonly description?: string + } + >, + ] + }["title"] + readonly metadata?: { + readonly id?: string | null + readonly title: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly fields: readonly [ + ( + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "string" + readonly format?: "email" | "uri" | "date" | "date-time" + readonly minLength?: number + readonly maxLength?: number + readonly pattern?: string + readonly placeholder?: string + readonly default?: string + readonly options?: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly custom?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "number" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "integer" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "boolean" + readonly default?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "multiselect" + readonly options: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly minItems?: number + readonly maxItems?: number + readonly custom?: boolean + readonly default?: ReadonlyArray + } + | { + readonly key: string + readonly type: "external" + readonly url: string + readonly title?: string + readonly description?: string + } + ), + ...Array< + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "string" + readonly format?: "email" | "uri" | "date" | "date-time" + readonly minLength?: number + readonly maxLength?: number + readonly pattern?: string + readonly placeholder?: string + readonly default?: string + readonly options?: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly custom?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "number" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "integer" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "boolean" + readonly default?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "multiselect" + readonly options: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly minItems?: number + readonly maxItems?: number + readonly custom?: boolean + readonly default?: ReadonlyArray + } + | { + readonly key: string + readonly type: "external" + readonly url: string + readonly title?: string + readonly description?: string + } + >, + ] + }["metadata"] + readonly fields: { + readonly id?: string | null + readonly title: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly fields: readonly [ + ( + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "string" + readonly format?: "email" | "uri" | "date" | "date-time" + readonly minLength?: number + readonly maxLength?: number + readonly pattern?: string + readonly placeholder?: string + readonly default?: string + readonly options?: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly custom?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "number" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "integer" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "boolean" + readonly default?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "multiselect" + readonly options: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly minItems?: number + readonly maxItems?: number + readonly custom?: boolean + readonly default?: ReadonlyArray + } + | { + readonly key: string + readonly type: "external" + readonly url: string + readonly title?: string + readonly description?: string + } + ), + ...Array< + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "string" + readonly format?: "email" | "uri" | "date" | "date-time" + readonly minLength?: number + readonly maxLength?: number + readonly pattern?: string + readonly placeholder?: string + readonly default?: string + readonly options?: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly custom?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "number" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "integer" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "boolean" + readonly default?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "multiselect" + readonly options: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly minItems?: number + readonly maxItems?: number + readonly custom?: boolean + readonly default?: ReadonlyArray + } + | { + readonly key: string + readonly type: "external" + readonly url: string + readonly title?: string + readonly description?: string + } + >, + ] + }["fields"] +} + +export type FormCreateOutput = { data: FormInfo }["data"] + +export type FormGetInput = { + readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"] + readonly formID: { readonly sessionID: string; readonly formID: string }["formID"] +} + +export type FormGetOutput = { data: FormInfo }["data"] + +export type FormStateInput = { + readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"] + readonly formID: { readonly sessionID: string; readonly formID: string }["formID"] +} + +export type FormStateOutput = { data: FormState }["data"] + +export type FormReplyInput = { + readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"] + readonly formID: { readonly sessionID: string; readonly formID: string }["formID"] + readonly answer: { + readonly answer: { readonly [x: string]: string | number | boolean | ReadonlyArray } + }["answer"] +} + +export type FormReplyOutput = void + +export type FormCancelInput = { + readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"] + readonly formID: { readonly sessionID: string; readonly formID: string }["formID"] +} + +export type FormCancelOutput = void + +export type PermissionRequestListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type PermissionRequestListOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type PermissionSavedListInput = { readonly projectID?: { readonly projectID?: string | undefined }["projectID"] } + +export type PermissionSavedListOutput = { data: Array }["data"] + +export type PermissionSavedRemoveInput = { readonly id: { readonly id: string }["id"] } + +export type PermissionSavedRemoveOutput = void + +export type PermissionCreateInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly id?: { + readonly id?: string | null + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray + readonly metadata?: { readonly [x: string]: JsonValue } + readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } + readonly agent?: string | null + }["id"] + readonly action: { + readonly id?: string | null + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray + readonly metadata?: { readonly [x: string]: JsonValue } + readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } + readonly agent?: string | null + }["action"] + readonly resources: { + readonly id?: string | null + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray + readonly metadata?: { readonly [x: string]: JsonValue } + readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } + readonly agent?: string | null + }["resources"] + readonly save?: { + readonly id?: string | null + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray + readonly metadata?: { readonly [x: string]: JsonValue } + readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } + readonly agent?: string | null + }["save"] + readonly metadata?: { + readonly id?: string | null + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray + readonly metadata?: { readonly [x: string]: JsonValue } + readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } + readonly agent?: string | null + }["metadata"] + readonly source?: { + readonly id?: string | null + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray + readonly metadata?: { readonly [x: string]: JsonValue } + readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } + readonly agent?: string | null + }["source"] + readonly agent?: { + readonly id?: string | null + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray + readonly metadata?: { readonly [x: string]: JsonValue } + readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } + readonly agent?: string | null + }["agent"] +} + +export type PermissionCreateOutput = { data: { id: string; effect: PermissionV2Effect } }["data"] + +export type PermissionListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type PermissionListOutput = { data: Array }["data"] + +export type PermissionGetInput = { + readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] + readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] +} + +export type PermissionGetOutput = { data: PermissionV2Request }["data"] + +export type PermissionReplyInput = { + readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] + readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] + readonly reply: { readonly reply: "once" | "always" | "reject"; readonly message?: string | undefined }["reply"] + readonly message?: { readonly reply: "once" | "always" | "reject"; readonly message?: string | undefined }["message"] +} + +export type PermissionReplyOutput = void + +export type FileReadInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly path: string +} + +export type FileReadOutput = globalThis.Uint8Array + +export type FileListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly path?: string | undefined + }["location"] + readonly path?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly path?: string | undefined + }["path"] +} + +export type FileListOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type FileFindInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly query: string + readonly type?: "file" | "directory" | undefined + readonly limit?: number | undefined + }["location"] + readonly query: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly query: string + readonly type?: "file" | "directory" | undefined + readonly limit?: number | undefined + }["query"] + readonly type?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly query: string + readonly type?: "file" | "directory" | undefined + readonly limit?: number | undefined + }["type"] + readonly limit?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly query: string + readonly type?: "file" | "directory" | undefined + readonly limit?: number | undefined + }["limit"] +} + +export type FileFindOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type CommandListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type CommandListOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type SkillListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type SkillListOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type EventSubscribeOutput = V2Event + +export type PtyListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type PtyListOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type PtyCreateInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly command?: { + readonly command?: string + readonly args?: ReadonlyArray + readonly cwd?: string + readonly title?: string + readonly env?: { readonly [x: string]: string } + }["command"] + readonly args?: { + readonly command?: string + readonly args?: ReadonlyArray + readonly cwd?: string + readonly title?: string + readonly env?: { readonly [x: string]: string } + }["args"] + readonly cwd?: { + readonly command?: string + readonly args?: ReadonlyArray + readonly cwd?: string + readonly title?: string + readonly env?: { readonly [x: string]: string } + }["cwd"] + readonly title?: { + readonly command?: string + readonly args?: ReadonlyArray + readonly cwd?: string + readonly title?: string + readonly env?: { readonly [x: string]: string } + }["title"] + readonly env?: { + readonly command?: string + readonly args?: ReadonlyArray + readonly cwd?: string + readonly title?: string + readonly env?: { readonly [x: string]: string } + }["env"] +} + +export type PtyCreateOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Pty +} + +export type PtyGetInput = { + readonly ptyID: { readonly ptyID: string }["ptyID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type PtyGetOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Pty +} + +export type PtyUpdateInput = { + readonly ptyID: { readonly ptyID: string }["ptyID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly title?: { + readonly title?: string + readonly size?: { readonly rows: number; readonly cols: number } + }["title"] + readonly size?: { readonly title?: string; readonly size?: { readonly rows: number; readonly cols: number } }["size"] +} + +export type PtyUpdateOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Pty +} + +export type PtyRemoveInput = { + readonly ptyID: { readonly ptyID: string }["ptyID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type PtyRemoveOutput = void + +export type ShellListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ShellListOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type ShellCreateInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly command: { + readonly command: string + readonly cwd?: string + readonly timeout: number + readonly metadata?: { readonly [x: string]: JsonValue } + }["command"] + readonly cwd?: { + readonly command: string + readonly cwd?: string + readonly timeout: number + readonly metadata?: { readonly [x: string]: JsonValue } + }["cwd"] + readonly timeout: { + readonly command: string + readonly cwd?: string + readonly timeout: number + readonly metadata?: { readonly [x: string]: JsonValue } + }["timeout"] + readonly metadata?: { + readonly command: string + readonly cwd?: string + readonly timeout: number + readonly metadata?: { readonly [x: string]: JsonValue } + }["metadata"] +} + +export type ShellCreateOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: ShellInfo1 +} + +export type ShellGetInput = { + readonly id: { readonly id: string }["id"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ShellGetOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: ShellInfo1 +} + +export type ShellTimeoutInput = { + readonly id: { readonly id: string }["id"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly timeout: { readonly timeout: number }["timeout"] +} + +export type ShellTimeoutOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: ShellInfo1 +} + +export type ShellOutputInput = { + readonly id: { readonly id: string }["id"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly cursor?: number | undefined + readonly limit?: number | undefined + }["location"] + readonly cursor?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly cursor?: number | undefined + readonly limit?: number | undefined + }["cursor"] + readonly limit?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly cursor?: number | undefined + readonly limit?: number | undefined + }["limit"] +} + +export type ShellOutputOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: { output: string; cursor: number; size: number; truncated: boolean } +} + +export type ShellRemoveInput = { + readonly id: { readonly id: string }["id"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ShellRemoveOutput = void + +export type QuestionRequestListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type QuestionRequestListOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type QuestionListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type QuestionListOutput = { data: Array }["data"] + +export type QuestionReplyInput = { + readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] + readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] + readonly answers: { readonly answers: ReadonlyArray> }["answers"] +} + +export type QuestionReplyOutput = void + +export type QuestionRejectInput = { + readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] + readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] +} + +export type QuestionRejectOutput = void + +export type ReferenceListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ReferenceListOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type ProjectCopyCreateInput = { + readonly projectID: { readonly projectID: string }["projectID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly strategy: { readonly strategy: string; readonly directory: string; readonly name?: string }["strategy"] + readonly directory: { readonly strategy: string; readonly directory: string; readonly name?: string }["directory"] + readonly name?: { readonly strategy: string; readonly directory: string; readonly name?: string }["name"] +} + +export type ProjectCopyCreateOutput = ProjectCopyCopy + +export type ProjectCopyRemoveInput = { + readonly projectID: { readonly projectID: string }["projectID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly directory: { readonly directory: string; readonly force: boolean }["directory"] + readonly force: { readonly directory: string; readonly force: boolean }["force"] +} + +export type ProjectCopyRemoveOutput = void + +export type ProjectCopyRefreshInput = { + readonly projectID: { readonly projectID: string }["projectID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ProjectCopyRefreshOutput = void + +export type VcsStatusInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type VcsStatusOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type VcsDiffInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly mode: "working" | "branch" + readonly context?: number | undefined + }["location"] + readonly mode: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly mode: "working" | "branch" + readonly context?: number | undefined + }["mode"] + readonly context?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly mode: "working" | "branch" + readonly context?: number | undefined + }["context"] +} + +export type VcsDiffOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + +export type DebugLocationListOutput = Array + +export type DebugLocationEvictInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type DebugLocationEvictOutput = void diff --git a/packages/client/src/promise/index.ts b/packages/client/src/promise/index.ts new file mode 100644 index 0000000000..fd889c64e5 --- /dev/null +++ b/packages/client/src/promise/index.ts @@ -0,0 +1,16 @@ +export * from "./generated/index" +export type { + AgentApi, + CatalogApi, + CommandApi, + EventApi, + IntegrationApi, + ModelApi, + PluginApi, + ProviderApi, + ReferenceApi, + SessionApi, + SkillApi, +} from "./api.js" +export type { EventSubscribeOutput as OpenCodeEvent } from "./generated/types" +export type OpenCodeClient = ReturnType diff --git a/packages/client/src/promise/service.ts b/packages/client/src/promise/service.ts new file mode 100644 index 0000000000..6d8aa5e327 --- /dev/null +++ b/packages/client/src/promise/service.ts @@ -0,0 +1,251 @@ +import { readFile } from "node:fs/promises" +import { spawn, type ChildProcess } from "node:child_process" +import { homedir } from "node:os" +import { join } from "node:path" +import type { DiscoverOptions, Endpoint, Info, EnsureOptions, StopOptions } from "../service.js" +import type { ServiceHealth, ServiceStopResponse } from "./generated/types.js" + +export * from "../service.js" + +// Find, start, and stop the local opencode background service. +// +// The registration file is the complete discovery contract. This module is +// intentionally implemented with Node APIs so Promise clients do not need +// Effect or @effect/platform-node at runtime. + +type Contender = { + readonly child: ChildProcess + readonly error: () => Error | undefined +} + +/** Discover a healthy, compatible local service without starting one. */ +export async function discover(options: DiscoverOptions = {}) { + return (await discoverLocal(options))?.endpoint +} + +async function discoverLocal(options: DiscoverOptions) { + const found = (await registered(options.file)).service + if (found?.state !== "ready") return undefined + if (options.version !== undefined && found.version !== options.version) return undefined + return found +} + +/** Ensure a healthy, compatible local service is running. */ +export async function ensure(options: EnsureOptions = {}): Promise { + const deadline = Date.now() + 120_000 + const contenders = new Set() + let announced = false + let lastSpawn = 0 + let spawnDelay = 5_000 + let ownerHeld = false + + const announce = (reason: "missing" | "version-mismatch", previousVersion?: string) => { + if (announced) return + announced = true + options.onStart?.(reason, previousVersion) + } + const spawnContender = () => { + const [command, ...args] = options.command ?? ["opencode", "serve", "--service"] + if (command === undefined) throw new Error("Missing service command") + try { + const child = spawn(command, args, { detached: true, stdio: "ignore" }) + let error: Error | undefined + child.once("error", (cause) => { + error = new Error("Failed to start server", { cause }) + }) + child.unref() + return { child, error: () => error } + } catch (cause) { + throw new Error("Failed to start server", { cause }) + } + } + + while (true) { + if (Date.now() >= deadline) throw new Error("Timed out waiting for the background service to start") + const registration = await registered(options.file, true) + + if (registration.service !== undefined) { + ownerHeld = false + spawnDelay = 5_000 + const service = registration.service + const compatible = !service.legacy && (options.version === undefined || service.version === options.version) + if (compatible && service.state === "ready") return service.endpoint + if (compatible && service.state === "failed") throw new Error("Background service failed to start") + if (!compatible) { + announce("version-mismatch", service.version) + await kill(service, options).catch(() => undefined) + lastSpawn = 0 + } + } else { + if (lastSpawn === 0 && registration.info !== undefined) lastSpawn = Date.now() + const failure = [...contenders].map(contenderFailure).find((error) => error !== undefined) + if (failure !== undefined) throw failure + const finished = [...contenders].filter(contenderFinished) + if (finished.some((item) => item.child.exitCode === 0)) { + ownerHeld = true + spawnDelay = Math.min(spawnDelay * 2, 30_000) + } + finished.forEach((item) => contenders.delete(item)) + // Keep one candidate plus one lock probe so a pre-lock stall cannot block recovery. + if (contenders.size < 2 && Date.now() - lastSpawn >= spawnDelay) { + announce("missing") + contenders.add(spawnContender()) + lastSpawn = Date.now() + } + } + await delay(1_000) + } +} + +function contenderFailure(contender: Contender) { + const error = contender.error() + if (error !== undefined) return error + if (contender.child.exitCode !== null && contender.child.exitCode !== 0) + return new Error(`Server process exited with code ${contender.child.exitCode}`) + if (contender.child.signalCode !== null) + return new Error(`Server process terminated by ${contender.child.signalCode}`) + return undefined +} + +function contenderFinished(contender: Contender) { + return contender.error() !== undefined || contender.child.exitCode !== null || contender.child.signalCode !== null +} + +/** Stop the registered local service. */ +export async function stop(options: StopOptions = {}) { + const existing = await find(options) + if (existing !== undefined) await kill(existing, options) +} + +function fallback() { + return join(process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state"), "opencode", "service.json") +} + +/** Create HTTP authentication headers for a service endpoint. */ +export function headers(endpoint: Endpoint) { + if (endpoint.auth === undefined) return undefined + return { + authorization: "Basic " + Buffer.from(endpoint.auth.username + ":" + endpoint.auth.password).toString("base64"), + } +} + +async function read(file?: string) { + const text = await readFile(file ?? fallback(), "utf8").catch(() => undefined) + if (text === undefined) return undefined + try { + return JSON.parse(text) as Info + } catch { + return undefined + } +} + +type LocalService = { + readonly info: Info + readonly endpoint: Endpoint + readonly version?: string + readonly state: "ready" | "waiting" | "failed" + readonly legacy: boolean +} + +async function probe(info: Info, allowLegacy = false): Promise { + const endpoint = { + url: info.url, + auth: + info.password === undefined + ? undefined + : { type: "basic" as const, username: "opencode", password: info.password }, + } satisfies Endpoint + const response = await fetch(new URL("/api/health", info.url), { + headers: headers(endpoint), + signal: AbortSignal.timeout(2_000), + }).catch(() => undefined) + const body = (await response?.json().catch(() => undefined)) as ServiceHealth | { readonly healthy: true } | undefined + if (body !== undefined && "version" in body && "pid" in body) { + if (body.pid !== info.pid) return undefined + if (info.version !== undefined && body.version !== info.version) return undefined + return { + info, + endpoint, + version: body.version, + state: response?.ok ? "ready" : response?.status === 500 ? "failed" : "waiting", + legacy: false, + } + } + if (!allowLegacy || body?.healthy !== true) return undefined + return { info, endpoint, state: "ready", legacy: true } +} + +async function registered(file?: string, allowLegacy = false) { + const info = await read(file) + if (info === undefined) return { info: undefined, service: undefined } + return { info, service: await probe(info, allowLegacy) } +} + +async function find(options: { readonly file?: string }) { + return (await registered(options.file, true)).service +} + +function signal(pid: number, name: NodeJS.Signals) { + try { + process.kill(pid, name) + } catch {} +} + +function stopped(pid: number) { + try { + process.kill(pid, 0) + return false + } catch { + return true + } +} + +async function waitUntilStopped(pid: number) { + for (let attempt = 0; attempt <= 100; attempt++) { + if (stopped(pid)) return true + if (attempt < 100) await delay(50) + } + return false +} + +function same(left: Info, right: Info) { + return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid +} + +async function kill(service: LocalService, options: { readonly file?: string }) { + const requested = await requestStop(service) + if (requested === "rejected") return + if (requested === "unsupported") { + const current = await find(options) + if (current === undefined || !same(current.info, service.info)) return + signal(service.info.pid, "SIGTERM") + } + if (await waitUntilStopped(service.info.pid)) return + + const latest = await find(options) + if (latest === undefined || !same(latest.info, service.info)) return + signal(service.info.pid, "SIGKILL") + if (!(await waitUntilStopped(service.info.pid))) + throw new Error(`Server process ${service.info.pid} is still running`) +} + +async function requestStop(service: LocalService) { + if (service.info.id === undefined || service.legacy) return "unsupported" as const + const response = await fetch(new URL("/api/service/stop", service.info.url), { + method: "POST", + headers: { ...headers(service.endpoint), "content-type": "application/json" }, + body: JSON.stringify({ instanceID: service.info.id }), + signal: AbortSignal.timeout(2_000), + }).catch(() => undefined) + if (response === undefined || response.status === 404 || response.status === 405) return "unsupported" as const + const body = (await response.json().catch(() => undefined)) as ServiceStopResponse | undefined + if (!response.ok || body?.accepted !== true) return "rejected" as const + return "accepted" as const +} + +function delay(milliseconds: number) { + return new Promise((resolve) => setTimeout(resolve, milliseconds)) +} + +/** Promise-based local service lifecycle operations. */ +export const Service = { discover, ensure, stop, headers } diff --git a/packages/client/src/service.ts b/packages/client/src/service.ts new file mode 100644 index 0000000000..96d54871f6 --- /dev/null +++ b/packages/client/src/service.ts @@ -0,0 +1,53 @@ +/** Connection details for a local OpenCode service. */ +export type Endpoint = { + /** Base URL of the service. */ + readonly url: string + /** Authentication required by the service, when configured. */ + readonly auth?: { + /** HTTP authentication scheme. */ + readonly type: "basic" + /** Basic authentication username. */ + readonly username: string + /** Basic authentication password. */ + readonly password: string + } +} + +/** Options used to discover the local OpenCode service. */ +export type DiscoverOptions = { + /** Absolute registration file path. Defaults to the XDG state directory. */ + readonly file?: string + /** Required service version. */ + readonly version?: string +} + +/** Reason ensuring the service requires a new process. */ +export type EnsureReason = "missing" | "version-mismatch" + +/** Options used to ensure the local OpenCode service is running. */ +export type EnsureOptions = DiscoverOptions & { + /** Service command and arguments. Defaults to `opencode serve --service`. */ + readonly command?: ReadonlyArray + /** Called once before spawning a new service process. */ + readonly onStart?: (reason: EnsureReason, previousVersion?: string) => void +} + +/** Options used to stop the local OpenCode service. */ +export type StopOptions = { + /** Absolute registration file path. Defaults to the XDG state directory. */ + readonly file?: string +} + +/** Contents of the local service registration file. */ +export type Info = { + /** Unique service instance identifier. */ + readonly id?: string + /** OpenCode version served by the process. */ + readonly version?: string + /** Base URL advertised by the service. */ + readonly url: string + /** Operating system process identifier. */ + readonly pid: number + /** Private service password, when authentication is enabled. */ + readonly password?: string +} diff --git a/packages/client/test/api.types.ts b/packages/client/test/api.types.ts new file mode 100644 index 0000000000..ba95bb4297 --- /dev/null +++ b/packages/client/test/api.types.ts @@ -0,0 +1,40 @@ +import { Effect } from "effect" +import { OpenCode as EffectOpenCode, type AppApi as EffectApi } from "../src/effect" + +type EffectClient = Effect.Success> +type PromiseClient = ReturnType + +declare const effectClient: EffectClient +declare const promiseClient: PromiseClient + +const effectApi: EffectApi = effectClient + +declare const sessionID: Parameters[0]["sessionID"] + +const effectList: Effect.Effect< + ReadonlyArray<{ readonly key: string; readonly value: unknown }>, + unknown +> = effectApi.session.instructions.entry.list({ sessionID }) +const effectPut: Effect.Effect = effectApi.session.instructions.entry.put({ + sessionID, + key: "review-notes", + value: { text: "Check the diff" }, +}) +const effectRemove: Effect.Effect = effectApi.session.instructions.entry.remove({ + sessionID, + key: "review-notes", +}) + +const promiseList: Promise> = + promiseClient.session.instructions.entry.list({ sessionID: "ses_test" }) +const promisePut: Promise = promiseClient.session.instructions.entry.put({ + sessionID: "ses_test", + key: "review-notes", + value: { text: "Check the diff" }, +}) +const promiseRemove: Promise = promiseClient.session.instructions.entry.remove({ + sessionID: "ses_test", + key: "review-notes", +}) + +void [effectList, effectPut, effectRemove, promiseList, promisePut, promiseRemove] diff --git a/packages/client/test/contract-identity.test.ts b/packages/client/test/contract-identity.test.ts index 64a2e958ce..8de2115fa7 100644 --- a/packages/client/test/contract-identity.test.ts +++ b/packages/client/test/contract-identity.test.ts @@ -1,47 +1,17 @@ import { expect, test } from "bun:test" import { Schema } from "effect" -import { AgentV2 } from "@opencode-ai/core/agent" -import { Location as CoreLocation } from "@opencode-ai/core/location" -import { ModelV2 } from "@opencode-ai/core/model" -import { SessionV2 } from "@opencode-ai/core/session" -import { SessionInput as CoreSessionInput } from "@opencode-ai/core/session/input" -import { SessionMessage as CoreSessionMessage } from "@opencode-ai/core/session/message" -import { Prompt as CorePrompt } from "@opencode-ai/core/session/prompt" import { Agent } from "@opencode-ai/schema/agent" -import { Location } from "@opencode-ai/schema/location" import { Model } from "@opencode-ai/schema/model" -import { Project } from "@opencode-ai/schema/project" -import { Provider } from "@opencode-ai/schema/provider" import { Prompt } from "@opencode-ai/schema/prompt" import { Session } from "@opencode-ai/schema/session" -import { SessionInput } from "@opencode-ai/schema/session-input" import { SessionMessage } from "@opencode-ai/schema/session-message" -import { Workspace } from "@opencode-ai/schema/workspace" -import { Api } from "@opencode-ai/server/api" -import { compile, emitPromise } from "@opencode-ai/httpapi-codegen" -import { ClientApi, endpointNames, groupNames, omitEndpoints } from "../src/contract" -test("Core and Server reuse the authoritative Schema and Protocol values", () => { - expect(AgentV2.ID).toBe(Agent.ID) - expect(CoreLocation.Ref).toBe(Location.Ref) - expect(ModelV2.Ref).toBe(Model.Ref) - expect(SessionV2.Info).toBe(Session.Info) - expect(CoreSessionInput.Admitted).toBe(SessionInput.Admitted) - expect(CoreSessionMessage.Message).toBe(SessionMessage.Message) - expect(CorePrompt).toBe(Prompt) - expect(Api.groups["server.session"].identifier).toBe("server.session") - expect(Object.keys(ClientApi.groups)).toEqual(Object.keys(Api.groups)) - expect(Session.ID.create()).toStartWith("ses_") - expect(Project.ID.global).toBe("global") - expect(Provider.ID.anthropic).toBe("anthropic") - expect(Workspace.ID.create()).toStartWith("wrk_") -}) +const Client = await import("../src/effect") -test("client and Server contracts generate identically", () => { - const server = compile(Api, { groupNames, endpointNames, omitEndpoints }) - const client = compile(ClientApi, { groupNames, endpointNames, omitEndpoints }) - - expect(emitPromise(client)).toEqual(emitPromise(server)) +test("effect entrypoint exposes canonical Schema contracts", () => { + expect(Client.Agent).toBe(Agent) + expect(Client.Model).toBe(Model) + expect(Client.Session).toBe(Session) }) test("shared DTO schemas construct and decode plain objects", () => { @@ -54,5 +24,4 @@ test("shared DTO schemas construct and decode plain objects", () => { expect(Object.getPrototypeOf(content)).toBe(Object.prototype) expect(Prompt.ast.annotations?.identifier).toBe("Prompt") expect(SessionMessage.AssistantText.ast.annotations?.identifier).toBe("Session.Message.Assistant.Text") - expect(CoreSessionMessage.AssistantText).toBe(SessionMessage.AssistantText) }) diff --git a/packages/client/test/effect.test.ts b/packages/client/test/effect.test.ts index 7bf4d26f8f..b52d86d382 100644 --- a/packages/client/test/effect.test.ts +++ b/packages/client/test/effect.test.ts @@ -1,27 +1,102 @@ import { expect, test } from "bun:test" import { DateTime, Effect, Stream } from "effect" import { HttpClient, HttpClientResponse } from "effect/unstable/http" -import { AbsolutePath, Agent, Location, Model, OpenCode, Prompt, Session, SessionMessage } from "../src/effect" +import { + AbsolutePath, + Agent, + Event, + Location, + Model, + OpenCode, + Prompt, + Session, + SessionMessage, +} from "../src/effect/index" -test("sessions.get returns the decoded Effect projection", async () => { +const synced = { type: "log.synced" as const, aggregateID: "ses_test", seq: Event.Seq.make(1) } + +test("health.get decodes the readiness response", async () => { + const httpClient = HttpClient.make((request) => + Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ healthy: true, version: "old", pid: 123 }))), + ) + const result = await Effect.gen(function* () { + const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" }) + return yield* client.health.get() + }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise) + + expect(result).toEqual({ healthy: true, version: "old", pid: 123 }) +}) + +test("session.get returns the decoded Effect projection", async () => { const httpClient = HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(session))), ) const result = await Effect.gen(function* () { const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" }) - return yield* client.sessions.get({ sessionID: Session.ID.make("ses_test") }) + return yield* client.session.get({ sessionID: Session.ID.make("ses_test") }) }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise) expect(DateTime.toEpochMillis(result.time.created)).toBe(1_717_171_717_000) }) -test("events.subscribe exposes and decodes the native Effect event stream", async () => { +test("session instructions methods use the public HTTP contract", async () => { + const requests: Array<{ method: string; url: string; body?: unknown }> = [] + const instructions = [{ key: "review-notes", value: { text: "Check the diff", priority: 1 } }] + const httpClient = HttpClient.make((request) => { + requests.push({ + method: request.method, + url: request.url, + body: request.body._tag === "Uint8Array" ? JSON.parse(new TextDecoder().decode(request.body.body)) : undefined, + }) + return Effect.succeed( + HttpClientResponse.fromWeb( + request, + request.method === "GET" ? Response.json({ data: instructions }) : new Response(null, { status: 204 }), + ), + ) + }) + const result = await Effect.gen(function* () { + const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" }) + const listed = yield* client.session.instructions.entry.list({ sessionID: Session.ID.make("ses_test") }) + yield* client.session.instructions.entry.put({ + sessionID: Session.ID.make("ses_test"), + key: "review-notes", + value: instructions[0].value, + }) + yield* client.session.instructions.entry.remove({ + sessionID: Session.ID.make("ses_test"), + key: "review-notes", + }) + return listed + }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise) + + expect(result).toEqual(instructions) + expect(requests).toEqual([ + { + method: "GET", + url: "http://localhost:3000/api/session/ses_test/instructions/entries", + body: undefined, + }, + { + method: "PUT", + url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes", + body: { value: { text: "Check the diff", priority: 1 } }, + }, + { + method: "DELETE", + url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes", + body: undefined, + }, + ]) +}) + +test("event.subscribe exposes and decodes the native Effect event stream", async () => { const httpClient = HttpClient.make((request) => Effect.succeed( HttpClientResponse.fromWeb( request, new Response( - `data: ${JSON.stringify({ id: "evt_connected", type: "server.connected", data: {} })}\n\n` + + `data: ${JSON.stringify({ id: "evt_connected", created: 0, type: "server.connected", data: {} })}\n\n` + `data: ${JSON.stringify(modelSwitchedEvent)}\n\n`, { headers: { "content-type": "text/event-stream" } }, ), @@ -30,17 +105,17 @@ test("events.subscribe exposes and decodes the native Effect event stream", asyn ) const events = await Effect.gen(function* () { const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" }) - return yield* client.events.subscribe().pipe(Stream.runCollect) + return yield* client.event.subscribe().pipe(Stream.runCollect) }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise) - expect(Array.from(events).map((event) => event.type)).toEqual(["server.connected", "session.next.model.switched"]) + expect(Array.from(events).map((event) => event.type)).toEqual(["server.connected", "session.model.selected"]) const durable = events[1] - if (durable?.type !== "session.next.model.switched") throw new Error("Expected model event") - expect(DateTime.toEpochMillis(durable.data.timestamp)).toBe(1_717_171_717_000) + if (durable?.type !== "session.model.selected") throw new Error("Expected model event") + expect(DateTime.toEpochMillis(durable.created)).toBe(1_717_171_717_000) expect(durable.durable).toEqual({ aggregateID: "ses_test", seq: 1, version: 1 }) }) -test("events.subscribe terminates on Effect protocol decode failures", async () => { +test("event.subscribe terminates on Effect protocol decode failures", async () => { const httpClient = HttpClient.make((request) => Effect.succeed( HttpClientResponse.fromWeb( @@ -53,42 +128,33 @@ test("events.subscribe terminates on Effect protocol decode failures", async () ) const error = await Effect.gen(function* () { const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" }) - return yield* client.events.subscribe().pipe(Stream.runCollect, Effect.flip) + return yield* client.event.subscribe().pipe(Stream.runCollect, Effect.flip) }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise) expect(error._tag).toBe("ClientError") }) test("session methods retain decoded Effect inputs and outputs", async () => { - const historyQueries: Array> = [] - let historyPage = 0 + const logQueries: Array> = [] const httpClient = HttpClient.make((request) => { const url = request.url - if (url.includes("/event")) { + if (url.includes("/log")) { + logQueries.push(Object.fromEntries(request.urlParams.params)) return Effect.succeed( HttpClientResponse.fromWeb( request, - new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`, { + new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\ndata: ${JSON.stringify(synced)}\n\n`, { headers: { "content-type": "text/event-stream" }, }), ), ) } - if (url.includes("/history")) { - historyPage++ - historyQueries.push(Object.fromEntries(request.urlParams.params)) - return Effect.succeed( - HttpClientResponse.fromWeb( - request, - Response.json( - historyPage === 1 ? { data: [modelSwitchedEvent], hasMore: true } : { data: [], hasMore: false }, - ), - ), - ) - } if (url.includes("/prompt")) { return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(admission))) } + if (url.endsWith("/compact")) { + return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(compactionAdmission))) + } if (url.includes("/context")) { return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ data: [] }))) } @@ -112,45 +178,33 @@ test("session methods retain decoded Effect inputs and outputs", async () => { }) const result = await Effect.gen(function* () { const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" }) - const page = yield* client.sessions.list({ limit: 10 }) - const active = yield* client.sessions.active() - const created = yield* client.sessions.create({ + const page = yield* client.session.list({ limit: 10 }) + const active = yield* client.session.active() + const created = yield* client.session.create({ location: Location.Ref.make({ directory: AbsolutePath.make("/tmp/project") }), }) - yield* client.sessions.switchAgent({ sessionID: Session.ID.make("ses_test"), agent: Agent.ID.make("build") }) - yield* client.sessions.switchModel({ + yield* client.session.switchAgent({ sessionID: Session.ID.make("ses_test"), agent: Agent.ID.make("build") }) + yield* client.session.switchModel({ sessionID: Session.ID.make("ses_test"), model: Model.Ref.make({ id: "claude", providerID: "anthropic" }), }) - const admitted = yield* client.sessions.prompt({ + const admitted = yield* client.session.prompt({ sessionID: Session.ID.make("ses_test"), - prompt: Prompt.make({ text: "Hello" }), + text: "Hello", resume: false, }) - yield* client.sessions.compact({ sessionID: Session.ID.make("ses_test") }) - yield* client.sessions.wait({ sessionID: Session.ID.make("ses_test") }) - const context = yield* client.sessions.context({ sessionID: Session.ID.make("ses_test") }) - const history = yield* client.sessions.history({ - sessionID: Session.ID.make("ses_test"), - after: 0, - limit: 1, - }) - const historyNext = history.hasMore - ? yield* client.sessions.history({ - sessionID: Session.ID.make("ses_test"), - after: history.data.at(-1)?.durable?.seq, - limit: 2, - }) - : undefined - const events = yield* client.sessions - .events({ sessionID: Session.ID.make("ses_test"), after: 0 }) + yield* client.session.compact({ sessionID: Session.ID.make("ses_test") }) + yield* client.session.wait({ sessionID: Session.ID.make("ses_test") }) + const context = yield* client.session.context({ sessionID: Session.ID.make("ses_test") }) + const log = yield* client.session + .log({ sessionID: Session.ID.make("ses_test"), after: Event.Seq.make(0) }) .pipe(Stream.runCollect) - yield* client.sessions.interrupt({ sessionID: Session.ID.make("ses_test") }) - const message = yield* client.sessions.message({ + yield* client.session.interrupt({ sessionID: Session.ID.make("ses_test") }) + const message = yield* client.session.message({ sessionID: Session.ID.make("ses_test"), messageID: SessionMessage.ID.make("msg_model"), }) - return { page, active, created, admitted, context, history, historyNext, events, message } + return { page, active, created, admitted, context, log, message } }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise) expect(DateTime.toEpochMillis(result.page.data[0].time.created)).toBe(1_717_171_717_000) @@ -159,19 +213,20 @@ test("session methods retain decoded Effect inputs and outputs", async () => { expect(Object.getPrototypeOf(result.created)).toBe(Object.prototype) expect(result.created.id).toBe("ses_test") expect(Object.getPrototypeOf(result.admitted)).toBe(Object.prototype) - expect(Object.getPrototypeOf(result.admitted.prompt)).toBe(Object.prototype) + expect(Object.getPrototypeOf(result.admitted.data)).toBe(Object.prototype) expect(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000) expect(result.context).toEqual([]) - expect(DateTime.toEpochMillis(result.history.data[0].data.timestamp)).toBe(1_717_171_717_000) - expect(result.history).toEqual(expect.objectContaining({ hasMore: true })) - expect(result.historyNext).toEqual({ data: [], hasMore: false }) - expect(historyQueries[0]).toEqual({ limit: "1", after: "0" }) - expect(historyQueries[1]).toEqual({ limit: "2", after: "1" }) - expect(DateTime.toEpochMillis(result.events[0].data.timestamp)).toBe(1_717_171_717_000) + expect(logQueries[0]).toEqual({ after: "0" }) + const logged = Array.from(result.log) + expect(logged.map((item) => item.type)).toEqual(["session.model.selected", "log.synced"]) + expect(logged[0]?.type === "session.model.selected" && DateTime.toEpochMillis(logged[0].created)).toBe( + 1_717_171_717_000, + ) + expect(logged.at(-1)).toEqual(synced) expect(result.message).toEqual(expect.objectContaining({ id: "msg_model", type: "model-switched" })) }) -test("sessions.history retains the typed SessionNotFoundError", async () => { +test("session.log retains the typed SessionNotFoundError", async () => { const httpClient = HttpClient.make((request) => Effect.succeed( HttpClientResponse.fromWeb( @@ -185,11 +240,7 @@ test("sessions.history retains the typed SessionNotFoundError", async () => { ) const error = await Effect.gen(function* () { const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" }) - return yield* client.sessions - .history({ - sessionID: Session.ID.make("ses_missing"), - }) - .pipe(Effect.flip) + return yield* client.session.log({ sessionID: Session.ID.make("ses_missing") }).pipe(Stream.runCollect, Effect.flip) }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise) expect(error._tag).toBe("SessionNotFoundError") @@ -220,12 +271,23 @@ const admission = { admittedSeq: 0, id: "msg_test", sessionID: "ses_test", - prompt: { text: "Hello" }, + type: "user", + data: { text: "Hello" }, delivery: "steer", timeCreated: 1_717_171_717_000, }, } +const compactionAdmission = { + data: { + type: "compaction", + admittedSeq: 1, + id: "msg_compaction", + sessionID: "ses_test", + timeCreated: 1_717_171_717_000, + }, +} + const modelSwitchedMessage = { id: "msg_model", type: "model-switched", @@ -235,12 +297,11 @@ const modelSwitchedMessage = { const modelSwitchedEvent = { id: "evt_model", - type: "session.next.model.switched", + created: 1_717_171_717_000, + type: "session.model.selected", durable: { aggregateID: "ses_test", seq: 1, version: 1 }, data: { - timestamp: 1_717_171_717_000, sessionID: "ses_test", - messageID: "msg_model", model: { id: "claude", providerID: "anthropic" }, }, } diff --git a/packages/client/test/fixture/service.ts b/packages/client/test/fixture/service.ts new file mode 100644 index 0000000000..40ef5fb5da --- /dev/null +++ b/packages/client/test/fixture/service.ts @@ -0,0 +1,77 @@ +import { appendFile, rename, writeFile } from "node:fs/promises" + +const [registration, mode, delay] = process.argv.slice(2) +if (registration === undefined || mode === undefined) throw new Error("Missing service fixture arguments") +if (mode === "failed") process.exit(1) +if (mode === "record-start") { + await writeFile(registration + ".started", "") + process.exit(1) +} +if (mode === "signal") process.kill(process.pid, process.platform === "win32" ? "SIGTERM" : "SIGKILL") + +if (mode === "delayed" || mode === "delayed-failed" || mode === "coordinated") { + await appendFile(registration + ".starts", process.pid + "\n") + const owner = await writeFile(registration + ".owner", String(process.pid), { flag: "wx" }) + .then(() => true) + .catch(() => false) + if (!owner) process.exit() + if (mode === "coordinated") { + while ((await Bun.file(registration + ".starts").text()).trim().split("\n").length < 2) await Bun.sleep(10) + } else await Bun.sleep(Number(delay)) + if (mode === "delayed-failed") process.exit(1) +} + +let requests = 0 +const version = mode === "old" || mode === "reject-stop" ? "old" : "test" +const id = crypto.randomUUID() +const server = Bun.serve({ + port: 0, + async fetch(request) { + const pathname = new URL(request.url).pathname + if (pathname === "/api/service/stop" && mode === "reject-stop") { + await writeFile(registration + ".stop-attempt", "") + return Response.json({ accepted: false }) + } + if (pathname === "/api/service/stop" && mode === "graceful") { + const body = await request.json() + if (typeof body !== "object" || body === null || body.instanceID !== id) return Response.json({ accepted: false }) + await writeFile(registration + ".stop", JSON.stringify(body)) + setTimeout(shutdown, 25) + return Response.json({ accepted: true }) + } + if (pathname !== "/api/health") return new Response(null, { status: 404 }) + requests += 1 + if (mode === "modern" && requests === 1) { + await writeFile(registration + ".first-request", "") + while (!(await Bun.file(registration + ".release").exists())) await Bun.sleep(5) + return new Response(null, { status: 503 }) + } + if (mode === "legacy") return Response.json({ healthy: true }) + if (mode === "starting" && !(await Bun.file(registration + ".release").exists())) + return Response.json({ healthy: true, version, pid: process.pid }, { status: 503 }) + if (mode === "failed-owner") + return Response.json({ healthy: true, version, pid: process.pid }, { status: 500 }) + if (mode === "starting" || mode === "graceful" || mode === "reject-stop") + return Response.json({ healthy: true, version, pid: process.pid }) + return Response.json({ healthy: true, version, pid: process.pid }) + }, +}) + +await writeFile( + registration + ".tmp", + JSON.stringify({ + id, + version: mode === "legacy" ? undefined : version, + url: server.url.toString(), + pid: process.pid, + }), + { mode: 0o600 }, +) +await rename(registration + ".tmp", registration) + +function shutdown() { + server.stop(true) + process.exit() +} +process.on("SIGTERM", shutdown) +process.on("SIGINT", shutdown) diff --git a/packages/client/test/import-boundaries.test.ts b/packages/client/test/import-boundaries.test.ts index 4875a3a5dc..6a979b00a7 100644 --- a/packages/client/test/import-boundaries.test.ts +++ b/packages/client/test/import-boundaries.test.ts @@ -27,6 +27,21 @@ describe("public import boundaries", () => { expect(within(network, protocol).length).toBeGreaterThan(0) expect(within(network, core)).toEqual([]) expect(within(network, server)).toEqual([]) + + const promiseService = await bundleInputs("@opencode-ai/client/service", "bun") + + expect(within(promiseService, effect)).toEqual([]) + expect(within(promiseService, schema)).toEqual([]) + expect(within(promiseService, protocol)).toEqual([]) + expect(within(promiseService, core)).toEqual([]) + expect(within(promiseService, server)).toEqual([]) + + const effectService = await bundleInputs("@opencode-ai/client/effect/service", "bun") + + expect(within(effectService, effect).length).toBeGreaterThan(0) + expect(within(effectService, protocol).length).toBeGreaterThan(0) + expect(within(effectService, core)).toEqual([]) + expect(within(effectService, server)).toEqual([]) }) }) diff --git a/packages/client/test/promise-service.test.ts b/packages/client/test/promise-service.test.ts new file mode 100644 index 0000000000..1cf6757a78 --- /dev/null +++ b/packages/client/test/promise-service.test.ts @@ -0,0 +1,96 @@ +import { afterEach, expect, test } from "bun:test" +import { mkdtemp, rm } from "node:fs/promises" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { Service, type EnsureReason } from "../src/promise/service" + +const fixture = join(import.meta.dir, "fixture/service.ts") +const processes: Bun.Subprocess[] = [] +const directories: string[] = [] + +afterEach(async () => { + processes.forEach((process) => process.kill("SIGTERM")) + await Promise.all(processes.splice(0).map((process) => process.exited)) + await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))) +}) + +test("discovers a registered service", async () => { + const registration = await setup("graceful") + + expect(await Service.discover({ file: registration, version: "test" })).toEqual( + expect.objectContaining({ url: expect.stringMatching(/^http:\/\//) }), + ) + expect(await Service.discover({ file: registration, version: "other" })).toBeUndefined() +}) + +test("ensures a missing service with native promises", async () => { + const directory = await temp() + const registration = join(directory, "service.json") + const starts: EnsureReason[] = [] + + const endpoint = await Service.ensure({ + file: registration, + version: "test", + command: [process.execPath, fixture, registration, "coordinated"], + onStart: (reason) => starts.push(reason), + }) + const info = await Bun.file(registration).json() + try { + expect(endpoint.url).toBe(info.url) + expect(starts).toEqual(["missing"]) + } finally { + process.kill(info.pid, "SIGTERM") + await waitForExit(info.pid) + } +}, 15_000) + +test("reports a failed registered service", async () => { + const registration = await setup("failed-owner") + + await expect(Service.ensure({ file: registration, version: "test", command: [] })).rejects.toThrow( + "Background service failed to start", + ) +}) + +test("requests graceful stop of the exact service instance", async () => { + const registration = await setup("graceful") + const info = await Bun.file(registration).json() + + await Service.stop({ file: registration }) + + expect(await Bun.file(registration + ".stop").json()).toEqual({ instanceID: info.id }) +}) + +async function setup(mode: string) { + const directory = await temp() + const registration = join(directory, "service.json") + processes.push(Bun.spawn([process.execPath, fixture, registration, mode], { stdout: "ignore", stderr: "inherit" })) + await waitForFile(registration) + return registration +} + +async function temp() { + const directory = await mkdtemp(join(tmpdir(), "opencode-promise-service-")) + directories.push(directory) + return directory +} + +async function waitForFile(file: string) { + for (let attempt = 0; attempt < 600; attempt++) { + if (await Bun.file(file).exists()) return + await Bun.sleep(5) + } + throw new Error(`Timed out waiting for ${file}`) +} + +async function waitForExit(pid: number) { + for (let attempt = 0; attempt < 600; attempt++) { + try { + process.kill(pid, 0) + } catch { + return + } + await Bun.sleep(5) + } + throw new Error(`Timed out waiting for process ${pid}`) +} diff --git a/packages/client/test/promise.test.ts b/packages/client/test/promise.test.ts index 322a39cd6b..d3c966ef9b 100644 --- a/packages/client/test/promise.test.ts +++ b/packages/client/test/promise.test.ts @@ -1,44 +1,212 @@ import { expect, test } from "bun:test" -import { isSessionNotFoundError, isUnauthorizedError, OpenCode } from "../src" +import { isSessionNotFoundError, isUnauthorizedError, OpenCode } from "../src/promise/index" test("exposes every standard HTTP API group", () => { const client = OpenCode.make({ baseUrl: "http://localhost:3000" }) expect(Object.keys(client)).toEqual([ "health", + "server", "location", - "agents", - "sessions", - "messages", - "models", - "providers", - "integrations", - "credentials", - "permissions", - "files", - "commands", - "skills", - "events", - "ptys", - "questions", - "references", - "projectCopies", + "agent", + "plugin", + "session", + "message", + "model", + "generate", + "provider", + "integration", + "mcp", + "credential", + "project", + "form", + "permission", + "file", + "command", + "skill", + "event", + "pty", + "shell", + "question", + "reference", + "projectCopy", + "vcs", + "debug", ]) - expect(Object.keys(client.messages)).toEqual(["list"]) - expect(Object.keys(client.integrations)).toEqual([ - "list", - "get", - "connectKey", - "connectOauth", - "attemptStatus", - "attemptComplete", - "attemptCancel", - ]) - expect(Object.keys(client.files)).toEqual(["list", "find"]) - expect(Object.keys(client.ptys)).toEqual(["list", "create", "get", "update", "remove"]) + expect(Object.keys(client.debug)).toEqual(["location"]) + expect(Object.keys(client.debug.location)).toEqual(["list", "evict"]) + expect(Object.keys(client.message)).toEqual(["list"]) + expect(Object.keys(client.integration)).toEqual(["list", "get", "wellknown", "connect", "oauth", "command"]) + expect(Object.keys(client.integration.wellknown)).toEqual(["add"]) + expect(Object.keys(client.integration.connect)).toEqual(["key"]) + expect(Object.keys(client.integration.oauth)).toEqual(["connect", "status", "complete", "cancel"]) + expect(Object.keys(client.integration.command)).toEqual(["connect", "status", "cancel"]) + expect(Object.keys(client.file)).toEqual(["read", "list", "find"]) + expect(Object.keys(client.vcs)).toEqual(["status", "diff"]) + expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"]) + expect(Object.keys(client.shell)).toEqual(["list", "create", "get", "timeout", "output", "remove"]) + expect(Object.keys(client.project)).toEqual(["list", "current", "directories"]) }) -test("sessions.get returns the wire projection", async () => { +test("server.get uses the public HTTP contract", async () => { + let request: Request | undefined + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async (input) => { + request = input instanceof Request ? input : new Request(input) + return Response.json({ urls: ["http://192.168.1.10:4096"] }) + }, + }) + + expect(await client.server.get()).toEqual({ urls: ["http://192.168.1.10:4096"] }) + expect(request?.method).toBe("GET") + expect(request?.url).toBe("http://localhost:3000/api/server") +}) + +test("experimental wellknown integration add uses the public HTTP contract", async () => { + let request: Request | undefined + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async (input, init) => { + request = input instanceof Request ? input : new Request(input, init) + return new Response(null, { status: 204 }) + }, + }) + + await client.integration.wellknown.add({ + url: "https://example.com", + location: { directory: "/tmp/project" }, + }) + + expect(request?.method).toBe("POST") + expect(request?.url).toBe( + "http://localhost:3000/api/experimental/integration/wellknown?location%5Bdirectory%5D=%2Ftmp%2Fproject", + ) + expect(await request?.json()).toEqual({ url: "https://example.com" }) +}) + +test("health.stop sends exact replacement identity", async () => { + let request: Request | undefined + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async (input, init) => { + request = input instanceof Request ? input : new Request(input, init) + return Response.json({ accepted: true }) + }, + }) + + expect(await client.health.stop({ instanceID: "instance" })).toEqual({ accepted: true }) + expect(request?.method).toBe("POST") + expect(request?.url).toBe("http://localhost:3000/api/service/stop") + expect(await request?.json()).toEqual({ instanceID: "instance" }) +}) + +test("MCP resource catalog uses the public HTTP contract", async () => { + let request: Request | undefined + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async (input) => { + request = input instanceof Request ? input : new Request(input) + return Response.json({ + location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } }, + data: { + resources: [{ server: "docs", name: "Readme", uri: "docs://readme" }], + templates: [{ server: "docs", name: "File", uriTemplate: "docs://{path}" }], + }, + }) + }, + }) + + const result = await client.mcp.resource.catalog({ location: { directory: "/tmp/project" } }) + + expect(result.data.resources[0]?.uri).toBe("docs://readme") + expect(request?.method).toBe("GET") + expect(request?.url).toBe("http://localhost:3000/api/mcp/resource?location%5Bdirectory%5D=%2Ftmp%2Fproject") +}) + +test("file.read returns binary content from the public HTTP contract", async () => { + let request: Request | undefined + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async (input) => { + request = input instanceof Request ? input : new Request(input) + return new Response(new Uint8Array([104, 105])) + }, + }) + + const content = await client.file.read({ + path: "src/a b#c.ts", + location: { directory: "/tmp/project" }, + }) + + expect(Array.from(content)).toEqual([104, 105]) + expect(request?.url).toBe( + "http://localhost:3000/api/fs/read/src/a%20b%23c.ts?location%5Bdirectory%5D=%2Ftmp%2Fproject", + ) +}) + +test("project methods use the public HTTP contract", async () => { + const requests: string[] = [] + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async (input) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url + requests.push(url) + if (url.includes("/directories")) return Response.json([]) + return Response.json({ id: "proj_test", directory: "/tmp/project" }) + }, + }) + + const current = await client.project.current({ location: { workspace: "wrk_test" } }) + const directories = await client.project.directories({ + projectID: current.id, + location: { directory: current.directory }, + }) + + expect(current).toEqual({ id: "proj_test", directory: "/tmp/project" }) + expect(directories).toEqual([]) + expect(requests).toEqual([ + "http://localhost:3000/api/project/current?location%5Bworkspace%5D=wrk_test", + "http://localhost:3000/api/project/proj_test/directories?location%5Bdirectory%5D=%2Ftmp%2Fproject", + ]) +}) + +test("shell list and remove use the public HTTP contract", async () => { + const requests: Array<{ method: string; url: string }> = [] + const shell = { + id: "sh_test", + status: "running", + command: "pwd", + cwd: "/tmp/project", + shell: "/bin/zsh", + file: "/tmp/opencode-shell", + metadata: { sessionID: "ses_test" }, + time: { started: 1_717_171_717_000 }, + } + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async (input, init) => { + const request = input instanceof Request ? input : new Request(input, init) + requests.push({ method: request.method, url: request.url }) + if (request.method === "DELETE") return new Response(null, { status: 204 }) + return Response.json({ + location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } }, + data: [shell], + }) + }, + }) + + const result = await client.shell.list({ location: { directory: "/tmp/project" } }) + await client.shell.remove({ id: shell.id }) + + expect(result.data).toEqual([shell]) + expect(requests).toEqual([ + { method: "GET", url: "http://localhost:3000/api/shell?location%5Bdirectory%5D=%2Ftmp%2Fproject" }, + { method: "DELETE", url: "http://localhost:3000/api/shell/sh_test" }, + ]) +}) + +test("session.get returns the wire projection", async () => { const client = OpenCode.make({ baseUrl: "http://localhost:3000", fetch: async (input) => { @@ -49,43 +217,152 @@ test("sessions.get returns the wire projection", async () => { }, }) - const result = await client.sessions.get({ sessionID: "ses_test" }) + const result = await client.session.get({ sessionID: "ses_test" }) expect(result.time.created).toBe(1_717_171_717_000) }) -test("events.subscribe exposes the Promise event stream wire projection", async () => { +test("session instructions methods use the public HTTP contract", async () => { + const requests: Array<{ method: string; url: string; body?: unknown }> = [] + const instructions = [{ key: "review-notes", value: { text: "Check the diff", priority: 1 } }] + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async (input, init) => { + const request = input instanceof Request ? input : new Request(input, init) + requests.push({ + method: request.method, + url: request.url, + body: request.method === "PUT" ? await request.json() : undefined, + }) + if (request.method === "GET") return Response.json({ data: instructions }) + return new Response(null, { status: 204 }) + }, + }) + + const result = await client.session.instructions.entry.list({ sessionID: "ses_test" }) + await client.session.instructions.entry.put({ + sessionID: "ses_test", + key: "review-notes", + value: instructions[0].value, + }) + await client.session.instructions.entry.remove({ sessionID: "ses_test", key: "review-notes" }) + + expect(result).toEqual(instructions) + expect(requests).toEqual([ + { + method: "GET", + url: "http://localhost:3000/api/session/ses_test/instructions/entries", + body: undefined, + }, + { + method: "PUT", + url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes", + body: { value: { text: "Check the diff", priority: 1 } }, + }, + { + method: "DELETE", + url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes", + body: undefined, + }, + ]) +}) + +test("session.pending.list uses the public HTTP contract", async () => { + const requests: Array<{ method: string; url: string }> = [] + const pending = [ + { + admittedSeq: 3, + id: "msg_pending", + sessionID: "ses_test", + timeCreated: 1_717_171_717_000, + type: "user", + data: { text: "Fix the failing tests" }, + delivery: "steer", + }, + ] + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async (input, init) => { + const request = input instanceof Request ? input : new Request(input, init) + requests.push({ method: request.method, url: request.url }) + return Response.json({ data: pending }) + }, + }) + + const result = await client.session.pending.list({ sessionID: "ses_test" }) + + expect(result).toEqual(pending) + expect(requests).toEqual([{ method: "GET", url: "http://localhost:3000/api/session/ses_test/pending" }]) +}) + +test("event.subscribe exposes the Promise event stream wire projection", async () => { const client = OpenCode.make({ baseUrl: "http://localhost:3000", fetch: async () => new Response( - `: heartbeat\n\ndata: ${JSON.stringify({ id: "evt_connected", type: "server.connected", data: {} })}\n\n` + + `: heartbeat\n\ndata: ${JSON.stringify({ id: "evt_connected", created: 0, type: "server.connected", data: {} })}\n\n` + `data: ${JSON.stringify(modelSwitchedEvent)}\n\n`, { headers: { "content-type": "text/event-stream" } }, ), }) const events = [] - for await (const event of client.events.subscribe()) events.push(event) + for await (const event of client.event.subscribe()) events.push(event) - expect(events).toEqual([{ id: "evt_connected", type: "server.connected", data: {} }, modelSwitchedEvent]) - expect(events[1]?.type === "session.next.model.switched" && events[1].data.timestamp).toBe(1_717_171_717_000) + expect(events).toEqual([{ id: "evt_connected", created: 0, type: "server.connected", data: {} }, modelSwitchedEvent]) + expect(events[1]?.type === "session.model.selected" && events[1].created).toBe(1_717_171_717_000) }) -test("events.subscribe terminates on malformed Promise SSE data", async () => { +test("event.subscribe terminates on malformed Promise SSE data", async () => { const client = OpenCode.make({ baseUrl: "http://localhost:3000", fetch: async () => new Response("data: {not-json}\n\n", { headers: { "content-type": "text/event-stream" } }), }) - await expect(client.events.subscribe()[Symbol.asyncIterator]().next()).rejects.toMatchObject({ + await expect(client.event.subscribe()[Symbol.asyncIterator]().next()).rejects.toMatchObject({ name: "ClientError", reason: "MalformedResponse", }) }) +test("event.subscribe accepts a fragmented SSE event below the size limit", async () => { + const event = { id: "evt_large", type: "test.large", data: { output: "x".repeat(12 * 1024 * 1024) } } + const encoded = new TextEncoder().encode(`data: ${JSON.stringify(event)}\n\n`) + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async () => + new Response( + new ReadableStream({ + start(controller) { + for (let offset = 0; offset < encoded.length; offset += 64 * 1024) { + controller.enqueue(encoded.slice(offset, offset + 64 * 1024)) + } + controller.close() + }, + }), + { headers: { "content-type": "text/event-stream" } }, + ), + }) + + await expect(client.event.subscribe()[Symbol.asyncIterator]().next()).resolves.toEqual({ done: false, value: event }) +}) + +test("event.subscribe rejects an SSE event above the size limit", async () => { + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async () => + new Response(`data: ${JSON.stringify({ output: "x".repeat(16 * 1024 * 1024) })}`, { + headers: { "content-type": "text/event-stream" }, + }), + }) + + await expect(client.event.subscribe()[Symbol.asyncIterator]().next()).rejects.toMatchObject({ + name: "ClientError", + reason: "SseEventTooLarge", + }) +}) + test("session methods use the public HTTP contract", async () => { const requests: Array<{ url: string; init?: RequestInit }> = [] - let historyPage = 0 const client = OpenCode.make({ baseUrl: "http://localhost:3000", fetch: async (input, init) => { @@ -96,13 +373,15 @@ test("session methods use the public HTTP contract", async () => { headers: { "content-type": "text/event-stream" }, }) } - if (url.includes("/history")) { - historyPage++ - return Response.json( - historyPage === 1 ? { data: [modelSwitchedEvent], hasMore: true } : { data: [], hasMore: false }, - ) + if (url.includes("/log")) { + return new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\ndata: ${JSON.stringify(synced)}\n\n`, { + headers: { "content-type": "text/event-stream" }, + }) } if (url.includes("/prompt")) return Response.json(admission) + if (url.includes("/generate")) return Response.json({ data: { text: "A transient answer" } }) + if (url.includes("/synthetic")) return Response.json(syntheticAdmission) + if (url.endsWith("/compact")) return Response.json(compactionAdmission) if (url.includes("/context")) return Response.json({ data: [] }) if (url.includes("/message/")) return Response.json({ data: modelSwitchedMessage }) if (url.endsWith("/api/session/active")) return Response.json({ data: { ses_test: { type: "running" } } }) @@ -112,61 +391,70 @@ test("session methods use the public HTTP contract", async () => { }, }) - const page = await client.sessions.list({ limit: 10, order: "desc" }) - const active = await client.sessions.active() - const created = await client.sessions.create({ location: { directory: "/tmp/project" } }) - await client.sessions.switchAgent({ sessionID: "ses_test", agent: "build" }) - await client.sessions.switchModel({ + const page = await client.session.list({ limit: 10, order: "desc", parentID: null }) + const active = await client.session.active() + const created = await client.session.create({ location: { directory: "/tmp/project" } }) + await client.session.switchAgent({ sessionID: "ses_test", agent: "build" }) + await client.session.switchModel({ sessionID: "ses_test", model: { id: "claude", providerID: "anthropic" }, }) - const admitted = await client.sessions.prompt({ + const admitted = await client.session.prompt({ sessionID: "ses_test", - prompt: { text: "Hello" }, + text: "Hello", resume: false, }) - await client.sessions.compact({ sessionID: "ses_test" }) - await client.sessions.wait({ sessionID: "ses_test" }) - const context = await client.sessions.context({ sessionID: "ses_test" }) - const history = await client.sessions.history({ sessionID: "ses_test", after: 0, limit: 1 }) - const historyAfter = history.data.at(-1)?.durable?.seq - const historyNext = history.hasMore - ? await client.sessions.history({ sessionID: "ses_test", after: historyAfter, limit: 2 }) - : undefined - const events = [] - for await (const event of client.sessions.events({ sessionID: "ses_test", after: 0 })) events.push(event) - await client.sessions.interrupt({ sessionID: "ses_test" }) - const message = await client.sessions.message({ sessionID: "ses_test", messageID: "msg_model" }) + const generated = await client.session.generate({ sessionID: "ses_test", prompt: "Summarize this session" }) + const synthetic = await client.session.synthetic({ + sessionID: "ses_test", + text: "Completed", + delivery: "queue", + resume: false, + }) + await client.session.compact({ sessionID: "ses_test" }) + await client.session.wait({ sessionID: "ses_test" }) + const context = await client.session.context({ sessionID: "ses_test" }) + const log = [] + for await (const item of client.session.log({ sessionID: "ses_test", after: 0 })) log.push(item) + await client.session.interrupt({ sessionID: "ses_test" }) + const message = await client.session.message({ sessionID: "ses_test", messageID: "msg_model" }) expect(page.cursor.next).toBe("next") expect(active).toEqual({ ses_test: { type: "running" } }) expect(created.id).toBe("ses_test") expect(admitted.id).toBe("msg_test") + expect(generated.text).toBe("A transient answer") + expect(synthetic).toMatchObject({ type: "synthetic", data: { text: "Completed" }, delivery: "queue" }) expect(context).toEqual([]) - expect(history).toEqual({ data: [modelSwitchedEvent], hasMore: true }) - expect(historyNext).toEqual({ data: [], hasMore: false }) - expect(events).toEqual([modelSwitchedEvent]) + expect(log).toEqual([modelSwitchedEvent, synced]) expect(message).toEqual(modelSwitchedMessage) expect(requests.map((request) => [request.init?.method, request.url])).toEqual([ - ["GET", "http://localhost:3000/api/session?limit=10&order=desc"], + ["GET", "http://localhost:3000/api/session?limit=10&order=desc&parentID=null"], ["GET", "http://localhost:3000/api/session/active"], ["POST", "http://localhost:3000/api/session"], ["POST", "http://localhost:3000/api/session/ses_test/agent"], ["POST", "http://localhost:3000/api/session/ses_test/model"], ["POST", "http://localhost:3000/api/session/ses_test/prompt"], + ["POST", "http://localhost:3000/api/session/ses_test/generate"], + ["POST", "http://localhost:3000/api/session/ses_test/synthetic"], ["POST", "http://localhost:3000/api/session/ses_test/compact"], ["POST", "http://localhost:3000/api/session/ses_test/wait"], ["GET", "http://localhost:3000/api/session/ses_test/context"], - ["GET", "http://localhost:3000/api/session/ses_test/history?limit=1&after=0"], - ["GET", "http://localhost:3000/api/session/ses_test/history?limit=2&after=1"], - ["GET", "http://localhost:3000/api/session/ses_test/event?after=0"], + ["GET", "http://localhost:3000/api/experimental/session/ses_test/log?after=0"], ["POST", "http://localhost:3000/api/session/ses_test/interrupt"], ["GET", "http://localhost:3000/api/session/ses_test/message/msg_model"], ]) const body = requests.find((request) => request.url.endsWith("/api/session/ses_test/prompt"))?.init?.body if (typeof body !== "string") throw new Error("Expected JSON request body") expect(JSON.parse(body)).toEqual({ - prompt: { text: "Hello" }, + text: "Hello", + resume: false, + }) + const syntheticBody = requests.find((request) => request.url.endsWith("/synthetic"))?.init?.body + if (typeof syntheticBody !== "string") throw new Error("Expected JSON synthetic request body") + expect(JSON.parse(syntheticBody)).toEqual({ + text: "Completed", + delivery: "queue", resume: false, }) }) @@ -179,14 +467,14 @@ test("middleware errors remain declared client errors", async () => { }) try { - await client.sessions.create({}) + await client.session.create({}) throw new Error("Expected request to fail") } catch (error) { expect(isUnauthorizedError(error)).toBe(true) } }) -test("sessions.history decodes SessionNotFoundError", async () => { +test("session.log decodes SessionNotFoundError", async () => { const client = OpenCode.make({ baseUrl: "http://localhost:3000", fetch: async () => @@ -197,7 +485,7 @@ test("sessions.history decodes SessionNotFoundError", async () => { }) try { - await client.sessions.history({ sessionID: "ses_missing" }) + await client.session.log({ sessionID: "ses_missing" })[Symbol.asyncIterator]().next() throw new Error("Expected request to fail") } catch (error) { expect(isSessionNotFoundError(error)).toBe(true) @@ -229,12 +517,35 @@ const admission = { admittedSeq: 0, id: "msg_test", sessionID: "ses_test", - prompt: { text: "Hello" }, + type: "user", + data: { text: "Hello" }, delivery: "steer", timeCreated: 1_717_171_717_000, }, } +const syntheticAdmission = { + data: { + admittedSeq: 1, + id: "msg_synthetic", + sessionID: "ses_test", + type: "synthetic", + data: { text: "Completed" }, + delivery: "queue", + timeCreated: 1_717_171_717_000, + }, +} + +const compactionAdmission = { + data: { + type: "compaction", + admittedSeq: 1, + id: "msg_compaction", + sessionID: "ses_test", + timeCreated: 1_717_171_717_000, + }, +} + const modelSwitchedMessage = { id: "msg_model", type: "model-switched", @@ -242,14 +553,15 @@ const modelSwitchedMessage = { model: { id: "claude", providerID: "anthropic" }, } +const synced = { type: "log.synced", aggregateID: "ses_test", seq: 1 } + const modelSwitchedEvent = { id: "evt_model", - type: "session.next.model.switched", + created: 1_717_171_717_000, + type: "session.model.selected", durable: { aggregateID: "ses_test", seq: 1, version: 1 }, data: { - timestamp: 1_717_171_717_000, sessionID: "ses_test", - messageID: "msg_model", model: { id: "claude", providerID: "anthropic" }, }, } diff --git a/packages/client/test/service.test.ts b/packages/client/test/service.test.ts new file mode 100644 index 0000000000..8e73536bca --- /dev/null +++ b/packages/client/test/service.test.ts @@ -0,0 +1,239 @@ +import { NodeFileSystem } from "@effect/platform-node" +import { afterEach, expect, test } from "bun:test" +import { Effect } from "effect" +import { mkdtemp, rm, writeFile } from "node:fs/promises" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { Service, type EnsureReason } from "../src/effect/service" + +const fixture = join(import.meta.dir, "fixture/service.ts") +const processes: Bun.Subprocess[] = [] +const directories: string[] = [] + +afterEach(async () => { + processes.forEach((process) => process.kill("SIGTERM")) + await Promise.all(processes.splice(0).map((process) => process.exited)) + await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))) +}) + +test("a concurrent same-version start cannot invalidate a resolved endpoint", async () => { + const directory = await temp() + const registration = join(directory, "service.json") + spawn(registration, "modern") + await waitForFile(registration) + const original = await Bun.file(registration).json() + + const starts: EnsureReason[] = [] + const first = run( + Service.ensure({ + file: registration, + version: "test", + command: [], + onStart: (reason) => starts.push(reason), + }), + ) + await waitForFile(registration + ".first-request") + + const resolved = await run(Service.ensure({ file: registration, version: "test" })) + expect(resolved.url).toBe(original.url) + + await writeFile(registration + ".release", "") + await first + + expect(starts).toEqual([]) + expect(await Bun.file(registration).json()).toEqual(original) + expect(await health(resolved.url)).toEqual({ healthy: true, version: "test", pid: original.pid }) +}) + +test("waits for a registered service to finish starting", async () => { + const directory = await temp() + const registration = join(directory, "service.json") + const process = spawn(registration, "starting") + await waitForFile(registration) + const result = run(Service.ensure({ file: registration, version: "test", command: [] })) + + await Bun.sleep(500) + expect(process.exitCode).toBe(null) + await writeFile(registration + ".release", "") + expect((await result).url).toBe((await Bun.file(registration).json()).url) +}) + +test("reports a failed registered service without spawning", async () => { + const directory = await temp() + const registration = join(directory, "service.json") + const process = spawn(registration, "failed-owner") + await waitForFile(registration) + + await expect(run(Service.ensure({ file: registration, version: "test", command: [] }))).rejects.toThrow( + "Background service failed to start", + ) + expect(process.exitCode).toBe(null) +}) + +test("requests graceful stop of the exact service instance", async () => { + const directory = await temp() + const registration = join(directory, "service.json") + const process = spawn(registration, "graceful") + await waitForFile(registration) + const info = await Bun.file(registration).json() + + await run(Service.stop({ file: registration })) + await process.exited + expect(await Bun.file(registration + ".stop").json()).toEqual({ instanceID: info.id }) +}) + +test("does not spawn contenders while an incompatible service rejects replacement", async () => { + const directory = await temp() + const registration = join(directory, "service.json") + const contender = join(directory, "contender.json") + const existing = spawn(registration, "reject-stop") + await waitForFile(registration) + const controller = new AbortController() + const starting = Effect.runPromise( + Service.ensure({ + file: registration, + version: "test", + command: [process.execPath, fixture, contender, "record-start"], + }).pipe(Effect.provide(NodeFileSystem.layer)), + { signal: controller.signal }, + ) + + await waitForFile(registration + ".stop-attempt") + await Bun.sleep(500) + controller.abort() + await starting.catch(() => undefined) + + expect(await Bun.file(contender + ".started").exists()).toBe(false) + expect(existing.exitCode).toBe(null) +}) + +test("a legacy health response is still replaced", async () => { + const directory = await temp() + const registration = join(directory, "service.json") + const existing = spawn(registration, "legacy") + await waitForFile(registration) + + const starts: EnsureReason[] = [] + const result = run(Service.ensure({ file: registration, command: [], onStart: (reason) => starts.push(reason) })) + + await expect(result).rejects.toThrow("Missing service command") + expect(starts).toEqual(["version-mismatch"]) + await existing.exited +}, 10_000) + +test("waits for a slow winner while bounding lock probes", async () => { + const directory = await temp() + const registration = join(directory, "service.json") + const endpoint = await run( + Service.ensure({ + file: registration, + version: "test", + command: [process.execPath, fixture, registration, "coordinated"], + }), + ) + const info = await Bun.file(registration).json() + try { + expect(endpoint.url).toBe(info.url) + expect(await health(endpoint.url)).toEqual({ healthy: true, version: "test", pid: info.pid }) + expect((await Bun.file(registration + ".starts").text()).trim().split("\n")).toHaveLength(2) + } finally { + process.kill(info.pid, "SIGTERM") + } +}, 15_000) + +test("reports a contender that fails to start", async () => { + const directory = await temp() + const registration = join(directory, "service.json") + await expect( + run( + Service.ensure({ + file: registration, + version: "test", + command: [process.execPath, fixture, registration, "failed"], + }), + ), + ).rejects.toThrow("Server process exited with code 1") +}, 10_000) + +test("reports a contender terminated by a signal", async () => { + const directory = await temp() + const registration = join(directory, "service.json") + await expect( + run( + Service.ensure({ + file: registration, + version: "test", + command: [process.execPath, fixture, registration, "signal"], + }), + ), + ).rejects.toThrow(/Server process (terminated by|exited with code)/) +}, 10_000) + +test("reports a slow contender that eventually fails", async () => { + const directory = await temp() + const registration = join(directory, "service.json") + await expect( + run( + Service.ensure({ + file: registration, + version: "test", + command: [process.execPath, fixture, registration, "delayed-failed", "8000"], + }), + ), + ).rejects.toThrow("Server process exited with code 1") +}, 15_000) + +test("replaces an incompatible owner that appears during startup", async () => { + const directory = await temp() + const registration = join(directory, "service.json") + const starting = run( + Service.ensure({ + file: registration, + version: "test", + command: [process.execPath, fixture, registration, "delayed", "8000"], + }), + ) + await Bun.sleep(1_000) + const old = spawn(registration, "old") + await waitForFile(registration) + const endpoint = await starting + const info = await Bun.file(registration).json() + try { + expect(endpoint.url).toBe(info.url) + expect(info.version).toBe("test") + await old.exited + } finally { + process.kill(info.pid, "SIGTERM") + } +}, 20_000) + +function run(effect: Effect.Effect) { + return Effect.runPromise(effect.pipe(Effect.provide(NodeFileSystem.layer))) +} + +function spawn(registration: string, mode: string, ...args: string[]) { + const subprocess = Bun.spawn([process.execPath, fixture, registration, mode, ...args], { + stdout: "ignore", + stderr: "inherit", + }) + processes.push(subprocess) + return subprocess +} + +async function temp() { + const directory = await mkdtemp(join(tmpdir(), "opencode-client-service-")) + directories.push(directory) + return directory +} + +async function waitForFile(file: string) { + for (let attempt = 0; attempt < 600; attempt++) { + if (await Bun.file(file).exists()) return + await Bun.sleep(5) + } + throw new Error(`Timed out waiting for ${file}`) +} + +async function health(url: string) { + return fetch(new URL("/api/health", url), { signal: AbortSignal.timeout(1_000) }).then((response) => response.json()) +} diff --git a/packages/client/tsconfig.build.json b/packages/client/tsconfig.build.json new file mode 100644 index 0000000000..e235ae78cf --- /dev/null +++ b/packages/client/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "noEmit": false, + "declaration": true + }, + "include": ["src"] +} diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 47fc90bc55..a7ef7a1fa3 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -3,6 +3,8 @@ "extends": "@tsconfig/bun/tsconfig.json", "compilerOptions": { "lib": ["ESNext", "DOM", "DOM.Iterable"], + "allowImportingTsExtensions": false, + "allowJs": false, "noUncheckedIndexedAccess": false }, "include": ["src"] diff --git a/packages/codemode/AGENTS.md b/packages/codemode/AGENTS.md index df812c6d86..cbea866d4e 100644 --- a/packages/codemode/AGENTS.md +++ b/packages/codemode/AGENTS.md @@ -4,6 +4,7 @@ - Do not add a speculative generic permission or approval policy. A host omits tools it does not expose and enforces domain authorization inside each provided tool. - Keep Code Mode unaware of host session, channel, and conversation models. The hosting application supplies trusted execution scope around it. - Tool schemas are the model-facing Interface. Keep arguments minimal and natural to the operation; never add unrelated IDs as ambient capability tokens. +- When interpreter behavior or support changes, update `interpreter-support.md` and direct tests in the same PR. ## OpenAPI @@ -15,8 +16,8 @@ ## Future Design Notes -- If a captured user-visible output channel returns (an earlier `output.text`/`output.file`/`output.image` API was removed from v1), keep `output` as its name, distinct from the program return value: `return` stays the structured result for the model, while `output.*` describes artifacts the host may render into a conversation or UI after execution. Keep this host-neutral and let applications decide how captured output is delivered. In v1, hosts collect media host-side (outside the sandbox) instead. -- Improve the sandbox failure taxonomy. Distinguish parse/compile mistakes, unsupported syntax, user-thrown errors, invalid returned data, tool refusal, tool internal failure, timeout, and genuine runtime defects so agents can recover accurately instead of treating everything as a generic execution failure. +- If a captured user-visible output channel returns (an earlier `output.text`/`output.file`/`output.image` API was removed from v1), keep `output` as its name, distinct from the program return value: `return` stays the structured result for the model, while `output.*` describes artifacts the host may render into a conversation or UI after execution. Keep this host-neutral and let applications decide how captured output is delivered. In v1, hosts collect media host-side (outside CodeMode) instead. +- Improve the failure taxonomy. Distinguish parse/compile mistakes, unsupported syntax, user-thrown errors, invalid returned data, tool refusal, tool internal failure, timeout, and genuine runtime defects so agents can recover accurately instead of treating everything as a generic execution failure. - Preserve the public/private error split. Tool authors should be able to return a safe model-visible message while retaining a private cause for host diagnostics. Unknown host failures must remain sanitized by default. - Think deliberately about richer binary boundaries before allowing `Blob`, `File`, `ArrayBuffer`, streams, or typed arrays beyond today's JSON-like values. If CodeMode supports binary tool args/results, use explicit tagged data shapes and clear size limits rather than relying on ambient runtime serialization. - Keep host capabilities explicit. Globals such as `fetch`, `crypto`, filesystem handles, extra modules, or network clients should be opt-in runtime capabilities with obvious policy defaults, not ambient authority. Default to unavailable unless a host deliberately provides the capability. diff --git a/packages/codemode/README.md b/packages/codemode/README.md index 37bde2d869..622fcaa574 100644 --- a/packages/codemode/README.md +++ b/packages/codemode/README.md @@ -1,37 +1,40 @@ # @opencode-ai/codemode -Effect-native confined code execution over explicit, schema-described tools. +This is our take on code mode. Programs are written in a lightweight, JavaScript-like DSL and run in the package's +own interpreter. They never execute as actual JavaScript, so there is no runtime to escape into. The interpreter +itself can reach nothing; every effect a program has goes through a tool you explicitly supplied. The tradeoff is a +bounded language rather than full JavaScript: the [interpreter support checklist](./interpreter-support.md) documents +exactly what is supported. -CodeMode lets a model write a small JavaScript program that can call only the tools supplied by the host. The program can sequence calls, transform plain data, branch, loop, and run independent calls in parallel without receiving ambient filesystem, process, network, module, or application authority. +[Cloudflare's post](https://blog.cloudflare.com/code-mode/) introduced the idea. Their implementation executes +generated code in isolate sandboxes. We took a lighter route: a pure interpreter that runs wherever your application +runs, no sandbox required. -The package is currently private to this workspace. Its API is designed around one-shot and reusable execution: +## How it differs from JavaScript -```ts -// One execution -yield * CodeMode.execute({ tools, code }) +The deliberate differences: -// A reusable runtime -const runtime = CodeMode.make({ tools, limits }) -yield * runtime.execute(code) -``` +- **No ambient authority.** No `fetch`, `process`, filesystem, timers, or host globals - only the allowlisted standard + library and supplied `tools`. +- **No dynamic code.** No `eval`, `Function`, or module loading. +- **Plain-data boundaries.** Tool arguments and program results are JSON-like data. Dates become ISO strings, RegExp, + Map, and Set serialize as `{}`, and promises, functions, and runtime references cannot cross the boundary. +- **Eager, supervised promises.** Tool calls and async functions start immediately when called. Whatever is still + running when the program returns is interrupted - race losers and fire-and-forget calls alike - so a program must + await every call whose completion matters. Rejections that settle un-awaited become `warnings` on the result instead + of crashing the run. +- **REPL-style results.** An omitted `return` yields the final top-level expression; `undefined` normalizes to `null`. -## Install - -Within this workspace: - -```json -{ - "dependencies": { - "@opencode-ai/codemode": "workspace:*" - } -} -``` - -Hosts interact with CodeMode through `effect` (tool `run` implementations, `Effect`-typed results), so they should depend on `effect` themselves. +Beyond these, the language is a growing subset rather than a divergent one: unsupported syntax returns an +`UnsupportedSyntax` diagnostic with a source location, and current gaps (for example thenable assimilation, classes, +generators, and full sparse-array parity) are tracked as unchecked items in the +[interpreter support checklist](./interpreter-support.md). ## Quick Start -Define tools with Effect Schema, then place them in the object tree exposed to programs as `tools`: +The package is workspace-private (`"@opencode-ai/codemode": "workspace:*"`). Hosts interact with it through `effect` +and should depend on `effect` themselves. Define tools with Effect Schema, then expose them to programs through +`tools`: ```ts import { CodeMode, Tool } from "@opencode-ai/codemode" @@ -60,69 +63,59 @@ const result = `) ``` -`result` is always a `CodeMode.Result`. Program, validation, limit, and tool failures are returned as diagnostics rather than failing the Effect. Host interruption remains interruption. - -Successful result values are JSON-safe data. A program that returns `undefined`, including by reaching the end without `return`, produces `null`; nested `undefined` values are normalized to `null` as well. +`result` is always a `CodeMode.Result`. Program, validation, limit, and tool failures are returned as diagnostics +rather than failing the Effect; host interruption remains interruption. ## API ### `Tool.make` -```ts -const tool = Tool.make({ - description, - input, // Effect Schema (validating) or JSON Schema (render-only) - output, // optional; same choice - run, -}) -``` +`input` and `output` each accept a validating Effect Schema or a render-only JSON Schema document. Effect Schema input +is decoded before `run` is invoked; an Effect Schema `output` is decoded and copied before the program sees it. JSON +Schemas only shape the model-visible signature. Without `output` the signature advertises `Promise`. +Descriptions and schemas are model-visible contract; keep authorization in `run`. -`input` and `output` each accept a validating Effect Schema or a render-only JSON Schema document (the natural shape for adapter-provided tools whose schemas arrive as JSON Schema, e.g. MCP definitions). Effect Schema input is decoded before `run` is invoked, and `run` returns the encoded representation of an Effect Schema `output`, which CodeMode decodes and copies before exposing it to the program. JSON Schemas only shape the model-visible signature; values pass through unvalidated (they still cross the plain-data boundary). +Dots in tool names are namespace separators: `{ "issues.list": tool }` exposes `tools.issues.list(...)`, exactly like +`{ issues: { list: tool } }`. Other non-identifier characters render with bracket notation, e.g. +`tools.context7["resolve-library-id"](...)`. -`output` is optional. Without it the tool's signature advertises `Promise` and the host result is exposed as-is. +### `CodeMode.execute` and `CodeMode.make` -The description and schemas are part of the model-visible tool contract. Keep descriptions concrete and put authorization in `run` or in the service it calls. - -Public tool types are grouped under the same namespace: `Tool.Definition`, `Tool.Options`, `Tool.SchemaType`, and `Tool.JsonSchema`. - -### `CodeMode.execute` - -Use `CodeMode.execute` for a single execution: +`CodeMode.execute({ ...options, code })` runs once and is equivalent to `CodeMode.make(options).execute(code)`. A +runtime from `make` reuses the tool set and policy: ```ts -const result = - yield * - CodeMode.execute({ - tools: { orders: { lookup: lookupOrder } }, - code: `return await tools.orders.lookup({ id: "order_42" })`, - limits: { maxToolCalls: 10 }, - onToolCallStart: (call) => Effect.logDebug("CodeMode tool started", call), - onToolCallEnd: (call) => Effect.logDebug("CodeMode tool settled", call), - }) -``` - -The Effect environment is inferred from the supplied tools. CodeMode does not erase service requirements introduced by tool implementations. - -### `CodeMode.make` - -Use `CodeMode.make` when the tool set and execution policy are reused: - -```ts -const runtime = CodeMode.make({ - tools: { orders: { lookup: lookupOrder } }, - limits: { timeoutMs: 30_000 }, -}) +const runtime = CodeMode.make({ tools, limits: { timeoutMs: 30_000 } }) runtime.catalog() // structured tool descriptions runtime.instructions() // model-facing syntax and tool guide runtime.execute(source) // CodeMode.Result ``` -`CodeMode.Input`, `CodeMode.Result`, `CodeMode.Success`, `CodeMode.Failure`, `CodeMode.Diagnostic`, and `CodeMode.DiagnosticKind` are both Effect schemas and their inferred TypeScript types. Hosts can combine `CodeMode.Input` and `CodeMode.Result` with `runtime.instructions()` and `runtime.execute()` when constructing a framework-specific agent tool. +The Effect environment is inferred from the supplied tools; service requirements are not erased. Optional +`onToolCallStart` / `onToolCallEnd` hooks observe admitted calls with decoded input, outcome, and duration; both are +Effect-returning and must not fail. -All other CodeMode types use the same namespace: `CodeMode.Options`, `CodeMode.ExecuteOptions`, `CodeMode.Runtime`, `CodeMode.ExecutionLimits`, `CodeMode.DiscoveryOptions`, `CodeMode.DataValue`, `CodeMode.ToolDescription`, and the `CodeMode.ToolCall*` observation types. +### OpenAPI tools -### Results +`OpenAPI.fromSpec` turns an OpenAPI 3.x document into namespaced tools - one tool per operation, using dotted +`operationId` segments as namespaces: + +```ts +const api = OpenAPI.fromSpec({ spec, auth: { resolve } }) +const runtime = CodeMode.make({ tools: { opencode: api.tools } }) +``` + +It is synchronous and returns `{ tools, skipped }`: operations with unsupported encodings, non-JSON bodies, binary +responses, or streaming land in `skipped` instead of producing broken tools. Auth is resolved host-side and never +model-visible; generated tools require `HttpClient.HttpClient` in the environment. `readOnly` properties are omitted +from request signatures and `writeOnly` properties from response signatures. These JSON Schemas are model-facing, not +runtime filters: nested value bodies and server responses pass through unchanged. See the option docstrings in +`src/openapi/types.ts` for full semantics. + +## Outputs + +Every execution returns a `CodeMode.Result`: ```ts type Result = Success | Failure @@ -130,6 +123,7 @@ type Result = Success | Failure interface Success { readonly ok: true readonly value: CodeMode.DataValue + readonly warnings?: ReadonlyArray readonly logs?: ReadonlyArray readonly truncated?: boolean readonly toolCalls: ReadonlyArray @@ -144,218 +138,65 @@ interface Failure { } ``` -`toolCalls` contains the names of calls admitted by the runtime in call order. It is retained on failure so hosts can audit partial execution without exposing inputs or host failures. `truncated` is present when the value or logs were cut to fit `maxOutputBytes` (see Execution Limits). +`value` is JSON-safe data. `warnings` are non-fatal diagnostics alongside a valid value (un-awaited rejections, +timeout cleanup after the return). `logs` holds program console output, `truncated` marks any output-budget cut, and +`toolCalls` lists admitted calls in order - retained on failure for auditing. -### Tool-call hooks +Failure `error` and success `warnings` share one diagnostic vocabulary: -`onToolCallStart` receives `{ index, name, input }` after input decoding and before tool execution. The input is decoded host-side data and may include values produced by schema transformations; applications should avoid logging sensitive tool arguments indiscriminately. +| Kind | Meaning | +| ----------------------- | --------------------------------------------------------------------------------------------------------- | +| `ParseError` | Source is empty or cannot be parsed. | +| `UnsupportedSyntax` | Parsed JavaScript is outside the supported subset. | +| `UnknownTool` | A program referenced a tool the host did not provide. | +| `InvalidToolInput` | Tool input failed schema decoding or safe-data copying. | +| `InvalidToolOutput` | Tool output failed schema decoding or safe-data copying. | +| `InvalidDataValue` | Program data violated the plain-data contract (depth, circularity, blocked properties, non-data values). | +| `ToolCallLimitExceeded` | Calls exceeded `maxToolCalls`. | +| `TimeoutExceeded` | Execution exceeded `timeoutMs`; as a warning, background work was interrupted after the program returned. | +| `ToolFailure` | A tool refused or failed. | +| `ExecutionFailure` | The program threw or another execution error occurred. | +| `Truncated` | Warning-only marker: additional warnings were omitted by `maxOutputBytes`. | -`onToolCallEnd` receives `{ index, name, input, durationMs, outcome, message? }` when an admitted call settles. `outcome` is `"success"` or `"failure"`; `message` is the model-safe failure message and is present only on failure. Interrupted calls (for example when the execution timeout fires) do not produce an end event. Both hooks are Effect-returning and must not fail. - -### OpenAPI tools - -`OpenAPI.fromSpec` turns an OpenAPI 3.x document into a tool subtree - one tool per operation. Dotted `operationId` values form namespaces such as `v2.session.get`. Missing IDs receive a flat method/path fallback such as `getUsersById`; names are sanitized and deduplicated. The host places the subtree under a key in its `tools` tree; that key is the model-visible namespace. - -```ts -import { CodeMode, OpenAPI } from "@opencode-ai/codemode" -import { Effect } from "effect" -import { FetchHttpClient } from "effect/unstable/http" - -const api = OpenAPI.fromSpec({ - spec: await Bun.file("openapi.json").json(), // parsed document (no YAML) - auth: { - resolve: ({ name, scopes, operation }) => - name === "BearerAuth" ? Effect.succeed({ type: "bearer", token }) : Effect.succeed(undefined), - }, -}) - -const runtime = CodeMode.make({ tools: { opencode: api.tools } }) -const result = await Effect.runPromise(runtime.execute(code).pipe(Effect.provide(FetchHttpClient.layer))) -``` - -`fromSpec` is synchronous and returns `{ tools, skipped }`. The initial adapter supports query `form`/`deepObject`, path/header `simple`, JSON request bodies, JSON responses, and text responses; unsupported parameter encodings, non-JSON request bodies, binary responses, and streaming operations land in `skipped` instead of producing broken tools. Operation and path servers take precedence over document servers unless `baseUrl` explicitly overrides all of them. Tool inputs flatten path, query, header, and closed object-body fields into one model-facing object while retaining their HTTP locations internally. Cross-location name collisions receive a location prefix such as `path_id` and `query_id`; composed, nullable, dictionary, conditionally-required, and non-object JSON bodies remain under `body`. Auth is never model-visible. Responses are limited to 50 MiB, and non-2xx responses become safe tool failures carrying the status and a size-capped body summary. Deferred capabilities are tracked in `src/openapi/TODO.md`. - -Supported bearer, basic, header, and query authentication follows OpenAPI `security` semantics and is resolved host-side via `auth.resolve` - credential storage, OAuth flows, and token refresh never enter the compiler. Cookie authentication alternatives are discarded; an operation is skipped when it has no supported alternative. See the option docstrings in `src/openapi/types.ts` for the full semantics. Generated tools require `HttpClient.HttpClient` (from `effect/unstable/http`) in the Effect environment - provide `FetchHttpClient.layer` or a custom/test client layer at execution. The supplied client owns redirect policy; credentialed hosts should reject redirects or strip credentials when the origin changes. +Unknown host failures, defects, and invalid outputs are sanitized. `toolError("safe message")` is the explicit channel +for a model-visible refusal; its optional cause never crosses the boundary. ## Discovery -The agent-tool instructions use a budgeted catalog. Every tool namespace is always listed with its tool count regardless of budget, and as many complete, JSDoc-annotated tool signatures (each with a one-line description) as fit an estimated-token budget are inlined. Schema field descriptions and tags are part of each signature's measured cost. Selection is round-robin across namespaces for fairness: in each round (namespaces alphabetical), every namespace still holding un-inlined tools attempts to place its next-cheapest signature against the shared budget, and a namespace whose next signature does not fit drops out while the others keep going - so every namespace gets some representation before any namespace gets everything. The instructions state exactly how comprehensive the list is, both overall (`COMPLETE list` vs `PARTIAL - N of M shown`) and per namespace (`(3 tools)`, `(3 tools, 1 shown)`, `(3 tools, none shown)`). - -The catalog-entry budget defaults to 2,000 estimated tokens (characters / 4, the same heuristic OpenCode uses). It applies only to full tool entries shown in the catalog; fixed instructions and namespace summaries are not counted. Override it when constructing a runtime: - -```ts -const runtime = CodeMode.make({ - tools, - discovery: { catalogBudget: 6_000 }, -}) -``` - -The budget must be a non-negative safe integer. - -The runtime search tool is always registered - including when the catalog is fully inlined - so a speculative `tools.$codemode.search` call never fails as an unknown tool. It is only advertised in the instructions when the inlined list is partial: - -```ts -const matches = await tools.$codemode.search({ - query: "order status", - namespace: "orders", // optional: scope to one top-level namespace - limit: 10, - offset: 0, -}) -``` - -`search` performs deterministic, additive field-weighted matching. The query is tokenized (camelCase boundaries split; every non-alphanumeric character is a separator; empties and `*` are dropped), and each term scores every tool: exact path or path-segment match (20), path substring (8), description substring (4), and searchable-text substring (2). Each term also carries naive singular variants (trailing `s`/`es` stripped), and a field check passes when the term or any variant matches - so a plural query term (`issues`) still finds a tool whose text only says `issue`, without changing the weights. The searchable text also includes the input schema's property names and their description strings, so a query naming a parameter finds its tool, and substring matching means partial words match. Scores sum across terms; matches are sorted by score (ties broken alphabetically by path), then sliced from the zero-based `offset` (default 0) to the configured `limit` (default 10). `remaining` counts matches after the current page. `next` is `{ offset }` when another page exists and `null` on the final page; spread it into the original request to preserve its query, namespace, and limit. - -```ts -const request = { query: "order status", namespace: "orders", limit: 10 } -const page = await tools.$codemode.search(request) -const nextPage = page.next ? await tools.$codemode.search({ ...request, ...page.next }) : undefined -``` - -Each result contains the path, description, and the same generated TypeScript signature used by the inline catalog, so no second lookup is needed. Signatures use the JSDoc-annotated multiline form: each described input/output field carries its schema `description` as a `/** ... */` comment, and constraints TypeScript cannot express ride along as tags (`@deprecated`, `@default`, `@format`, `@minItems`, `@maxItems`). - -```ts -tools.github.list_issues(input: { - /** Repository owner */ - owner: string, - /** Cursor from the previous response's pageInfo */ - after?: string, - /** - * Results per page - * @default 30 - */ - perPage?: number, -}): Promise -``` - -Result paths are rendered as JavaScript expressions rooted at `tools` (`tools.orders.lookup`, or `tools.context7["resolve-library-id"]` for non-identifier segments), so each `path` is directly usable as the call site. An empty query browses the catalog alphabetically by path; combined with `namespace` (`{ query: "", namespace: "orders" }`) it lists everything in that namespace. A query that names one tool path exactly (canonical path, `tools.`-prefixed path, or rendered JavaScript expression) is treated as a lookup and returns that tool alone. - -The instructions are structured markdown, ordered so the workflow sits at the top and the catalog at the bottom: a `## Workflow` section with numbered steps (find a tool via search when the catalog is partial, or pick from the inlined list when it is complete; call the exact path as-is; return only the needed fields), a `## Rules` section holding only guidance the workflow does not already cover (only listed/search-result Code Mode tools and internal runtime tools exist inside `tools`; filter and aggregate collections in code; narrow `Promise` results at runtime; run independent calls through `Promise.all`; enumerate `tools` with `Object.keys`/`for...in`; browse a namespace and paginate search results when search is advertised), a short `## Language` section that identifies the runtime as a restricted JavaScript orchestration language and names its major unavailable capabilities, and the budgeted `## Available tools` catalog. Example call forms use explicit `.`/`` placeholders - never a real or fabricated tool name. - -A host cannot define its own `$codemode` top-level namespace. - -## Supported Programs - -CodeMode executes a deliberately bounded JavaScript subset. It supports: - -- Plain data literals, property access, assignment, and destructuring. -- `if`, conditional expressions, `switch`, `for`, `for...of` (arrays, strings, Maps, Sets), `for...in` (own keys of plain objects, index strings of arrays, and namespace/tool names of `tools` references - anything else is an error suggesting `for...of` or `Object.keys`, rather than real JS's surprising behavior of indices for strings and zero iterations for Maps/Sets), `while`, and `do...while`. -- Arrow functions and function declarations with closures, defaults, rest parameters, and destructuring. -- Optional chaining, nullish coalescing, templates, spread (arrays, strings, Maps, Sets), and `try`/`catch`. -- Common array, string, number, `Object`, `Math`, and `JSON` operations. Mutating array methods include `push`/`pop`/`shift`/`unshift`/`splice` (removes in place and returns the removed elements)/`fill`/`copyWithin`; array `keys`/`values`/`entries` return **arrays** (matching the Map/Set convention) and work with `for...of` and spread. String methods include `localeCompare` (locale/options arguments ignored), `normalize`, and the `trimLeft`/`trimRight` aliases. `Object.keys` also accepts arrays (index strings, as in JS) and tool references: `Object.keys(tools)` lists the top-level namespaces, including `$codemode`, and `Object.keys(tools.ns)` lists the names at that node (a callable tool enumerates as `[]`; an unknown path is an `UnknownTool` diagnostic). `Object.values`/`Object.entries` on a tool reference fail with a pointer at `Object.keys(tools)` and `tools.$codemode.search`. -- `Date` - `Date.now()`/`Date.parse()`/`Date.UTC()`, `new Date(...)`, the getter methods, and date arithmetic/comparison via the time value. Dates stringify as ISO (`toString` included, for determinism across host timezones). -- Regular expressions - `/literals/` and `new RegExp(...)` with `test`/`exec` (stateful `lastIndex` for `g`), plus string `match`/`matchAll`/`replace`/`replaceAll`/`split`/`search` with patterns. Match results are arrays carrying `index` and named `groups` as own properties (`input` is omitted). `replace` and `replaceAll` accept function replacers with captures, offset, input, and named groups; callbacks run sequentially, may await tool calls, and have their results coerced to strings. Invalid patterns, invalid flags, and missing-`g` calls fail with catchable errors that say what was wrong and how to fix it (escaping hints, the exact `/pattern/g` to write). Patterns run on the host engine, so pathological backtracking is bounded only by the execution timeout. -- `Map` and `Set` - construction from entries/arrays/strings, `get`/`set`/`add`/`has`/`delete`/`clear`/`size`/`forEach`, and `keys`/`values`/`entries` returning **arrays** (not iterators). -- URL helpers - `URL` resolution and mutation, linked `URLSearchParams`, `URL.canParse`/`URL.parse`, URI and URI-component encoding/decoding, and query parameter construction, lookup, mutation, sorting, callbacks, and materialization. URLSearchParams iteration methods return arrays, matching the Map/Set convention. -- First-class promises - an un-awaited `tools.ns.tool(...)` is a promise value whose call starts immediately on a supervised fiber; `await` resolves it (awaiting a non-promise value is a no-op, and `return tools.ns.tool(...)` resolves like an async-function return). `Promise.all`, `Promise.allSettled`, and `Promise.race` accept any array mixing promises and plain values (built inline, beforehand, or via spread); `Promise.resolve`/`Promise.reject` construct settled promises. `Promise.allSettled` rejection reasons are the same plain `{ name?, message }` data a `catch` binding sees, and `Promise.race` interrupts its losing in-flight calls. At most 8 tool calls run concurrently. When a program completes, still-running un-awaited calls are awaited before the execution ends; a failure from a call that was never awaited surfaces as an unhandled-rejection diagnostic. -- `throw value` and `throw new Error(message)` for explicit program failure. `Error` (and `TypeError`/`RangeError`/`SyntaxError`/`ReferenceError`/`EvalError`/`URIError`) are real constructors, callable with or without `new`; error values are plain `{ name, message }` data that additionally satisfy `instanceof Error` (a specific type matches itself and `Error`, as in JS). Every caught failure - thrown errors, interpreter runtime errors, and tool failures - is `instanceof Error` in a `catch` block; a thrown non-error value (`throw "text"`) is not, matching JS. Caught failures carry the `name` the equivalent real-JS failure would have - `JSON.parse` and invalid regex patterns produce a `SyntaxError` (satisfying `instanceof SyntaxError`), an unknown identifier a `ReferenceError`, assigning to a constant a `TypeError`, a bad `normalize` form a `RangeError`; failures with no specific analogue (including tool failures) are named `"Error"`. `instanceof` also recognizes `Date`, `RegExp`, `Map`, `Set`, `URL`, `URLSearchParams`, `Array`, `Object`, and `Promise`; any other right-hand side is a catchable error. - -Inside a program, standard-library values stay live everywhere: the internal data checkpoints (`Object.*` helpers, spread, coercion inputs) preserve the instances, so `Object.values({ d: date })[0].getTime()` and a spread copy of an object holding a Map keep working. Only at the host boundary (final result, tool arguments, `JSON.stringify`) do they serialize exactly as `JSON.stringify` would: Date and URL become strings (an invalid Date becomes `null`), while RegExp, Map, Set, and URLSearchParams become `{}`. Promise values never cross a data boundary: an un-awaited promise in a result or tool argument produces a diagnostic that says to await it, instead of serializing to `{}`. - -It does not expose `eval`, dynamic imports, modules, classes, generators, timers, host globals, prototype mutation, custom promise constructors (`new Promise`), promise chaining (`.then`/`.catch`/`.finally` - `await` with `try`/`catch` is the supported style), or arbitrary method calls. Unsupported syntax returns an `UnsupportedSyntax` diagnostic with a source location when available. - -CodeMode is an orchestration language, not a general JavaScript runtime. +The generated instructions inline a budgeted catalog (default 2,000 estimated tokens, override with +`discovery: { catalogBudget }`): every namespace is always listed with its tool count, signatures are selected +round-robin so every namespace gets representation, and the instructions state whether the list is complete or +partial. Programs also get a global `search(...)` built-in - always available, advertised when the list is partial: +synchronous, deterministic field-weighted substring matching that returns directly callable paths with full +signatures, supports namespace scoping and pagination, and treats an empty query as browsing and an exact path as +lookup. Search counts as an admitted tool call. ## Execution Limits -The limits are exactly three knobs: +| Limit | Default | Bounds | +| ---------------- | -------------------: | ---------------------------------------------------- | +| `timeoutMs` | none - no timeout | Wall-clock execution time. | +| `maxToolCalls` | none - unlimited | Tool calls admitted during the execution. | +| `maxOutputBytes` | none - no truncation | Retained result value and logs; warnings separately. | -| Limit | Default | Bounds | -| ---------------- | -------------------: | -------------------------------------------------------------------- | -| `timeoutMs` | none - no timeout | Wall-clock execution time. | -| `maxToolCalls` | none - unlimited | Tool calls admitted during the execution. | -| `maxOutputBytes` | none - no truncation | Model-facing output: the serialized result value plus captured logs. | +No limit has a default, on purpose: execution budgets are host policy. A host without its own truncation or +interruption should set `maxOutputBytes` and `timeoutMs`. Limits are safe integers; invalid configuration throws a +`RangeError` at construction. Exceeding `maxOutputBytes` never fails the execution - oversized output is truncated +with an in-band marker. The timeout interrupts in-flight tool fibers and pure busy loops alike; a value the program +already returned survives a cleanup timeout as a success with a `TimeoutExceeded` warning. CodeMode does not limit +tool-call concurrency. Data nesting at boundaries is limited to 32 levels. -No limit has a default, on purpose: execution budgets are host policy, not library policy - a host that wants a bound sets one; a host that can interrupt the execution fiber (as OpenCode does on user cancel) may set no timeout, and a host with its own tool-output truncation (as OpenCode has) may leave `maxOutputBytes` unset. A host with neither should set `maxOutputBytes`, or oversized results silently flood model context. +## Boundaries and Non-Goals -Pass only the overrides you need: +The host owns authentication, authorization, tool selection, credentials, persistence, approval, and logging policy. +CodeMode owns interpretation, schema and plain-data boundaries, resource limits, diagnostics, and discovery. A program +can only exercise authority already present in the supplied tools - do not expose a broad tool and expect the prompt +to restrict it. -```ts -const runtime = CodeMode.make({ - tools, - limits: { - maxToolCalls: 20, - timeoutMs: 60_000, - }, -}) -``` - -Limits are safe integers. `timeoutMs` must be at least `1`; the others may be `0`. Invalid configuration throws a `RangeError` when `CodeMode.make` or `CodeMode.execute` is called. An explicitly `undefined` value is the same as leaving the limit unset. - -Exceeding a configured `maxOutputBytes` never fails the execution. An oversized result value is replaced by its truncated serialized text plus an explanatory marker, logs are kept from the start until the remaining budget is exhausted (with a final marker line noting the cut), and the result carries `truncated: true`. - -When configured, the timeout interrupts in-flight tool Effects, including eagerly started calls the program has not awaited (their fibers are supervised by the execution). The interpreter yields cooperatively between steps, so the timeout also interrupts pure busy loops (`while (true) {}`) - no separate work budget exists. Tool implementations remain responsible for making their external operations interruptible or independently bounded. - -Two interpreter internals are fixed constants rather than knobs: at most 8 tool calls run concurrently, and values crossing a data boundary may nest at most 32 levels deep (deeper values fail as `InvalidDataValue`, which reads better than a native stack-overflow error). Neither is part of the public contract. - -## Diagnostics - -Failures are data: - -| Kind | Meaning | -| ----------------------- | -------------------------------------------------------------------------------------------------------- | -| `ParseError` | Source is empty or cannot be parsed. | -| `UnsupportedSyntax` | Parsed JavaScript is outside the supported subset. | -| `UnknownTool` | A program referenced a tool the host did not provide. | -| `InvalidToolInput` | Tool input failed schema decoding or safe-data copying. | -| `InvalidToolOutput` | Tool output failed schema decoding or safe-data copying. | -| `InvalidDataValue` | Program data violated the plain-data contract (depth, circularity, blocked properties, non-data values). | -| `ToolCallLimitExceeded` | Calls exceeded `maxToolCalls`. | -| `TimeoutExceeded` | Execution exceeded `timeoutMs`. | -| `ToolFailure` | A tool refused or failed. | -| `ExecutionFailure` | The program threw or another execution error occurred. | - -Unknown host failures, defects, invalid outputs, and copying failures are sanitized. To return a safe operational refusal, fail with `toolError`: - -```ts -import { toolError } from "@opencode-ai/codemode" - -run: ({ id }) => (authorized(id) ? loadOrder(id) : Effect.fail(toolError("Order is unavailable"))) -``` - -Only the supplied message is model-visible. The optional cause is never returned in `CodeMode.Result`; hosts should perform any required internal logging before crossing this boundary. - -## Authority Boundary - -CodeMode confines programs to the supplied tool tree, but it does not decide what those tools may do. - -The host owns: - -- Authentication and authorization. -- Tool selection and immutable scope. -- Credentials and network clients. -- Persistence, idempotency, approval, and durable side effects. -- Logging and redaction policy. - -CodeMode owns: - -- Parsing and interpreting the supported subset without `eval`. -- Schema boundaries around tool calls. -- Plain-data copying and blocked prototype members. -- Resource limits, call accounting, and normalized diagnostics. -- Model-facing tool discovery and instructions. - -A program cannot gain authority through prose or generated code. It can only exercise authority already present in the supplied tools. Do not expose a broad tool and expect the prompt to restrict it. - -## Laws - -The public contract is guided by these equivalences: - -- `CodeMode.execute({ ...options, code })` is equivalent to `CodeMode.make(options).execute(code)`. -- A tool implementation is not invoked unless its input has decoded successfully. -- A tool result is not visible to the program unless its output has decoded and crossed the plain-data boundary successfully. -- Unknown host failures do not become model-visible diagnostics; `ToolError` is the explicit safe-message channel. -- Host interruption remains interruption rather than a `CodeMode.Failure`. - -## Non-Goals - -- Generic permission prompts or approval workflows. -- Durable pause/resume, replay, or storage adapters. -- Exactly-once external side effects. -- Application authorization or product policy. -- A filesystem or process sandbox for arbitrary JavaScript. -- Compatibility with the full JavaScript language or npm ecosystem. - -Applications that need approval or durable consequences should model those above CodeMode and expose only the currently authorized tools. +Non-goals: permission prompts and approval workflows, durable pause/resume or replay, exactly-once side effects, +application authorization policy, sandboxing arbitrary JavaScript, and compatibility with the full language or npm +ecosystem. Applications that need approval or durable consequences should model those above CodeMode and expose only +the currently authorized tools. ## Testing @@ -365,5 +206,3 @@ From the package directory: bun test bun run typecheck ``` - -The direct suite covers public projections, discovery, schema boundaries, diagnostic sanitization, resource limits, tool-call observation, and interruption. diff --git a/packages/codemode/codemode.md b/packages/codemode/codemode.md deleted file mode 100644 index f56c405746..0000000000 --- a/packages/codemode/codemode.md +++ /dev/null @@ -1,176 +0,0 @@ -# CodeMode Design and Status - -This is the living design and status document for `@opencode-ai/codemode` and its existing V2 OpenCode adapter. -It records current behavior, intentional boundaries, durable rationale, and material remaining work. - -Completed implementation history, branch names, test counts, and closed findings belong in git, not here. Remove -completed work instead of preserving checked-off chronology. - -Detailed package API documentation lives in [README.md](./README.md). OpenAPI-specific follow-ups live in -[src/openapi/TODO.md](./src/openapi/TODO.md). - -## How CodeMode Works - -### Purpose - -CodeMode gives a model one `execute` tool backed by a confined JavaScript interpreter. Inside the program, the model -can call an explicit tree of schema-described tools, sequence dependent work, run independent calls concurrently, -and filter or aggregate results before returning them to the agent loop. - -The goals are: - -- Reduce model context consumed by large tool catalogs. -- Avoid an agent round-trip between every dependent tool call. -- Keep large intermediate results inside the program instead of sending them through model context. -- Give generated code only the authority explicitly supplied by the host. - -CodeMode is an orchestration language, not a general JavaScript runtime or an application authorization system. - -### Runtime - -The generic runtime lives in `packages/codemode` and is host-neutral: - -1. The host builds a tree of `Tool.make(...)` definitions and calls `CodeMode.make(...)` or `CodeMode.execute(...)`. -2. CodeMode generates model instructions, a budgeted inline catalog, and the internal `$codemode.search` tool. -3. TypeScript syntax is transpiled away, Acorn parses the resulting JavaScript, and an owned tree-walking interpreter - executes it without `eval`. -4. Tool inputs and outputs cross schema and plain-data boundaries before they become visible on either side. -5. Execution returns `CodeMode.Result`. Expected program and tool failures are diagnostic data; host interruption - remains Effect interruption. - -Effect Schemas validate and transform tool inputs and outputs. JSON Schemas render model-facing signatures but do not -validate values; adapter-provided values still cross the plain-data boundary. A tool without an output schema is -advertised as `Promise`. - -### Discovery and model workflow - -The model sees a token-budgeted catalog. Every namespace remains visible, and complete signatures are selected -round-robin across namespaces so one large namespace cannot starve the others. `$codemode.search` is always callable -and is advertised when the inline catalog is partial. - -The intended workflow is: - -1. Pick an exact signature from the inline catalog, or return `$codemode.search(...)` results and use a selected path - in the next execution. -2. Call the exact returned path without guessing or normalizing segments. -3. Narrow `Promise` results before reading fields. -4. Start independent calls together and await them with `Promise.all`. -5. Filter and aggregate inside the program, then return only the data needed by the model. - -Search returns directly usable JavaScript paths, descriptions, and complete TypeScript signatures. It supports exact -path lookup, namespace browsing, deterministic ranking, and pagination. - -### Tool execution - -Calling a tool starts its Effect eagerly on a supervised fiber. The returned sandbox promise is run-once and can be -awaited directly or through the supported `Promise` combinators. At most eight tool calls execute concurrently. -Unfinished calls are drained before successful program completion, and an unhandled call failure becomes a diagnostic. - -The public execution-policy knobs are `timeoutMs`, `maxToolCalls`, and `maxOutputBytes`. The package supplies no -defaults because budgets are host policy. The interpreter also enforces fixed internal boundaries for tool-call -concurrency and data nesting depth. - -### Data, files, and failures - -Program results and tool arguments are JSON-like data. Dates become ISO strings at host boundaries; RegExp, Map, and -Set values become `{}` as they do under JSON serialization. Promise and runtime reference values cannot cross the -boundary. - -Unknown host failures and invalid outputs are sanitized. `ToolError` is the explicit channel for a safe message that a -tool wants the model to see. Diagnostic categories distinguish parsing, unsupported syntax, unknown tools, invalid -data, tool failures, limits, timeouts, and execution failures. - -Files and other attachment content stay outside the interpreter. A host may collect them while child tools execute and -attach them to the outer result, but the program receives only the structured tool output. - -### V2 OpenCode adapter - -This section describes the `v2` branch integration. On `dev`, CodeMode is integrated through -`packages/opencode/src/tool/code-mode.ts`, where nested MCP calls run the `tool.execute.before` and -`tool.execute.after` plugin hooks. - -CodeMode is integrated into V2 through `packages/core/src/tool/registry.ts` and -`packages/core/src/tool/execute.ts`: - -- Core has one canonical `Tool` representation. Location-scoped producers register direct or deferred tools through - `Tools.Service`. -- Each model step snapshots effective registrations, applies catalog visibility filtering, and exposes direct tools - normally. -- When visible deferred tools exist, Core reserves and materializes one `execute` tool. Grouped deferred tools become - CodeMode namespaces instead of flattened model-facing names. -- Each nested call checks that its captured registration is still current before dispatching it. -- Authorization and side-effect ordering remain responsibilities of the leaf tool. Catalog visibility is not execution - authorization. -- Structured child output enters the interpreter. File parts are collected host-side and attached to the outer result. -- Nested call statuses are returned as final `execute` metadata for the TUI. -- `execute` is the one model-facing tool invocation. Nested calls reuse its invocation context and do not independently - run registry hooks or model-output bounding; this keeps complete intermediate structured values available for - in-program filtering. The outer `execute` settlement is the single model-output bounding boundary. -- Core supplies no CodeMode timeout or tool-call limit. User cancellation interrupts the outer invocation and its - supervised children; the outer settlement applies Core's normal output-retention policy. - -MCP tools use this canonical path: they register as grouped tools and are deferred while CodeMode is enabled. Existing -output schemas are preserved in generated signatures. Direct Core tools remain direct and are not ambient globals -inside CodeMode. - -## Intentionally Unsupported - -These are product boundaries rather than DSL backlog: - -- Ambient filesystem, process, environment, network, credential, or application access. External work must go through - supplied tools. -- Modules, imports, dynamic imports, `eval`, arbitrary host globals, npm packages, and prototype mutation. -- Generic permission prompts, authorization policy, durable pause/resume, replay, storage, or exactly-once external - side effects. Hosts and tools own those concerns. -- Heuristic parsing of text tool results as JSON. A result should not silently change type based on its contents. - -The OpenAPI adapter may gain more transports and encodings, but it must continue skipping operations it cannot -represent accurately rather than guessing semantics. - -## Decisions and Rationale - -| Decision | Rationale | -| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Keep an owned tree-walking interpreter. | The product need is bounded tool orchestration, not arbitrary JavaScript. Owning the language surface keeps authority and behavior explicit. | -| Treat schemas as the model-facing interface. | Signatures drive correct calls; Effect Schema also provides the runtime validation boundary, while JSON Schema supports adapter interoperability. | -| Keep authority host-owned. | CodeMode can only confine programs to supplied tools. The host chooses those tools, and each tool enforces its own authorization and side-effect policy. | -| Use progressive catalog disclosure plus search. | Large tool sets should not consume the prompt, but every namespace must remain discoverable and speculative search calls should remain valid. | -| Start tool promises eagerly and supervise them. | This preserves normal call-time parallelism while giving each call run-once settlement and interruption safety. | -| Keep files outside the sandbox value space. | Models should compose structured data without routing binary payloads through generated code or context. | -| Treat `execute` as the model-facing invocation boundary. | Nested calls are implementation details of one orchestration program. Reusing the outer context and bounding only the final result preserves complete intermediate data without inventing durable child-call identities. | -| Return expected failures as data. | Models need actionable diagnostics without exposing private host causes; host interruption and defects must still propagate correctly. | -| Leave execution-limit defaults to hosts. | Appropriate budgets depend on the surrounding product and its own cancellation, retention, and output-bounding policies. | -| Skip unsupported OpenAPI operations. | Incorrect parameter encoding, authentication, or transport behavior is worse than a precise `skipped` reason. | - -## Remaining Work - -Keep only material unresolved work here. Small isolated defects should be GitHub issues; adapter-only work belongs in -the adapter TODO. Delete entries when completed. - -### DSL expansion - -The supported JavaScript subset should grow when common model-generated code improves tool orchestration. These are -current omissions to implement, not intentional product boundaries. - -- [ ] Design proper multi-stage promise pipelines. Supporting `.then`, `.catch`, and `.finally` should preserve promise - assimilation, cancellation, failure handling, and concurrent per-item pipelines rather than adding syntax-only - shims. Consider `Promise.any` in the same pass. -- [ ] Support async iteration and `for await...of`. Define behavior first for the runtime's supported promise and - collection values, then extend it to bounded host streams when a stream boundary exists. -- [ ] Support callback-bearing standard-library variants that models commonly generate: the mapper argument to - `Array.from(...)` and replacers for `JSON.stringify(...)`, including Effect-aware callbacks where needed. -- [ ] Close basic `Object` parity gaps: let `Object.values`/`Object.entries` accept arrays, make `Object.assign` validate - and mutate its target, add `Object.is`, and let `Object.fromEntries` consume every supported iterable. -- [ ] Add deterministic modern collection conveniences where they improve orchestration: `Object.groupBy`, Set - composition methods, and `Array.prototype.toSpliced`. -- [ ] Complete the deterministic `Math` surface beyond the current arithmetic, rounding, root, power, and logarithm - helpers. Decide separately whether nondeterministic `Math.random` belongs in the runtime. -- [ ] Refine diagnostics so user throws, expected tool failures, unexpected host/tool defects, and genuine interpreter - defects are distinguishable without leaking private causes. - -### Tool and result contracts - -- [ ] Design explicit tagged representations and size rules before allowing Blob, File, ArrayBuffer, typed arrays, or - host streams to cross the sandbox boundary. -- [ ] Define one consistent policy for tool path segments named `__proto__`, `constructor`, or `prototype`. They must - either be safely callable, rejected before catalog generation, or use one documented escaping rule. diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md new file mode 100644 index 0000000000..eebef60302 --- /dev/null +++ b/packages/codemode/interpreter-support.md @@ -0,0 +1,327 @@ +# CodeMode Interpreter Support + +This is the checkable support matrix for CodeMode's confined JavaScript interpreter. It tracks the language and +standard-library surface that programs can use today, plus concrete gaps that may be implemented later. + +- `[x]` means the feature is implemented at the scope described here. +- `[ ]` means a concrete compatibility gap remains. +- Checked items do not promise complete ECMAScript edge-case parity; known differences are stated explicitly. +- Intentional boundaries are not listed as compatibility work. + +When behavior changes, update this file and the tests in the same change. The implementation and tests remain the +ultimate source of truth. + +## Source and execution model + +- [x] JavaScript parsed with the latest syntax accepted by Acorn, then restricted by the interpreter allowlist. +- [x] Erasable TypeScript syntax, including type annotations, type declarations, assertions, and non-null assertions. + TypeScript is transpiled first; the emitted JavaScript must still use the supported subset. +- [x] Top-level `await` and `return` through the program's implicit async-function scope. +- [x] Explicit `return`, final top-level expression as a REPL-style result, and `null` when no value is produced. +- [x] Program results use JSON-like boundaries, with `undefined` and non-finite numbers normalized to `null`. Tool + arguments follow JSON serialization semantics before their schema applies (see the tools section). +- [x] Live Date, RegExp, Map, Set, URL, and URLSearchParams values inside CodeMode. +- [x] Tool calls through the host-provided `tools` tree only. +- [x] The global `search(...)` built-in: synchronous tool discovery that counts as an admitted tool call and is + shadowable by program declarations like other globals. +- [x] Cooperative timeout, an optional total tool-call limit, output bounding, and unrestricted tool-call concurrency. + +## Values and literals + +- [x] `null`, `undefined`, booleans, finite and non-finite numbers, and strings. +- [x] Array literals, including holes and spread from arrays, strings, Maps, Sets, and URLSearchParams. +- [x] Object literals with shorthand, computed string/number keys, and spread from plain data objects; `null` and + `undefined` are no-ops, while arrays are rejected. +- [x] Template literals with interpolation. +- [x] Regular-expression literals. +- [x] `NaN` and `Infinity` globals. +- [ ] BigInt literals and in-interpreter BigInt arithmetic; BigInt remains invalid at JSON-like host boundaries. +- [ ] Symbol primitive values and symbol-keyed properties. +- [ ] Tagged-template calls. +- [ ] Getter and setter definitions in object literals. + +## Bindings and destructuring + +- [x] `const`, `let`, and accepted `var` declarations. +- [x] Object and array destructuring in declarations, parameters, assignment expressions, and `for...of` bindings. +- [x] Nested patterns, defaults, elisions, and rest elements. +- [x] Assignment to identifiers, unblocked plain-object fields, non-negative integer array indexes, and writable URL + fields. +- [x] Direct function declarations are hoisted in program and block statement lists. +- [x] Parameter defaults observe a temporal dead zone for later parameters. +- [ ] JavaScript-correct function scoping, hoisting, and redeclaration for accepted `var` declarations. +- [x] Predeclare `let` and `const` bindings in every lexical scope, including program/block bodies, switch bodies, and + loop headers, so reads before initialization and self- or cross-referential initializers observe the JavaScript + temporal dead zone. +- [ ] Hoist function declarations accepted directly in switch cases. +- [x] Computed object destructuring keys such as `const { [field]: value } = record`. +- [x] Object destructuring from arrays, such as `const { length } = values`. +- [x] Array destructuring from supported non-array iterables: strings, Maps, Sets, and URLSearchParams. + +## Statements and control flow + +- [x] Blocks and empty statements. +- [x] `if`/`else` and conditional expressions. +- [x] `switch`, including default clauses and fallthrough. +- [x] `for`, `while`, and `do...while`. +- [x] `for...of` over arrays, strings, Maps, Sets, and URLSearchParams. +- [x] `for...in` over own keys of plain objects, arrays, and tool references. +- [x] Unlabeled `break` and `continue`. +- [x] `try`, `catch`, optional catch bindings, and `finally`. +- [x] `throw` with arbitrary values. +- [ ] Labeled statements, labeled `break`, and labeled `continue`. +- [ ] `for await...of` and async iteration. + +## Functions and callbacks + +- [x] Function declarations, function expressions, and arrow functions. +- [x] Synchronous and `async` functions. +- [x] Closures, recursion, default parameters, rest parameters, and destructured parameters. +- [x] Expression and block function bodies. +- [x] User callbacks for the supported Array, Map, Set, URLSearchParams, sort, string-replacement, and `Array.from` + mapper APIs, with one shared acceptance rule everywhere including promise reactions. +- [x] `Boolean`, `Number`, `String`, `parseInt`, `parseFloat`, `isFinite`, `isNaN`, and URI helpers as callbacks. +- [x] Built-in method references as callbacks, such as `values.map(Math.abs)`, `records.map(JSON.stringify)`, + `items.forEach(console.log)`, and `Promise.resolve(-1).then(Math.abs)`. Extra callback arguments a built-in + does not consume are ignored, like JS; consumed arguments stay strictly validated (`Math.floor` still rejects a + string). Intrinsic references keep their receiver (`"abc".includes` works as a predicate), unlike detached JS + methods, which lose `this`. +- [x] Constructors work as callbacks with JS call semantics: `Error` types construct (`messages.map(Error)`), + and new-requiring constructors (`Map`, `Set`, `URL`, `URLSearchParams`, `Promise`) throw a `TypeError`, + like JS. +- [x] Tool references and detached `Promise` statics are rejected as callbacks with a hint to wrap them in an + arrow function. +- [ ] Stop automatically awaiting promise-returning string replacers; match JavaScript's synchronous callback-result + coercion. +- [x] The optional `thisArg` of iteration methods is accepted and ignored: CodeMode functions have no `this`, so + ignoring it matches JS arrow-function semantics exactly. +- [ ] `this` in non-arrow CodeMode functions and callbacks. +- [ ] User-defined constructor calls. +- [ ] `Function.prototype.call`, `apply`, and `bind` for CodeMode functions. +- [ ] Classes and private fields. +- [ ] Generator functions and `yield`. + +## Expressions and operators + +- [x] Property access with dot or computed bracket syntax. +- [x] Optional property access and optional calls. +- [x] Function/tool calls and spread arguments. +- [x] Sequence expressions (the comma operator). +- [x] `await` for CodeMode promises; a plain value passes through unchanged, though every `await` still defers its + continuation one reaction turn. +- [x] `new` for Array, Object, Error types, Date, RegExp, Map, Set, URL, URLSearchParams, and Promise. +- [x] Arithmetic operators: `+`, `-`, `*`, `/`, `%`, and `**`. +- [x] Equality and ordering: `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, and `>=`. +- [x] Bitwise operators: `&`, `|`, `^`, `~`, `<<`, `>>`, and `>>>`. +- [x] Logical operators: `&&`, `||`, `??`, and `!`, with short-circuiting. +- [x] Unary `+`, unary `-`, `void`, `typeof`, `instanceof`, and own-property-only `in`. +- [x] Prefix and postfix `++` and `--`. +- [x] Plain, arithmetic, bitwise, and logical assignment operators. +- [x] Property deletion on plain data objects and arrays, including computed and optional forms; deleting an array index + creates a hole without changing its length. + +## Promises and tools + +- [x] Tool calls start eagerly and return supervised, run-once CodeMode promises. +- [x] Direct `await`, repeated awaits, and implicit resolution when a promise is returned from a function/program. +- [x] `Promise.resolve` and `Promise.reject`. +- [x] `Promise.all`, `Promise.allSettled`, `Promise.race`, and `Promise.any` over supported collections containing + promises and plain values. +- [x] `Promise.all` preserves result order and rejects on the first observed failure without cancelling siblings. +- [x] `Promise.allSettled` returns plain fulfilled/rejected outcome records. +- [x] `Promise.race` settles from the first result without cancelling losers at settlement time. +- [x] Real promise values from `Promise.all`, `Promise.allSettled`, and `Promise.race`; separately constructed + combinator batches overlap as in normal JavaScript. +- [x] Promise chaining with `.then`, `.catch`, and `.finally`: handlers run deferred in attach order, returned + promises are adopted, handler throws reject the derived promise, `.finally` preserves the original settlement + unless its cleanup fails, and direct self-resolution rejects with a `TypeError`. +- [x] Every `await` (including of plain values and already-settled promises) defers its continuation one reaction + turn, so concurrent async functions interleave at await points as in JavaScript. +- [x] Combinators settle one reaction turn after their deciding member (V8-observable ordering): reactions already + attached to members run first, and an aggregate cannot beat a plain value settling in the same turn into a + `Promise.race`. Exact microtask-count parity beyond this observable ordering is not a documented guarantee. +- [x] All still-pending work (race losers, fail-fast `Promise.all` stragglers, and un-awaited calls alike) is + interrupted when the program returns; rejections that settled un-awaited become `Success.warnings` + diagnostics. A combinator abandoned inside its final settlement turn counts as pending and is interrupted + without a warning. +- [x] `try`/`catch` can handle awaited tool and promise failures. +- [x] `Promise.any`: first fulfillment wins; all-rejected rejects with an `AggregateError` whose `errors` array holds + the catch-normalized reasons in input order, and empty input rejects with an empty `AggregateError`. +- [x] `new Promise((resolve, reject) => ...)`: the executor runs synchronously and receives first-class resolve/reject + callables that settle the promise exactly once (they may escape the executor and settle later); an executor + throw rejects unless the promise already settled, resolving with a promise adopts it, and resolving with the + promise itself rejects with a `TypeError`. Resolver callables work anywhere callbacks are accepted, including + `.then`/`.catch` handlers and collection callbacks, but remain opaque references that cannot cross the data + boundary. +- [ ] Thenable assimilation; objects with a callable `then` field remain plain data. +- [x] Dotted tool names are canonicalized into namespace paths; a path can be both callable and a namespace, and the + last definition supplied for a canonical path wins. +- [x] Tool path segments may be named `constructor`, `prototype`, or `__proto__` because paths use inert Map keys. +- [x] Outbound tool arguments follow JSON serialization semantics, like `JSON.stringify`: object properties with + `undefined` values are dropped, `undefined` array elements and non-finite numbers become `null`, and sparse + arrays densify. Tools never receive `undefined` inside their input object, though a bare `tools.t(undefined)` + argument still reaches schema decoding as `undefined`. Program results keep the stricter + normalization where every `undefined` becomes `null`. +- [ ] Tokenize and case-fold non-ASCII tool paths, descriptions, and queries for tool search. + +## Objects and properties + +- [x] Own-field reads and writes on plain data objects. +- [x] `Object()` and `new Object()` return `{}` for nullish arguments and pass objects through unchanged; + primitive wrapper objects (`Object(1)`) are rejected explicitly. +- [x] Computed property names and object spread. +- [x] `Object.keys`, `Object.values`, `Object.entries`, `Object.hasOwn`, `Object.assign`, and `Object.fromEntries`. +- [x] `Object.keys` over arrays and tool references. +- [x] Object identity is preserved by in-CodeMode Object helpers. +- [x] Prototype traversal and mutation through `__proto__`, `constructor`, and `prototype` are blocked. +- [ ] Legal own data fields named `__proto__`, `constructor`, or `prototype` are rejected at JSON/tool boundaries and + cannot be created, read, or written in CodeMode; tool path segments with those names remain supported. +- [x] `Object.is` for supported data values. +- [ ] `Object.groupBy`. + +## Arrays + +- [x] The `Array` constructor with or without `new`: `Array(a, b)` collects arguments and `Array(n)` creates a sparse + array of that length; invalid lengths throw `RangeError`. Iteration, spread, join, and JSON handle holes like + JavaScript, and host results normalize holes to `null`. +- [x] Static methods: `Array.isArray`, `Array.of`, and `Array.from`, including the `Array.from` mapper form with + `(value, index)` arguments. +- [x] Iteration/transformation: `map`, `filter`, `flatMap`, and `forEach`. +- [x] Searching/tests: `find`, `findIndex`, `findLast`, `findLastIndex`, `some`, `every`, `includes`, `indexOf`, and + `lastIndexOf`. +- [x] Aggregation: `reduce` and `reduceRight`. +- [x] Ordering: `sort`, `toSorted`, `reverse`, and `toReversed`. +- [x] Access/copying: `at`, `slice`, `concat`, `flat`, `with`, and `join`. +- [x] Mutation: `push`, `pop`, `shift`, `unshift`, `splice`, `fill`, and `copyWithin`. +- [x] Materialized iteration helpers: `keys`, `values`, and `entries` return arrays rather than iterators. +- [x] `length`, numeric indexing, index assignment, spread, and `for...of`. +- [x] The `thisArg` argument of `Array.from` is accepted and ignored, like JS arrows. +- [ ] `Array.prototype.toSpliced`. +- [ ] Canonical array/string index parsing: a key such as `"01"` must remain an ordinary property key rather than + aliasing index `1`. +- [ ] `Array.prototype.sort` and `toSorted` must preserve trailing holes; they currently turn holes into own + `undefined` elements. + +## Strings + +- [x] Case/normalization: `toLowerCase`, `toUpperCase`, `normalize`. +- [x] Trimming: `trim`, `trimStart`, and `trimEnd`. +- [x] Searching/tests: `includes`, `startsWith`, `endsWith`, `indexOf`, `lastIndexOf`, and `search`. +- [x] Slicing/access: `slice`, `substring`, `at`, `charAt`, `charCodeAt`, and `codePointAt`. +- [x] Construction/transformation: `split`, `concat`, `repeat`, `padStart`, `padEnd`, `replace`, and `replaceAll`. +- [x] Regular-expression integration: `match`, materialized `matchAll`, `replace`, `replaceAll`, `split`, and `search`. +- [x] `localeCompare`; locale and options arguments are currently ignored. +- [x] `toString`, `length`, numeric indexing, spread, and `for...of` by Unicode code point. +- [x] Static `String.fromCharCode` and `String.fromCodePoint`. +- [x] Native argument coercion for supported String methods; for example, `includes(1)` and `slice("1")` coerce like + native JS, `split(undefined)` returns the whole string, and `includes`/`startsWith`/`endsWith` reject regular + expressions with a native-style `TypeError`. Opaque runtime references still reject as data errors, and + `repeat` still requires a finite non-negative count. +- [x] Native no-argument parity for `match()`, `matchAll()`, and `search()`; all behave as an empty pattern. Present + arguments must still be a regular expression or string pattern. + +## Numbers and Math + +- [x] Coercion functions: `Number`, `parseInt`, and `parseFloat`. +- [x] Number predicates/parsers: `Number.isInteger`, `Number.isFinite`, `Number.isNaN`, `Number.isSafeInteger`, + `Number.parseInt`, and `Number.parseFloat`. +- [x] Number formatting: `toFixed`, `toPrecision`, `toExponential`, `toString`, and `valueOf`. +- [x] Number constants: `MAX_SAFE_INTEGER`, `MIN_SAFE_INTEGER`, `MAX_VALUE`, `MIN_VALUE`, `EPSILON`, `NaN`, + `POSITIVE_INFINITY`, and `NEGATIVE_INFINITY`. +- [x] Math constants: `PI`, `E`, `LN2`, `LN10`, `LOG2E`, `LOG10E`, `SQRT2`, and `SQRT1_2`. +- [x] Math methods: `random`, `max`, `min`, `abs`, `acos`, `acosh`, `asin`, `asinh`, `atan`, `atan2`, `atanh`, + `floor`, `ceil`, `round`, `trunc`, `sign`, `sqrt`, `cbrt`, `pow`, `hypot`, `cos`, `cosh`, `sin`, `sinh`, + `tan`, `tanh`, `log`, `log2`, `log10`, `log1p`, `exp`, `expm1`, `f16round`, `fround`, `clz32`, and `imul`. +- [x] Native zero-argument behavior for `Number()` and `String()`: they produce `0` and `""`, while + `Number(undefined)` stays `NaN` and `String(undefined)` stays `"undefined"`. +- [x] `++` and `--` use CodeMode numeric coercion (numeric strings increment, plain data objects become `NaN`, Dates + use their epoch time) and reject opaque runtime references as data errors. +- [x] Unknown static members on global namespaces and on `Number`/`String`/the coercion functions read as `undefined` + for feature detection. Calling any undefined value reports a native-style `TypeError` naming the callee, for + example `Math.sumPrecise is not a function.` Blocked members (`constructor`, `__proto__`, ...) still throw, + and unknown `Promise` statics keep their descriptive error. +- [ ] `Math.sumPrecise`. +- [x] Global coercing `isFinite` and `isNaN`; opaque runtime references reject as data errors, like `Number(...)`. + +## JSON and console + +- [x] `JSON.parse` and `JSON.stringify` for supported data objects; the blocked data-key gap listed above still applies. +- [x] Numeric/string indentation for `JSON.stringify`. +- [x] Captured `console.log`, `console.info`, `console.debug`, `console.warn`, and `console.error`. +- [x] Captured `console.dir` and `console.table`. +- [ ] `JSON.parse` reviver callbacks. +- [ ] `JSON.stringify` function/array replacers. + +## Date + +- [x] `Date.now`, `Date.parse`, and `Date.UTC`. +- [x] `new Date()` from the current time, epoch milliseconds, a date string, another Date, or local components. +- [x] `Date()` without `new` returns the current time as a string, like JS, but in deterministic ISO format + rather than the host's locale/timezone string. +- [x] `getTime`, `valueOf`, `toISOString`, `toJSON`, and deterministic ISO `toString`. +- [x] Local getters: `getFullYear`, `getMonth`, `getDate`, `getDay`, `getHours`, `getMinutes`, `getSeconds`, and + `getMilliseconds`. +- [x] UTC getters: `getUTCFullYear`, `getUTCMonth`, `getUTCDate`, `getUTCDay`, `getUTCHours`, `getUTCMinutes`, + `getUTCSeconds`, and `getUTCMilliseconds`. +- [x] `getTimezoneOffset`, arithmetic, relational comparison, and `instanceof Date`. +- [x] Date values serialize to ISO strings; invalid dates serialize to `null`. +- [ ] Date setters. +- [ ] `Date.prototype.toUTCString` and its `toGMTString` alias. +- [ ] Native one-argument Date coercion; unsupported boolean/object inputs currently become invalid dates instead of + being coerced. +- [ ] Native Date loose-equality and default primitive-coercion semantics. +- [ ] Native `RangeError` branding for invalid `toISOString()` calls. + +## Regular expressions + +- [x] Literal and `RegExp(pattern, flags)` construction, with or without `new`. +- [x] `test`, `exec`, and `toString`. +- [x] Readable `source`, `flags`, `lastIndex`, `global`, `ignoreCase`, `multiline`, `sticky`, `unicode`, and `dotAll`. +- [x] Captures, safe named groups (blocked member names are omitted), match `.index`, and stateful global matching. +- [x] Integration with supported String methods, including function replacers. +- [ ] Writable `lastIndex`. +- [ ] `hasIndices`, match `indices`, and `unicodeSets` metadata for the `d` and `v` flags. +- [ ] `RegExp.escape`. + +## Map and Set + +- [x] `new Map()` from entry arrays or another Map. +- [x] Map `get`, `set`, `has`, `delete`, `clear`, `size`, and `forEach`. +- [x] `new Set()` from arrays, strings, or another Set. +- [x] Set `add`, `has`, `delete`, `clear`, `size`, and `forEach`. +- [x] Materialized `keys`, `values`, and `entries` arrays for Map and Set. +- [x] Spread, `for...of`, `Array.from`, and `Object.fromEntries` integration. +- [x] Map and Set values serialize to `{}` at host/JSON boundaries. +- [ ] Set composition and relation methods: `union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, + `isSupersetOf`, and `isDisjointFrom`. + +## URL and URI helpers + +- [x] `encodeURI`, `encodeURIComponent`, `decodeURI`, and `decodeURIComponent`. +- [x] `new URL(input, base)`, `URL.canParse`, and `URL.parse`. +- [x] URL `toString`, `toJSON`, and linked `searchParams`. +- [x] Readable URL fields: `href`, `origin`, `protocol`, `username`, `password`, `host`, `hostname`, `port`, + `pathname`, `search`, and `hash`. +- [x] Writable URL fields except `origin`. +- [x] `new URLSearchParams()` from query strings, data objects, pairs, Maps, and URLSearchParams. +- [x] URLSearchParams `append`, `delete`, `get`, `getAll`, `has`, `set`, `sort`, `forEach`, `keys`, `values`, + `entries`, `toString`, and `size`. +- [x] URL values serialize to their href; URLSearchParams serialize to `{}`. + +## Errors and diagnostics + +- [x] `Error`, `TypeError`, `RangeError`, `SyntaxError`, `ReferenceError`, `EvalError`, and `URIError`, callable with + or without `new`. +- [x] `AggregateError` with the `(errors, message?)` signature and an own `errors` array, constructed directly or by + an all-rejected `Promise.any`. +- [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization. +- [x] `instanceof` for Date, RegExp, Map, Set, URL, URLSearchParams, Array, Object, Promise, and Error types. +- [x] Catchable user throws, runtime failures raised during interpreted evaluation, awaited tool failures, and awaited + tool-call-limit failures; parse/compile failures, cooperative timeout, and output bounding remain outside program + `catch`. +- [x] Source locations on unsupported-syntax diagnostics for JavaScript-shaped input; TypeScript transpilation may + shift them. +- [x] Sanitized model-visible diagnostics and explicit safe `ToolError` messages. +- [ ] Distinguish user-thrown failures from interpreter defects and explicit tool refusals from sanitized internal tool + failures; preserve those categories in caught errors, promise rejection handlers, and `Promise.allSettled` + reasons. diff --git a/packages/codemode/package.json b/packages/codemode/package.json index 1641518483..45a7eb80d9 100644 --- a/packages/codemode/package.json +++ b/packages/codemode/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/codemode", - "version": "1.18.11", + "version": "1.18.3", "description": "Effect-native confined code execution over schema-described tools", "private": true, "type": "module", diff --git a/packages/codemode/src/codemode.ts b/packages/codemode/src/codemode.ts index 14782d7c8a..1f99c01e74 100644 --- a/packages/codemode/src/codemode.ts +++ b/packages/codemode/src/codemode.ts @@ -1,18 +1,24 @@ import { Effect, Schema } from "effect" -import { executeWithLimits } from "./interpreter/runtime.js" -import { type HostTools, type Services, type ToolDescription, ToolRuntime } from "./tool-runtime.js" -import type { Definition } from "./tool.js" +import { executeWithLimits } from "./interpreter/execute.js" +import { type Services, type ToolDescription, ToolRuntime } from "./tool-runtime.js" +import type { Tools } from "./tools.js" /** A tool call admitted during an execution. */ export type { ToolCall, ToolCallEnded, ToolCallHooks, ToolCallStarted, ToolDescription } from "./tool-runtime.js" /** Resource budgets enforced independently during each CodeMode program execution. */ export type ExecutionLimits = { - /** Maximum wall-clock execution time in milliseconds. No default: absent means no timeout. */ + /** + * Wall-clock milliseconds before interruption. Result delivery waits for tool cleanup. + * No default: absent means no timeout. + */ readonly timeoutMs?: number /** Maximum number of tool calls admitted by the runtime. No default: absent means unlimited. */ readonly maxToolCalls?: number - /** Maximum UTF-8 bytes of model-facing output. No default: absent means no truncation. */ + /** + * Maximum UTF-8 bytes retained from the result and logs. Warnings have a separate equal budget; + * truncation notices and host formatting are additional. + */ readonly maxOutputBytes?: number } @@ -22,10 +28,6 @@ export type DiscoveryOptions = { readonly catalogBudget?: number } -type ToolTree = { - readonly [name: string]: Definition | ToolTree -} - export type ResolvedExecutionLimits = { readonly timeoutMs: number | undefined readonly maxToolCalls: number | undefined @@ -33,24 +35,24 @@ export type ResolvedExecutionLimits = { } /** Options for one CodeMode execution. */ -export type ExecuteOptions = {}> = { +export type ExecuteOptions = {}> = { /** Source for one program in the supported JavaScript subset. */ code: string - /** Explicit tool tree exposed to the program as `tools`. */ - tools?: Tools & ToolTree> + /** Explicit tools exposed to the program as `tools`. */ + tools?: Provided & Tools> /** Per-execution overrides for the default resource limits. */ limits?: ExecutionLimits /** Observes decoded tool input immediately before tool execution. */ - onToolCallStart?: (call: ToolRuntime.ToolCallStarted) => Effect.Effect> + onToolCallStart?: (call: ToolRuntime.ToolCallStarted) => Effect.Effect> /** Observes each admitted tool call as it settles, with outcome and duration. */ - onToolCallEnd?: (call: ToolRuntime.ToolCallEnded) => Effect.Effect> + onToolCallEnd?: (call: ToolRuntime.ToolCallEnded) => Effect.Effect> } /** A JSON value that can cross the confined interpreter boundary. */ export type DataValue = Schema.Json /** Configuration shared by `CodeMode.make` and `CodeMode.execute`. */ -export type Options = {}> = Omit, "code"> & { +export type Options = {}> = Omit, "code"> & { /** Progressive-disclosure configuration for the agent-facing tool catalog. */ readonly discovery?: DiscoveryOptions } @@ -70,8 +72,9 @@ export const DiagnosticKind = Schema.Literals([ "TimeoutExceeded", "ToolFailure", "ExecutionFailure", + "Truncated", ]) -/** Stable categories produced by program, schema, tool, and limit failures. */ +/** Stable categories produced by program, schema, tool, limit, and truncation diagnostics. */ export type DiagnosticKind = typeof DiagnosticKind.Type export const Diagnostic = Schema.Struct({ @@ -87,6 +90,7 @@ const ToolCallSchema = Schema.Struct({ name: Schema.String }) export const Success = Schema.Struct({ ok: Schema.Literal(true), value: Schema.Json, + warnings: Schema.optionalKey(Schema.Array(Diagnostic)), logs: Schema.optionalKey(Schema.Array(Schema.String)), truncated: Schema.optionalKey(Schema.Boolean), toolCalls: Schema.Array(ToolCallSchema), @@ -109,18 +113,14 @@ export const Result = Schema.Union([Success, Failure]) /** Result of executing a CodeMode program. Program failures are data, not Effect failures. */ export type Result = typeof Result.Type -/** Reusable confined runtime over one explicit tool tree. */ +/** Reusable confined runtime over explicit tools. */ export type Runtime = { readonly catalog: () => ReadonlyArray readonly instructions: () => string readonly execute: (code: string) => Effect.Effect } -const validateLimit = ( - name: keyof ExecutionLimits, - value: Value, - minimum: number, -): Value => { +const validateLimit = (name: keyof ExecutionLimits, value: number | undefined, minimum: number): number | undefined => { if (value !== undefined && (!Number.isSafeInteger(value) || value < minimum)) { throw new RangeError(`${name} must be a safe integer greater than or equal to ${minimum}.`) } @@ -134,26 +134,24 @@ const resolveExecutionLimits = (limits?: ExecutionLimits): ResolvedExecutionLimi }) /** Executes one Effect-native CodeMode program without constructing a reusable runtime. */ -export const execute = >( - options: ExecuteOptions, -): Effect.Effect> => { - const tools = (options.tools ?? {}) as HostTools> - ToolRuntime.assertValidTools(tools) +export const execute = >( + options: ExecuteOptions, +): Effect.Effect> => { + const tools = (options.tools ?? {}) as Tools> return executeWithLimits(options, resolveExecutionLimits(options.limits), ToolRuntime.searchIndex(tools)) } /** Creates an Effect-native runtime over explicit, schema-described tools. */ -export const make = = {}>( - options: Options = {} as Options, -): Runtime> => { - const tools = (options.tools ?? {}) as HostTools> - ToolRuntime.assertValidTools(tools) +export const make = = {}>( + options: Options = {} as Options, +): Runtime> => { + const tools = (options.tools ?? {}) as Tools> const limits = resolveExecutionLimits(options.limits) const prepared = ToolRuntime.prepare(tools, options.discovery?.catalogBudget) return { catalog: () => prepared.catalog, instructions: () => prepared.instructions, - execute: (code) => executeWithLimits({ ...options, code }, limits, prepared.searchIndex), + execute: (code) => executeWithLimits({ ...options, code }, limits, prepared.searchIndex), } } diff --git a/packages/codemode/src/interpreter/errors.ts b/packages/codemode/src/interpreter/errors.ts new file mode 100644 index 0000000000..ed621dc7f7 --- /dev/null +++ b/packages/codemode/src/interpreter/errors.ts @@ -0,0 +1,93 @@ +import type { Diagnostic } from "../codemode.js" +import { ToolError } from "../tool-error.js" +import { copyOut, ToolRuntimeError, type SafeObject } from "../tool-runtime.js" +import { type AstNode, formatLocation, InterpreterRuntimeError, ProgramThrow, sourceLocation } from "./model.js" +import { containsRuntimeReference } from "./references.js" +import { spreadItems } from "../stdlib/collections.js" +import { coerceToString, createAggregateErrorValue, createErrorValue, errorConstructors } from "../stdlib/value.js" + +export const normalizeError = (error: unknown): Diagnostic => { + if (error instanceof InterpreterRuntimeError) { + return { + kind: error.kind, + message: `${error.message}${formatLocation(error.node)}`, + ...(error.node?.loc ? { location: sourceLocation(error.node) } : {}), + ...(error.suggestions ? { suggestions: error.suggestions } : {}), + } + } + + if (error instanceof ToolRuntimeError) { + return { + kind: error.kind, + message: error.message, + ...(error.suggestions.length > 0 ? { suggestions: error.suggestions } : {}), + } + } + + if (error instanceof ToolError) { + return { kind: "ToolFailure", message: error.message } + } + + if (error instanceof ProgramThrow) { + const value = error.value + let message: string + if (containsRuntimeReference(value)) { + // Never expose runtime reference internals through thrown values. + message = "a non-data value" + } else if (typeof value === "string") { + message = value + } else if ( + value !== null && + typeof value === "object" && + typeof (value as { message?: unknown }).message === "string" + ) { + message = (value as { message: string }).message + } else { + try { + message = JSON.stringify(copyOut(value, "json")) ?? String(value) + } catch { + message = String(value) + } + } + return { kind: "ExecutionFailure", message: `Uncaught: ${message}` } + } + + if (error instanceof RangeError && /call stack|recursion/i.test(error.message)) { + return { + kind: "ExecutionFailure", + message: "Execution exceeded the maximum nesting depth.", + } + } + + if (error instanceof Error) { + return { + kind: error.name === "SyntaxError" ? "ParseError" : "ExecutionFailure", + message: error.message, + } + } + + return { + kind: "ExecutionFailure", + message: String(error), + } +} + +export const caughtErrorValue = (thrown: unknown): unknown => { + if (thrown instanceof ProgramThrow) return thrown.value + if (thrown instanceof InterpreterRuntimeError) return createErrorValue(thrown.errorName, thrown.message) + const name = thrown instanceof Error && errorConstructors.has(thrown.name) ? thrown.name : "Error" + return createErrorValue(name, normalizeError(thrown).message) +} + +export const constructErrorValue = (name: string, args: Array, node: AstNode): SafeObject => { + if (name !== "AggregateError") return createErrorValue(name, args[0] === undefined ? "" : coerceToString(args[0])) + const errors = spreadItems(args[0]) + if (errors === undefined) { + throw new InterpreterRuntimeError( + "new AggregateError(...) expects an array of errors (e.g. new AggregateError(errors, message?)).", + node, + ).as("TypeError") + } + // Error values must not alias caller-owned arrays. + return createAggregateErrorValue([...errors], args[1] === undefined ? "" : coerceToString(args[1])) +} diff --git a/packages/codemode/src/interpreter/execute.ts b/packages/codemode/src/interpreter/execute.ts new file mode 100644 index 0000000000..af2eccf9b8 --- /dev/null +++ b/packages/codemode/src/interpreter/execute.ts @@ -0,0 +1,231 @@ +import { parse } from "acorn" +import { Cause, Effect, Scope } from "effect" +import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript" +import type { DataValue, Diagnostic, ExecuteOptions, ResolvedExecutionLimits, Result } from "../codemode.js" +import { copyIn, copyOut, ToolRuntime, type Services } from "../tool-runtime.js" +import type { Tools } from "../tools.js" +import { normalizeError } from "./errors.js" +import { InterpreterRuntimeError, isRecord, type ProgramNode } from "./model.js" +import { PromiseRuntime } from "./promises.js" +import { Interpreter } from "./runtime.js" + +export const executeWithLimits = >( + options: ExecuteOptions, + limits: ResolvedExecutionLimits, + searchIndex: ToolRuntime.DiscoveryPlan["searchIndex"], +): Effect.Effect> => { + if (options.code.trim().length === 0) { + return Effect.succeed({ + ok: false, + error: { kind: "ParseError", message: "Code cannot be empty." }, + toolCalls: [], + }) + } + + // Allocate execution state inside suspension so reused Effects never share it. + return Effect.suspend(() => { + const tools = ToolRuntime.make( + (options.tools ?? {}) as Tools>, + limits.maxToolCalls, + searchIndex, + { + onToolCallStart: options.onToolCallStart, + onToolCallEnd: options.onToolCallEnd, + }, + ) + const logs: Array = [] + const logged = () => (logs.length > 0 ? { logs: [...logs] } : {}) + // Set only after copy-out so timeouts cannot report invalid values as completed. + let returned: { value: DataValue; promises: PromiseRuntime> } | undefined + + const base = Effect.acquireUseRelease( + Scope.make("parallel"), + (scope) => + Effect.gen(function* () { + const program = parseProgram(options.code) + const promises = new PromiseRuntime>(scope) + const interpreter = new Interpreter>( + tools.invoke, + tools.search, + tools.keys, + promises, + logs, + ) + const value = yield* interpreter.run(program) + const result = copyOut(copyIn(value, "Execution result"), "nullify") as DataValue + returned = { value: result, promises } + const warnings = yield* promises.interrupt() + return { + ok: true, + value: result, + ...(warnings.length > 0 ? { warnings } : {}), + ...logged(), + toolCalls: tools.calls, + } satisfies Result + }), + (scope, exit) => Scope.close(scope, exit), + ) + const timeoutMs = limits.timeoutMs + const operation = + timeoutMs === undefined + ? base + : base.pipe( + Effect.timeoutOrElse({ + duration: timeoutMs, + orElse: () => + Effect.sync(() => { + if (returned === undefined) { + return { + ok: false, + error: { kind: "TimeoutExceeded", message: `Execution timed out after ${timeoutMs}ms.` }, + ...logged(), + toolCalls: tools.calls, + } satisfies Result + } + // Keep the timeout warning first so truncation preserves it. + return { + ok: true, + value: returned.value, + warnings: [ + { + kind: "TimeoutExceeded", + message: `The program returned, but background work was still running at the ${timeoutMs}ms timeout and was interrupted. Await all started promises.`, + }, + ...returned.promises.diagnostics(), + ], + ...logged(), + toolCalls: tools.calls, + } satisfies Result + }), + }), + ) + + return operation.pipe( + Effect.catchCause((cause) => + Cause.hasInterruptsOnly(cause) + ? Effect.interrupt + : Effect.succeed({ + ok: false, + error: normalizeError(Cause.squash(cause)), + ...logged(), + toolCalls: tools.calls, + } satisfies Result), + ), + Effect.map((result) => + limits.maxOutputBytes === undefined ? result : boundOutput(result, limits.maxOutputBytes), + ), + ) + }) +} + +const parseProgram = (code: string): ProgramNode => { + const transpiled = transpileModule(`async function __codemode__() {\n${code}\n}`, { + reportDiagnostics: true, + compilerOptions: { + target: ScriptTarget.ESNext, + module: ModuleKind.ESNext, + }, + }) + const diagnostic = transpiled.diagnostics?.find((item) => item.category === DiagnosticCategory.Error) + + if (diagnostic) { + throw new InterpreterRuntimeError( + `Failed to parse TypeScript: ${flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`, + undefined, + "ParseError", + ) + } + + const bodyStart = transpiled.outputText.indexOf("{") + 1 + const bodyEnd = transpiled.outputText.lastIndexOf("}") + const executableCode = transpiled.outputText.slice(bodyStart, bodyEnd) + const parsed = parse(executableCode, { + ecmaVersion: "latest", + sourceType: "script", + allowReturnOutsideFunction: true, + allowAwaitOutsideFunction: true, + locations: true, + }) as unknown + + if (!isRecord(parsed) || parsed.type !== "Program" || !Array.isArray(parsed.body)) { + throw new InterpreterRuntimeError("Failed to parse script as a Program node.") + } + + return parsed as ProgramNode +} + +const utf8ByteLength = (value: string): number => new TextEncoder().encode(value).byteLength + +// Drop a replacement character produced by truncating inside a UTF-8 sequence. +const utf8Truncate = (value: string, maxBytes: number): string => { + const bytes = new TextEncoder().encode(value) + if (bytes.byteLength <= maxBytes) return value + const text = new TextDecoder("utf-8").decode(bytes.slice(0, Math.max(0, maxBytes))) + return text.endsWith("\uFFFD") ? text.slice(0, -1) : text +} + +// Warnings have a separate budget so result data cannot starve diagnostics. +const boundOutput = (result: Result, maxOutputBytes: number): Result => { + let truncated = false + + let value: DataValue = null + let valueBytes = 0 + if (result.ok) { + const serialized = JSON.stringify(result.value) ?? "null" + const bytes = utf8ByteLength(serialized) + if (bytes > maxOutputBytes) { + truncated = true + value = `${utf8Truncate(serialized, maxOutputBytes)} [result truncated: ${bytes} bytes exceeds the ${maxOutputBytes}-byte output limit; return a smaller value]` + valueBytes = maxOutputBytes + } else { + value = result.value + valueBytes = bytes + } + } + + const warnings = result.ok ? (result.warnings ?? []) : [] + const keptWarnings: Array = [] + let warningBytes = 0 + for (const warning of warnings) { + const bytes = utf8ByteLength(JSON.stringify(warning)) + 1 + if (warningBytes + bytes > maxOutputBytes) break + warningBytes += bytes + keptWarnings.push(warning) + } + if (keptWarnings.length < warnings.length) { + truncated = true + keptWarnings.push({ + kind: "Truncated", + message: `${warnings.length - keptWarnings.length} additional warnings omitted by the output limit.`, + }) + } + + const logs = result.logs ?? [] + const kept: Array = [] + const logBudget = Math.max(0, maxOutputBytes - valueBytes) + let logBytes = 0 + for (const line of logs) { + const lineBytes = utf8ByteLength(line) + 1 + if (logBytes + lineBytes > logBudget) break + logBytes += lineBytes + kept.push(line) + } + if (kept.length < logs.length) { + truncated = true + kept.push(`[logs truncated: showing ${kept.length} of ${logs.length} lines]`) + } + + if (!truncated) return result + const warningsPart = keptWarnings.length > 0 ? { warnings: keptWarnings } : {} + const logsPart = kept.length > 0 ? { logs: kept } : {} + return result.ok + ? { + ok: true, + value, + ...warningsPart, + ...logsPart, + truncated: true, + toolCalls: result.toolCalls, + } + : { ok: false, error: result.error, ...logsPart, truncated: true, toolCalls: result.toolCalls } +} diff --git a/packages/codemode/src/interpreter/methods.ts b/packages/codemode/src/interpreter/methods.ts new file mode 100644 index 0000000000..6eb592e7de --- /dev/null +++ b/packages/codemode/src/interpreter/methods.ts @@ -0,0 +1,881 @@ +import { Effect } from "effect" +import { + type AstNode, + CodeModeFunction, + CoercionFunction, + ErrorConstructorReference, + GlobalMethodReference, + GlobalNamespace, + IntrinsicReference, + InterpreterRuntimeError, + PromiseCapabilityFunction, + PromiseNamespace, + UriFunction, +} from "./model.js" +import { containsOpaqueReference, rejectCircularInsertion, typeofValue } from "./references.js" +import { isBlockedMember, type SafeObject } from "../tool-runtime.js" +import { + CodeModeDate, + CodeModeMap, + CodeModePromise, + CodeModeRegExp, + CodeModeSet, + CodeModeURL, + CodeModeURLSearchParams, +} from "../values.js" +import { invokeDateMethod, invokeDateStatic } from "../stdlib/date.js" +import { invokeJsonMethod } from "../stdlib/json.js" +import { invokeMathMethod } from "../stdlib/math.js" +import { invokeNumberMethod, invokeNumberStatic } from "../stdlib/number.js" +import { invokeObjectMethod } from "../stdlib/object.js" +import { invokeRegExpMethod, matchToValue, toHostRegex } from "../stdlib/regexp.js" +import { invokeStringStatic } from "../stdlib/string.js" +import { invokeURLMethod, invokeURLStatic, uriArgument } from "../stdlib/url.js" +import { boundedData, coerceToNumber, coerceToString, errorBrandName } from "../stdlib/value.js" + +export type CallbackRunner = { + readonly invokeFunction: (fn: CodeModeFunction, args: Array) => Effect.Effect + readonly invokeCallable: ( + callable: unknown, + args: Array, + node: AstNode, + ) => Effect.Effect + readonly settlePromise: (promise: CodeModePromise) => Effect.Effect +} + +// The single acceptance list for callbacks: collections, sort, string replacers, +// Array.from mappers, and promise reactions all admit exactly these callables. +// Admission means dispatchable, not necessarily invocable: new-requiring +// constructors pass the gate and throw a TypeError on call, like JS. +export type SupportedCallback = + | CodeModeFunction + | CoercionFunction + | UriFunction + | PromiseCapabilityFunction + | GlobalMethodReference + | IntrinsicReference + | ErrorConstructorReference + | GlobalNamespace + | PromiseNamespace + +export const isSupportedCallback = (value: unknown): value is SupportedCallback => + value instanceof CodeModeFunction || + value instanceof CoercionFunction || + value instanceof UriFunction || + value instanceof PromiseCapabilityFunction || + value instanceof GlobalMethodReference || + value instanceof IntrinsicReference || + value instanceof ErrorConstructorReference || + // Callable namespaces dispatch like JS: Array/Object/Date/RegExp construct, + // new-requiring constructors throw a TypeError. Math/JSON/console stay non-callable. + (value instanceof GlobalNamespace && typeofValue(value) === "function") || + value instanceof PromiseNamespace + +export const invokeIntrinsic = ( + runner: CallbackRunner, + ref: IntrinsicReference, + args: Array, + node: AstNode, +): Effect.Effect => { + if (typeof ref.receiver === "string") { + if (ref.name === "replace" || ref.name === "replaceAll") { + if (isSupportedCallback(args[1])) return invokeStringReplacer(runner, ref.receiver, ref.name, args, node) + if (typeofValue(args[1]) === "function") { + throw new InterpreterRuntimeError( + `String.${ref.name} cannot use this callable as a replacer; wrap it in an arrow function, e.g. (match) => tools.ns.tool(match).`, + node, + ) + } + } + return Effect.succeed(invokeStringMethod(ref.receiver, ref.name, args, node)) + } + if (typeof ref.receiver === "number") { + return Effect.succeed(invokeNumberMethod(ref.receiver, ref.name, args, node)) + } + if (Array.isArray(ref.receiver)) { + return invokeArrayMethod(runner, ref.receiver, ref.name, args, node) + } + if (ref.receiver instanceof CodeModeDate) { + return Effect.succeed(invokeDateMethod(ref.receiver, ref.name, node)) + } + if (ref.receiver instanceof CodeModeRegExp) { + return Effect.succeed(invokeRegExpMethod(ref.receiver, ref.name, args, node)) + } + if (ref.receiver instanceof CodeModeMap) { + return invokeMapMethod(runner, ref.receiver, ref.name, args, node) + } + if (ref.receiver instanceof CodeModeSet) { + return invokeSetMethod(runner, ref.receiver, ref.name, args, node) + } + if (ref.receiver instanceof CodeModeURL) { + return Effect.succeed(invokeURLMethod(ref.receiver, ref.name, node)) + } + if (ref.receiver instanceof CodeModeURLSearchParams) { + return invokeURLSearchParamsMethod(runner, ref.receiver, ref.name, args, node) + } + throw new InterpreterRuntimeError(`Method '${ref.name}' is not available in CodeMode.`, node) +} + +export const invokeGlobalMethod = (ref: GlobalMethodReference, args: Array, node: AstNode): unknown => { + if (ref.namespace === "console") + throw new InterpreterRuntimeError(`console.${ref.name} is not available in CodeMode.`, node) + if (ref.namespace === "Object") return invokeObjectMethod(ref.name, args, node) + if (ref.namespace === "Math") return invokeMathMethod(ref.name, args, node) + if (ref.namespace === "Array") return invokeArrayStatic(ref.name, args, node) + if (ref.namespace === "Number") return invokeNumberStatic(ref.name, args, node) + if (ref.namespace === "String") return invokeStringStatic(ref.name, args, node) + if (ref.namespace === "URL") return invokeURLStatic(ref.name, args, node) + if (ref.namespace === "Date") return invokeDateStatic(ref.name, args, node) + if ( + ref.namespace === "RegExp" || + ref.namespace === "Map" || + ref.namespace === "Set" || + ref.namespace === "URLSearchParams" + ) { + throw new InterpreterRuntimeError(`${ref.namespace}.${ref.name} is not available in CodeMode.`, node) + } + return invokeJsonMethod(ref.name, args, node) +} + +const requireDataArgument = (name: string, index: number, arg: unknown, node: AstNode): unknown => { + if (containsOpaqueReference(arg)) { + throw new InterpreterRuntimeError( + `String.${name} expects argument ${index + 1} to be a data value.`, + node, + "InvalidDataValue", + ) + } + return arg +} + +const invokeStringMethod = (value: string, name: string, args: Array, node: AstNode): unknown => { + // Coerce arguments like native JS; opaque runtime references still reject. + const str = (index: number): string => coerceToString(requireDataArgument(name, index, args[index], node)) + const num = (index: number): number => coerceToNumber(requireDataArgument(name, index, args[index], node)) + const optNum = (index: number): number | undefined => (args[index] === undefined ? undefined : num(index)) + const optStr = (index: number): string | undefined => (args[index] === undefined ? undefined : str(index)) + const rejectRegex = (): void => { + if (args[0] instanceof CodeModeRegExp) { + throw new InterpreterRuntimeError( + `String.${name} cannot take a regular expression; use regex.test(string) or String.search instead.`, + node, + ).as("TypeError") + } + } + + let result: unknown + switch (name) { + case "toLowerCase": + result = value.toLowerCase() + break + case "toUpperCase": + result = value.toUpperCase() + break + case "trim": + result = value.trim() + break + case "trimStart": + result = value.trimStart() + break + case "trimEnd": + result = value.trimEnd() + break + // Locale/options are deliberately unsupported; comparison uses the host default locale. + case "localeCompare": + result = value.localeCompare(str(0)) + break + case "normalize": { + const form = optStr(0) + try { + result = value.normalize(form) + } catch { + throw new InterpreterRuntimeError( + `String.normalize expects the form "NFC", "NFD", "NFKC", or "NFKD" (got ${JSON.stringify(form)}).`, + node, + ).as("RangeError") + } + break + } + case "split": { + // Native: an undefined separator returns the whole string, not a split on "undefined", + // unless the limit truncates to zero. + if (args[0] === undefined) { + const requestedLimit = optNum(1) + result = requestedLimit !== undefined && requestedLimit >>> 0 === 0 ? [] : [value] + break + } + if (args[0] instanceof CodeModeRegExp) { + result = value.split(args[0].regex, optNum(1)) + break + } + const requestedLimit = optNum(1) + result = value.split(str(0), requestedLimit === undefined ? undefined : requestedLimit >>> 0) + break + } + case "slice": + result = value.slice(optNum(0), optNum(1)) + break + case "includes": + rejectRegex() + result = value.includes(str(0), optNum(1)) + break + case "startsWith": + rejectRegex() + result = value.startsWith(str(0), optNum(1)) + break + case "endsWith": + rejectRegex() + result = value.endsWith(str(0), optNum(1)) + break + case "indexOf": + result = value.indexOf(str(0), optNum(1)) + break + case "lastIndexOf": + result = value.lastIndexOf(str(0), optNum(1)) + break + case "replace": + case "replaceAll": { + if (args[0] instanceof CodeModeRegExp) { + const pattern = args[0].regex + const replacement = str(1) + if (name === "replaceAll" && !pattern.global) { + throw new InterpreterRuntimeError( + `String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.replace to replace only the first match.`, + node, + ) + } + result = name === "replace" ? value.replace(pattern, replacement) : value.replaceAll(pattern, replacement) + break + } + if (name === "replace") { + result = value.replace(str(0), str(1)) + break + } + result = value.replaceAll(str(0), str(1)) + break + } + case "match": { + const pattern = toHostRegex(args[0], name, node) + const matched = value.match(pattern) + if (matched === null) return null + // Preserve the own `index` and `groups` properties on non-global matches. + if (pattern.global) return boundedData(matched, "String.match result") + return matchToValue(matched) + } + case "matchAll": { + const pattern = toHostRegex(args[0], name, node, "g") + if (!pattern.global) { + throw new InterpreterRuntimeError( + `String.matchAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.match for a single match.`, + node, + ) + } + return Array.from(value.matchAll(pattern), matchToValue) + } + case "search": { + result = value.search(toHostRegex(args[0], name, node)) + break + } + case "repeat": { + const count = num(0) + if (!Number.isFinite(count) || count < 0) + throw new InterpreterRuntimeError("String.repeat expects a finite non-negative count.", node).as("RangeError") + result = value.repeat(count) + break + } + case "padStart": + result = value.padStart(num(0), optStr(1)) + break + case "padEnd": + result = value.padEnd(num(0), optStr(1)) + break + case "charAt": + result = value.charAt(optNum(0) ?? 0) + break + case "at": + result = value.at(optNum(0) ?? 0) + break + case "substring": + result = value.substring(optNum(0) ?? 0, optNum(1)) + break + case "charCodeAt": + result = value.charCodeAt(optNum(0) ?? 0) + break + case "codePointAt": + result = value.codePointAt(optNum(0) ?? 0) + break + case "toString": + result = value + break + case "concat": { + result = value.concat(...args.map((_, index) => str(index))) + break + } + default: + throw new InterpreterRuntimeError(`String method '${name}' is not available in CodeMode.`, node) + } + return boundedData(result, `String.${name} result`) +} + +export const arrayStatics = new Set(["isArray", "of", "from"]) + +const invokeArrayStatic = (name: string, args: Array, node: AstNode): unknown => { + switch (name) { + case "isArray": + return Array.isArray(args[0]) + case "of": + return [...args] + case "from": + return arrayFromItems(args[0], node) + default: + throw new InterpreterRuntimeError(`Array.${name} is not available in CodeMode.`, node) + } +} + +const arrayFromItems = (source: unknown, node: AstNode): Array => { + if (source instanceof CodeModeMap) return Array.from(source.map.entries(), ([key, item]) => [key, item]) + if (source instanceof CodeModeSet) return Array.from(source.set.values()) + if (source instanceof CodeModeURLSearchParams) { + return Array.from(source.params.entries(), ([key, value]) => [key, value]) + } + if (source instanceof CodeModePromise) { + throw new InterpreterRuntimeError( + "Array.from received an un-awaited Promise; await it before creating the array.", + node, + "InvalidDataValue", + ) + } + if (typeof source === "string") return Array.from(source) + if (Array.isArray(source)) return [...source] + if ( + source !== null && + typeof source === "object" && + (Object.getPrototypeOf(source) === Object.prototype || Object.getPrototypeOf(source) === null) && + typeof (source as { length?: unknown }).length === "number" + ) { + return Array.from(source as ArrayLike) + } + throw new InterpreterRuntimeError( + "Array.from expects an array, string, Map, Set, or array-like value.", + node, + "InvalidDataValue", + ) +} + +export const invokeArrayFrom = ( + runner: CallbackRunner, + args: Array, + node: AstNode, +): Effect.Effect => { + const items = arrayFromItems(args[0], node) + if (args.length < 2 || args[1] === undefined) return Effect.succeed(items) + const apply = applyCollectionCallback(runner, args[1], "Array.from", node) + return Effect.gen(function* () { + const values: Array = [] + for (let index = 0; index < items.length; index += 1) { + values.push(yield* apply([items[index], index])) + } + return values + }) +} + +const invokeStringReplacer = ( + runner: CallbackRunner, + value: string, + name: "replace" | "replaceAll", + args: Array, + node: AstNode, +): Effect.Effect => { + const apply = applyCollectionCallback(runner, args[1], `String.${name}`, node) + const matches: Array<{ readonly match: string; readonly offset: number; readonly args: Array }> = [] + const collect = (...callbackArgs: Array): string => { + const match = callbackArgs[0] + const groups = callbackArgs[callbackArgs.length - 1] + const hasGroups = groups !== null && typeof groups === "object" + const offset = callbackArgs[callbackArgs.length - (hasGroups ? 3 : 2)] + if (typeof match !== "string" || typeof offset !== "number") { + throw new InterpreterRuntimeError(`String.${name} produced an invalid replacement match.`, node) + } + if (hasGroups) { + const safeGroups: SafeObject = Object.create(null) as SafeObject + for (const [key, group] of Object.entries(groups)) { + if (!isBlockedMember(key)) safeGroups[key] = group + } + callbackArgs[callbackArgs.length - 1] = safeGroups + } + matches.push({ match, offset, args: callbackArgs }) + return match + } + + const pattern = args[0] + if (pattern instanceof CodeModeRegExp) { + if (name === "replaceAll" && !pattern.regex.global) { + throw new InterpreterRuntimeError( + `String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.regex.source}/${pattern.regex.flags}g, or use String.replace to replace only the first match.`, + node, + ) + } + if (name === "replace") value.replace(pattern.regex, collect) + else value.replaceAll(pattern.regex, collect) + } else { + const search = coerceToString(requireDataArgument(name, 0, pattern, node)) + if (name === "replace") value.replace(search, collect) + else value.replaceAll(search, collect) + } + + return Effect.gen(function* () { + const output: Array = [] + let end = 0 + for (const match of matches) { + const replacement = yield* apply(match.args) + const resolved = + args[1] instanceof CodeModeFunction && args[1].async && replacement instanceof CodeModePromise + ? yield* runner.settlePromise(replacement) + : replacement + // Error values are branded plain objects; boundedData would strip the brand before coercion. + output.push( + value.slice(end, match.offset), + errorBrandName(resolved) + ? coerceToString(resolved) + : coerceToString(boundedData(resolved, `String.${name} replacer result`)), + ) + end = match.offset + match.match.length + } + output.push(value.slice(end)) + return boundedData(output.join(""), `String.${name} result`) + }) +} + +export const applyCollectionCallback = ( + runner: CallbackRunner, + callback: unknown, + name: string, + node: AstNode, +): ((args: Array) => Effect.Effect) => { + if (!isSupportedCallback(callback)) { + if (typeofValue(callback) === "function") { + throw new InterpreterRuntimeError( + `${name} cannot use this callable as a callback; wrap it in an arrow function, e.g. (value) => tools.ns.tool(value).`, + node, + ) + } + throw new InterpreterRuntimeError(`${name} expects a function callback.`, node) + } + return (callbackArgs) => runner.invokeCallable(callback, callbackArgs, node) +} + +const invokeMapMethod = ( + runner: CallbackRunner, + target: CodeModeMap, + name: string, + args: Array, + node: AstNode, +): Effect.Effect => { + switch (name) { + case "get": + return Effect.succeed(target.map.get(args[0])) + case "has": + return Effect.succeed(target.map.has(args[0])) + case "set": + return Effect.sync(() => { + target.map.set(args[0], args[1]) + return target + }) + case "delete": + return Effect.sync(() => target.map.delete(args[0])) + case "clear": + return Effect.sync(() => { + target.map.clear() + return undefined + }) + case "keys": + return Effect.sync(() => Array.from(target.map.keys())) + case "values": + return Effect.sync(() => Array.from(target.map.values())) + case "entries": + return Effect.sync(() => Array.from(target.map.entries(), ([key, item]): Array => [key, item])) + case "forEach": { + const apply = applyCollectionCallback(runner, args[0], "Map.forEach", node) + return Effect.gen(function* () { + for (const [key, item] of Array.from(target.map.entries())) yield* apply([item, key, target]) + return undefined + }) + } + default: + throw new InterpreterRuntimeError(`Map method '${name}' is not available in CodeMode.`, node) + } +} + +const invokeSetMethod = ( + runner: CallbackRunner, + target: CodeModeSet, + name: string, + args: Array, + node: AstNode, +): Effect.Effect => { + switch (name) { + case "has": + return Effect.succeed(target.set.has(args[0])) + case "add": + return Effect.sync(() => { + target.set.add(args[0]) + return target + }) + case "delete": + return Effect.sync(() => target.set.delete(args[0])) + case "clear": + return Effect.sync(() => { + target.set.clear() + return undefined + }) + case "keys": + case "values": + return Effect.sync(() => Array.from(target.set.values())) + case "entries": + return Effect.sync(() => Array.from(target.set.values(), (item): Array => [item, item])) + case "forEach": { + const apply = applyCollectionCallback(runner, args[0], "Set.forEach", node) + return Effect.gen(function* () { + for (const item of Array.from(target.set.values())) yield* apply([item, item, target]) + return undefined + }) + } + default: + throw new InterpreterRuntimeError(`Set method '${name}' is not available in CodeMode.`, node) + } +} + +const invokeURLSearchParamsMethod = ( + runner: CallbackRunner, + target: CodeModeURLSearchParams, + name: string, + args: Array, + node: AstNode, +): Effect.Effect => { + const arg = (index: number): string => uriArgument(args[index], `URLSearchParams.${name} argument ${index + 1}`) + const requireArgs = (count: number): void => { + if (args.length < count) { + throw new InterpreterRuntimeError( + `URLSearchParams.${name} requires ${count} argument${count === 1 ? "" : "s"}.`, + node, + ).as("TypeError") + } + } + switch (name) { + case "append": { + requireArgs(2) + return Effect.sync(() => { + target.params.append(arg(0), arg(1)) + return undefined + }) + } + case "delete": { + requireArgs(1) + return Effect.sync(() => { + if (args[1] !== undefined) target.params.delete(arg(0), arg(1)) + else target.params.delete(arg(0)) + return undefined + }) + } + case "get": + requireArgs(1) + return Effect.sync(() => target.params.get(arg(0))) + case "getAll": + requireArgs(1) + return Effect.sync(() => target.params.getAll(arg(0))) + case "has": + requireArgs(1) + return Effect.sync(() => (args[1] !== undefined ? target.params.has(arg(0), arg(1)) : target.params.has(arg(0)))) + case "set": { + requireArgs(2) + return Effect.sync(() => { + target.params.set(arg(0), arg(1)) + return undefined + }) + } + case "sort": + return Effect.sync(() => { + target.params.sort() + return undefined + }) + case "keys": + return Effect.sync(() => Array.from(target.params.keys())) + case "values": + return Effect.sync(() => Array.from(target.params.values())) + case "entries": + return Effect.sync(() => Array.from(target.params.entries(), ([key, value]): Array => [key, value])) + case "toString": + return Effect.sync(() => target.params.toString()) + case "forEach": { + requireArgs(1) + const apply = applyCollectionCallback(runner, args[0], "URLSearchParams.forEach", node) + return Effect.gen(function* () { + for (const [key, value] of Array.from(target.params.entries())) yield* apply([value, key, target]) + return undefined + }) + } + default: + throw new InterpreterRuntimeError(`URLSearchParams method '${name}' is not available in CodeMode.`, node) + } +} + +const invokeArrayMethod = ( + runner: CallbackRunner, + target: Array, + name: string, + args: Array, + node: AstNode, +): Effect.Effect => { + const optNumber = (value: unknown, label: string): number | undefined => { + if (value === undefined) return undefined + if (typeof value !== "number") + throw new InterpreterRuntimeError(`Array.${name} expects ${label} to be a number.`, node) + return value + } + switch (name) { + case "join": { + if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) { + throw new InterpreterRuntimeError("Array.join expects zero arguments or one string separator.", node) + } + const input = boundedData(target, "Array.join input") as Array + return Effect.succeed( + input.map((item) => coerceToString(item ?? "")).join(args.length === 0 ? "," : (args[0] as string)), + ) + } + case "includes": + if (args.length === 0 || args.length > 2) + throw new InterpreterRuntimeError("Array.includes expects a value and optional start index.", node) + return Effect.succeed(target.includes(args[0], optNumber(args[1], "start index"))) + case "indexOf": + return Effect.succeed(target.indexOf(args[0], optNumber(args[1], "start index"))) + case "lastIndexOf": + return Effect.succeed( + args[1] === undefined + ? target.lastIndexOf(args[0]) + : target.lastIndexOf(args[0], optNumber(args[1], "start index")), + ) + case "at": + return Effect.succeed(target.at(optNumber(args[0], "index") ?? 0)) + case "slice": + return Effect.succeed(target.slice(optNumber(args[0], "start"), optNumber(args[1], "end"))) + case "concat": + return Effect.succeed(target.concat(...args)) + case "flat": + return Effect.succeed(target.flat(optNumber(args[0], "depth") ?? 1)) + case "reverse": + return Effect.succeed(target.reverse()) + case "sort": + return Effect.map(sortArray(runner, target, args[0], "Array.sort", node), (sorted) => { + target.splice(0, target.length, ...sorted) + return target + }) + case "toSorted": + return sortArray(runner, target, args[0], "Array.toSorted", node) + case "toReversed": + return Effect.succeed([...target].reverse()) + case "with": { + const index = optNumber(args[0], "index") ?? 0 + const resolved = index < 0 ? target.length + index : index + if (resolved < 0 || resolved >= target.length) { + throw new InterpreterRuntimeError("Array.with index is out of range.", node) + } + const copied = [...target] + copied[resolved] = args[1] + return Effect.succeed(copied) + } + case "push": { + // Validate all insertions before mutating to avoid partial cyclic updates. + for (const item of args) rejectCircularInsertion(target, item, "Array.push result", node) + target.push(...args) + return Effect.succeed(target.length) + } + case "unshift": { + for (const item of args) rejectCircularInsertion(target, item, "Array.unshift result", node) + target.unshift(...args) + return Effect.succeed(target.length) + } + case "pop": + return Effect.succeed(target.pop()) + case "shift": + return Effect.succeed(target.shift()) + case "splice": { + if (args.length === 0) return Effect.succeed(target.splice(0, 0)) + const start = optNumber(args[0], "start") ?? 0 + if (args.length === 1) return Effect.succeed(target.splice(start)) + const deleteCount = optNumber(args[1], "delete count") ?? 0 + const inserted = args.slice(2) + for (const item of inserted) rejectCircularInsertion(target, item, "Array.splice result", node) + return Effect.succeed(target.splice(start, deleteCount, ...inserted)) + } + case "fill": { + rejectCircularInsertion(target, args[0], "Array.fill result", node) + return Effect.succeed(target.fill(args[0], optNumber(args[1], "start"), optNumber(args[2], "end"))) + } + case "copyWithin": + return Effect.succeed( + target.copyWithin( + optNumber(args[0], "target index") ?? 0, + optNumber(args[1], "start") ?? 0, + optNumber(args[2], "end"), + ), + ) + case "keys": + return Effect.succeed(Array.from(target.keys())) + case "values": + return Effect.succeed([...target]) + case "entries": + return Effect.succeed(Array.from(target.entries(), ([index, item]): Array => [index, item])) + } + + const apply = applyCollectionCallback(runner, args[0], `Array.${name}`, node) + return Effect.gen(function* () { + // Fix iteration length while reading existing elements live. + const length = target.length + switch (name) { + case "map": { + const values: Array = [] + values.length = length + for (let index = 0; index < length; index += 1) { + if (!(index in target)) continue + values[index] = yield* apply([target[index], index, target]) + } + return values + } + case "flatMap": { + const values: Array = [] + for (let index = 0; index < length; index += 1) { + if (!(index in target)) continue + const mapped = yield* apply([target[index], index, target]) + if (Array.isArray(mapped)) values.push(...mapped) + else values.push(mapped) + } + return values + } + case "filter": { + const values: Array = [] + for (let index = 0; index < length; index += 1) { + if (!(index in target)) continue + const item = target[index] + if (yield* apply([item, index, target])) values.push(item) + } + return values + } + case "find": + for (let index = 0; index < length; index += 1) { + const item = target[index] + if (yield* apply([item, index, target])) return item + } + return undefined + case "findIndex": + for (let index = 0; index < length; index += 1) { + if (yield* apply([target[index], index, target])) return index + } + return -1 + case "some": + for (let index = 0; index < length; index += 1) { + if (!(index in target)) continue + if (yield* apply([target[index], index, target])) return true + } + return false + case "every": + for (let index = 0; index < length; index += 1) { + if (!(index in target)) continue + if (!(yield* apply([target[index], index, target]))) return false + } + return true + case "forEach": + for (let index = 0; index < length; index += 1) { + if (index in target) yield* apply([target[index], index, target]) + } + return undefined + case "reduce": { + let start = 0 + let accumulator = args[1] + if (args.length < 2) { + while (start < length && !(start in target)) start += 1 + if (start === length) + throw new InterpreterRuntimeError("Array.reduce of an empty array with no initial value.", node).as( + "TypeError", + ) + accumulator = target[start] + start += 1 + } + for (let index = start; index < length; index += 1) { + if (!(index in target)) continue + accumulator = yield* apply([accumulator, target[index], index, target]) + } + return accumulator + } + case "reduceRight": { + let start = length - 1 + let accumulator = args[1] + if (args.length < 2) { + while (start >= 0 && !(start in target)) start -= 1 + if (start < 0) + throw new InterpreterRuntimeError("Array.reduceRight of an empty array with no initial value.", node).as( + "TypeError", + ) + accumulator = target[start] + start -= 1 + } + for (let index = start; index >= 0; index -= 1) { + if (!(index in target)) continue + accumulator = yield* apply([accumulator, target[index], index, target]) + } + return accumulator + } + case "findLast": + for (let index = length - 1; index >= 0; index -= 1) { + const item = target[index] + if (yield* apply([item, index, target])) return item + } + return undefined + case "findLastIndex": + for (let index = length - 1; index >= 0; index -= 1) { + if (yield* apply([target[index], index, target])) return index + } + return -1 + } + throw new InterpreterRuntimeError(`Array method '${name}' is not available in CodeMode.`, node) + }) +} + +const sortArray = ( + runner: CallbackRunner, + target: Array, + comparator: unknown, + name: string, + node: AstNode, +): Effect.Effect, unknown, R> => { + if (comparator === undefined) { + return Effect.sync(() => + [...target].sort((a, b) => { + const left = coerceToString(a) + const right = coerceToString(b) + return left < right ? -1 : left > right ? 1 : 0 + }), + ) + } + const apply = applyCollectionCallback(runner, comparator, name, node) + const mergeSort = (items: Array): Effect.Effect, unknown, R> => { + if (items.length <= 1) return Effect.succeed(items) + const midpoint = Math.floor(items.length / 2) + return Effect.gen(function* () { + const left = yield* mergeSort(items.slice(0, midpoint)) + const right = yield* mergeSort(items.slice(midpoint)) + const merged: Array = [] + let leftIndex = 0 + let rightIndex = 0 + while (leftIndex < left.length && rightIndex < right.length) { + // Treat a NaN comparator result as equal to preserve stable ordering. + const order = coerceToNumber(yield* apply([left[leftIndex], right[rightIndex]])) + if (Number.isNaN(order) || order <= 0) merged.push(left[leftIndex++]) + else merged.push(right[rightIndex++]) + } + return [...merged, ...left.slice(leftIndex), ...right.slice(rightIndex)] + }) + } + const defined = target.filter((item) => item !== undefined) + const undefinedCount = target.length - defined.length + return Effect.map(mergeSort(defined), (items) => [...items, ...Array(undefinedCount).fill(undefined)]) +} diff --git a/packages/codemode/src/interpreter/model.ts b/packages/codemode/src/interpreter/model.ts index d953514510..a70bdbe0a9 100644 --- a/packages/codemode/src/interpreter/model.ts +++ b/packages/codemode/src/interpreter/model.ts @@ -1,5 +1,5 @@ import type { SafeObject } from "../tool-runtime.js" -import type { SandboxURL } from "../values.js" +import type { CodeModePromise, CodeModeURL } from "../values.js" export type SourcePosition = { line: number @@ -30,13 +30,12 @@ export type Binding = { export type StatementResult = | { kind: "none" } - | { kind: "value"; value: unknown } | { kind: "return"; value: unknown } | { kind: "break" } | { kind: "continue" } export type MemberReference = { - target: SafeObject | Array | SandboxURL + target: SafeObject | Array | CodeModeURL key: string | number } @@ -45,6 +44,7 @@ export class CodeModeFunction { readonly parameters: ReadonlyArray, readonly body: AstNode, readonly capturedScopes: ReadonlyArray>, + readonly async: boolean, ) {} } @@ -61,12 +61,25 @@ export class ComputedValue { export class PromiseNamespace {} -export type PromiseMethodName = "all" | "allSettled" | "race" | "resolve" | "reject" +export type PromiseMethodName = "all" | "allSettled" | "race" | "any" | "resolve" | "reject" export class PromiseMethodReference { constructor(readonly name: PromiseMethodName) {} } +export type PromiseInstanceMethodName = "then" | "catch" | "finally" + +export class PromiseInstanceMethodReference { + constructor( + readonly promise: CodeModePromise, + readonly name: PromiseInstanceMethodName, + ) {} +} + +export class PromiseCapabilityFunction { + constructor(readonly settle: (value: unknown) => void) {} +} + export type GlobalNamespaceName = | "Object" | "Math" @@ -92,13 +105,15 @@ export class GlobalMethodReference { } export class CoercionFunction { - constructor(readonly name: "Number" | "String" | "Boolean" | "parseInt" | "parseFloat") {} + constructor(readonly name: "Number" | "String" | "Boolean" | "parseInt" | "parseFloat" | "isFinite" | "isNaN") {} } export class UriFunction { constructor(readonly name: "encodeURI" | "encodeURIComponent" | "decodeURI" | "decodeURIComponent") {} } +export class SearchFunction {} + export class ProgramThrow { constructor(readonly value: unknown) {} } @@ -122,11 +137,11 @@ export type DiagnosticKind = export const OptionalShortCircuit: unique symbol = Symbol("codemode.optional-short-circuit") export const supportedSyntaxMessage = - "Supported orchestration syntax: tools.* calls (they return promises - resolve them with await), data literals, destructuring, optional chaining, template literals, conditionals, switch, loops (incl. for...of and for...in over object/array/tools keys), arrow functions, spread, try/catch, array methods (map/filter/find/findIndex/some/every/reduce/flatMap/forEach/sort/slice/concat/indexOf/lastIndexOf/at/flat/reverse/includes/join), string methods (incl. match/matchAll/replace/split with regular expressions), Date/RegExp/Map/Set/URL/URLSearchParams, URI encoding helpers, Object/Math/JSON helpers, captured console.log/warn/error/dir/table, and Promise.all/allSettled/race/resolve/reject over arrays mixing promises and plain values for parallel tool calls (promise chaining with .then/.catch is not supported - use await with try/catch)." + "Supported orchestration syntax: tools.* calls (they return promises - resolve them with await), data literals, destructuring, optional chaining, template literals, conditionals, switch, loops (incl. for...of and for...in over object/array/tools keys), arrow functions, spread, try/catch, array methods (map/filter/find/findIndex/some/every/reduce/flatMap/forEach/sort/slice/concat/indexOf/lastIndexOf/at/flat/reverse/includes/join), string methods (incl. match/matchAll/replace/split with regular expressions), Date/RegExp/Map/Set/URL/URLSearchParams, URI encoding helpers, Object/Math/JSON helpers, captured console.log/warn/error/dir/table, Promise.all/allSettled/race/any/resolve/reject over arrays mixing promises and plain values for parallel tool calls, promise chaining with .then/.catch/.finally, and new Promise((resolve, reject) => ...) construction." export class InterpreterRuntimeError extends Error { readonly node?: AstNode - errorName: string = "Error" + errorName = "Error" constructor( message: string, diff --git a/packages/codemode/src/interpreter/promises.ts b/packages/codemode/src/interpreter/promises.ts new file mode 100644 index 0000000000..6f7c07c5b8 --- /dev/null +++ b/packages/codemode/src/interpreter/promises.ts @@ -0,0 +1,325 @@ +import { Cause, Deferred, Effect, Exit, Fiber, Scope } from "effect" +import type { Diagnostic } from "../codemode.js" +import type { SafeObject } from "../tool-runtime.js" +import { + type AstNode, + CodeModeFunction, + InterpreterRuntimeError, + ProgramThrow, + PromiseCapabilityFunction, + PromiseInstanceMethodReference, + PromiseMethodReference, +} from "./model.js" +import { caughtErrorValue, normalizeError } from "./errors.js" +import { applyCollectionCallback, isSupportedCallback, type CallbackRunner, type SupportedCallback } from "./methods.js" +import { typeofValue } from "./references.js" +import { spreadItems } from "../stdlib/collections.js" +import { createAggregateErrorValue } from "../stdlib/value.js" +import { CodeModePromise } from "../values.js" + +// Observation only controls rejection reporting; program completion interrupts all promise work. +export class PromiseRuntime { + private readonly active = new Set() + private readonly ids = new WeakMap() + private readonly observed = new WeakSet() + private readonly failures = new Map() + private nextID = 0 + + constructor(private readonly scope: Scope.Scope) {} + + create(effect: Effect.Effect): Effect.Effect { + return Effect.suspend(() => { + // Allocate before forking so reruns get distinct IDs and diagnostics retain creation order. + const id = this.nextID++ + return Effect.map(Effect.forkIn(effect, this.scope, { startImmediately: true }), (fiber) => { + const promise = new CodeModePromise(fiber) + this.active.add(promise) + this.ids.set(promise, id) + fiber.addObserver((exit) => { + this.active.delete(promise) + if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause) || this.observed.has(promise)) { + this.ids.delete(promise) + return + } + const failure = normalizeError(Cause.squash(exit.cause)) + this.failures.set(id, { + ...failure, + message: `Unhandled rejection from an un-awaited promise: ${failure.message}`, + }) + }) + return promise + }) + }) + } + + // Observation must be recorded when responsibility transfers, before the consumer fiber runs. + markObserved(promise: CodeModePromise): void { + this.observed.add(promise) + const id = this.ids.get(promise) + this.ids.delete(promise) + if (id !== undefined) this.failures.delete(id) + } + + await(promise: CodeModePromise): Effect.Effect> { + return Fiber.await(promise.fiber) + } + + diagnostics(): Array { + return [...this.failures].sort(([left], [right]) => left - right).map(([, failure]) => failure) + } + + // Re-check because a straggler can create promises before its interruption lands. + interrupt(): Effect.Effect> { + const self = this + return Effect.gen(function* () { + while (self.active.size > 0) { + yield* Fiber.interruptAll([...self.active].map((promise) => promise.fiber)) + } + return self.diagnostics() + }) + } +} + +export const selfResolutionError = (node?: AstNode): InterpreterRuntimeError => + new InterpreterRuntimeError("Chaining cycle detected: a promise cannot resolve with itself.", node).as("TypeError") + +export const invokePromiseMethod = ( + runner: CallbackRunner, + promises: PromiseRuntime, + ref: PromiseMethodReference, + args: Array, + node: AstNode, +): Effect.Effect => { + if (ref.name === "resolve") { + const value = args[0] + return value instanceof CodeModePromise ? Effect.succeed(value) : promises.create(Effect.succeed(value)) + } + if (ref.name === "reject") { + return promises.create(Effect.fail(new ProgramThrow(args[0]))) + } + + const spread = spreadItems(args[0]) + if (spread === undefined) { + return promises.create( + Effect.fail( + new InterpreterRuntimeError( + `Promise.${ref.name} expects an array of promises or plain values (e.g. Promise.${ref.name}(items.map((item) => tools.ns.tool(item)))).`, + node, + ).as("TypeError"), + ), + ) + } + const items = Array.from(spread) + + for (const item of items) { + if (item instanceof CodeModePromise) promises.markObserved(item) + } + + switch (ref.name) { + case "all": { + const observations = items.map((item) => + item instanceof CodeModePromise ? Effect.flatten(promises.await(item)) : Effect.succeed(item), + ) + return promises.create(settleAfterTurn(Effect.all(observations, { concurrency: "unbounded" }))) + } + case "allSettled": { + const observations = items.map((item) => + item instanceof CodeModePromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)), + ) + return promises.create( + settleAfterTurn( + Effect.gen(function* () { + const outcomes: Array = [] + for (const observation of observations) { + const exit = yield* observation + if (Exit.isSuccess(exit)) { + outcomes.push( + Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }), + ) + continue + } + if (Cause.hasInterruptsOnly(exit.cause)) { + // Teardown interruption is not a program-level rejection. + return yield* Effect.failCause(exit.cause) + } + outcomes.push( + Object.assign(Object.create(null) as SafeObject, { + status: "rejected", + reason: caughtErrorValue(Cause.squash(exit.cause)), + }), + ) + } + return outcomes + }), + ), + ) + } + case "race": { + if (items.length === 0) { + return promises.create( + Effect.fail( + new InterpreterRuntimeError( + "Promise.race([]) would never settle; provide at least one promise or value.", + node, + ), + ), + ) + } + const observations = items.map((item) => + item instanceof CodeModePromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)), + ) + return promises.create(settleAfterTurn(Effect.flatten(Effect.raceAll(observations)))) + } + case "any": { + const flipped = items.map((item) => + item instanceof CodeModePromise + ? Effect.flatMap(promises.await(item), (exit) => { + if (Exit.isSuccess(exit)) return Effect.fail(new PromiseAnyFulfilled(exit.value)) + if (Cause.hasInterruptsOnly(exit.cause)) return Effect.failCause(exit.cause) + return Effect.succeed(caughtErrorValue(Cause.squash(exit.cause))) + }) + : Effect.fail(new PromiseAnyFulfilled(item)), + ) + const body = Effect.all(flipped, { concurrency: "unbounded" }).pipe( + Effect.flatMap((reasons) => + Effect.fail(new ProgramThrow(createAggregateErrorValue(reasons, "All promises were rejected"))), + ), + Effect.catch((error) => + error instanceof PromiseAnyFulfilled ? Effect.succeed(error.value) : Effect.fail(error), + ), + ) + return promises.create(settleAfterTurn(body)) + } + } +} + +export const invokePromiseInstanceMethod = ( + runner: CallbackRunner, + promises: PromiseRuntime, + ref: PromiseInstanceMethodReference, + args: Array, + node: AstNode, +): Effect.Effect => { + const method = `Promise.prototype.${ref.name}` + promises.markObserved(ref.promise) + if (ref.name === "finally") { + return chainFinally(runner, promises, ref.promise, reactionHandler(args[0], method, node), method, node) + } + const onFulfilled = ref.name === "then" ? reactionHandler(args[0], method, node) : undefined + const onRejected = reactionHandler(ref.name === "then" ? args[1] : args[0], method, node) + return chainReaction(runner, promises, ref.promise, onFulfilled, onRejected, method, node) +} + +export const constructPromise = ( + runner: CallbackRunner, + promises: PromiseRuntime, + executor: unknown, + node: AstNode, +): Effect.Effect => { + if (!(executor instanceof CodeModeFunction)) { + throw new InterpreterRuntimeError( + "new Promise(...) expects an executor function (e.g. new Promise((resolve, reject) => { ... })).", + node, + ).as("TypeError") + } + return Effect.gen(function* () { + const deferred = Deferred.makeUnsafe() + const box: { own?: CodeModePromise } = {} + const promise = yield* promises.create( + Effect.flatMap(Deferred.await(deferred), (value) => { + if (!(value instanceof CodeModePromise)) return Effect.succeed(value) + if (value === box.own) return Effect.fail(selfResolutionError(node)) + return runner.settlePromise(value) + }), + ) + box.own = promise + const resolve = new PromiseCapabilityFunction((value) => { + Deferred.doneUnsafe(deferred, Exit.succeed(value)) + }) + const reject = new PromiseCapabilityFunction((value) => { + Deferred.doneUnsafe(deferred, Exit.fail(new ProgramThrow(value))) + }) + const executed = yield* Effect.exit(runner.invokeFunction(executor, [resolve, reject])) + if (!Exit.isSuccess(executed)) { + if (Cause.hasInterruptsOnly(executed.cause)) return yield* Effect.failCause(executed.cause) + Deferred.doneUnsafe(deferred, Exit.fail(Cause.squash(executed.cause))) + } + return promise + }) +} + +// Settle one reaction turn after the deciding member, after its existing reactions. +const settleAfterTurn = (body: Effect.Effect): Effect.Effect => + Effect.flatMap(Effect.exit(body), (exit) => Effect.andThen(Effect.yieldNow, exit)) + +class PromiseAnyFulfilled { + constructor(readonly value: unknown) {} +} + +const reactionHandler = (value: unknown, method: string, node: AstNode): SupportedCallback | undefined => { + if (isSupportedCallback(value)) return value + if (typeofValue(value) === "function") { + throw new InterpreterRuntimeError( + `${method} cannot use this callable as a handler; wrap it in an arrow function, e.g. (value) => tools.ns.tool(value).`, + node, + ) + } + return undefined +} + +// Teardown bypasses handlers; settled reactions yield once so handlers never run inline. +const reactionExit = ( + promises: PromiseRuntime, + source: CodeModePromise, +): Effect.Effect, unknown, R> => + Effect.gen(function* () { + const exit = yield* promises.await(source) + if (!Exit.isSuccess(exit) && Cause.hasInterruptsOnly(exit.cause)) return yield* Effect.failCause(exit.cause) + yield* Effect.yieldNow + return exit + }) + +const chainReaction = ( + runner: CallbackRunner, + promises: PromiseRuntime, + source: CodeModePromise, + onFulfilled: SupportedCallback | undefined, + onRejected: SupportedCallback | undefined, + method: string, + node: AstNode, +): Effect.Effect => { + const box: { derived?: CodeModePromise } = {} + const body = Effect.gen(function* () { + const exit = yield* reactionExit(promises, source) + const handler = Exit.isSuccess(exit) ? onFulfilled : onRejected + if (handler === undefined) return yield* exit + const input = Exit.isSuccess(exit) ? exit.value : caughtErrorValue(Cause.squash(exit.cause)) + const result = yield* applyCollectionCallback(runner, handler, method, node)([input]) + if (result === box.derived) return yield* Effect.fail(selfResolutionError(node)) + if (result instanceof CodeModePromise) return yield* runner.settlePromise(result) + return result + }) + return Effect.map(promises.create(body), (derived) => { + box.derived = derived + return derived + }) +} + +const chainFinally = ( + runner: CallbackRunner, + promises: PromiseRuntime, + source: CodeModePromise, + cleanup: SupportedCallback | undefined, + method: string, + node: AstNode, +): Effect.Effect => + promises.create( + Effect.gen(function* () { + const exit = yield* reactionExit(promises, source) + if (cleanup !== undefined) { + const result = yield* applyCollectionCallback(runner, cleanup, method, node)([]) + if (result instanceof CodeModePromise) yield* runner.settlePromise(result) + } + return yield* exit + }), + ) diff --git a/packages/codemode/src/interpreter/references.ts b/packages/codemode/src/interpreter/references.ts new file mode 100644 index 0000000000..b2ac2d22a2 --- /dev/null +++ b/packages/codemode/src/interpreter/references.ts @@ -0,0 +1,127 @@ +import { + type AstNode, + CodeModeFunction, + CoercionFunction, + ErrorConstructorReference, + GlobalMethodReference, + GlobalNamespace, + InterpreterRuntimeError, + IntrinsicReference, + PromiseCapabilityFunction, + PromiseInstanceMethodReference, + PromiseMethodReference, + PromiseNamespace, + SearchFunction, + UriFunction, +} from "./model.js" +import { ToolReference } from "../tool-runtime.js" +import { isCodeModeValue, CodeModePromise } from "../values.js" + +export const isRuntimeReference = (value: unknown): boolean => + value instanceof CodeModeFunction || + value instanceof ToolReference || + value instanceof IntrinsicReference || + value instanceof GlobalNamespace || + value instanceof GlobalMethodReference || + value instanceof PromiseNamespace || + value instanceof PromiseMethodReference || + value instanceof PromiseInstanceMethodReference || + value instanceof CodeModePromise || + value instanceof CoercionFunction || + value instanceof UriFunction || + value instanceof SearchFunction || + value instanceof PromiseCapabilityFunction || + value instanceof ErrorConstructorReference || + isCodeModeValue(value) + +function* childValues(value: object): Generator { + if (Array.isArray(value)) { + const length = value.length + for (let index = 0; index < length; index++) yield value[index] + return + } + yield* Object.values(value) +} + +export const containsRuntimeReference = (value: unknown): boolean => { + const pending: Array> = [[value].values()] + const seen = new Set() + while (pending.length > 0) { + const next = pending.at(-1)!.next() + if (next.done) { + pending.pop() + continue + } + const current = next.value + if (isRuntimeReference(current)) return true + if (current === null || typeof current !== "object" || seen.has(current)) continue + seen.add(current) + pending.push(childValues(current)) + } + return false +} + +// CodeMode values are data here, not opaque interpreter references. +export const containsOpaqueReference = (value: unknown): boolean => { + const pending: Array> = [[value].values()] + const seen = new Set() + while (pending.length > 0) { + const next = pending.at(-1)!.next() + if (next.done) { + pending.pop() + continue + } + const current = next.value + if (isCodeModeValue(current)) continue + if (isRuntimeReference(current)) return true + if (current === null || typeof current !== "object" || seen.has(current)) continue + seen.add(current) + pending.push(childValues(current)) + } + return false +} + +// Reject cycles before mutation so later boundary walks remain safe. +export const rejectCircularInsertion = ( + container: object, + value: unknown, + label: string, + node: AstNode, +): void => { + const pending: Array> = [[value].values()] + const seen = new Set() + while (pending.length > 0) { + const next = pending.at(-1)!.next() + if (next.done) { + pending.pop() + continue + } + const current = next.value + if (current === container) + throw new InterpreterRuntimeError(`${label} contains a circular value.`, node, "InvalidDataValue") + if (current === null || typeof current !== "object" || isRuntimeReference(current) || seen.has(current)) continue + seen.add(current) + pending.push(Array.isArray(current) ? current[Symbol.iterator]() : childValues(current)) + } +} + +export const typeofValue = (value: unknown): string => { + if ( + value instanceof CodeModeFunction || + value instanceof CoercionFunction || + value instanceof IntrinsicReference || + value instanceof GlobalMethodReference || + value instanceof PromiseMethodReference || + value instanceof PromiseInstanceMethodReference || + value instanceof PromiseNamespace || + value instanceof PromiseCapabilityFunction || + value instanceof ErrorConstructorReference + ) + return "function" + if (value instanceof UriFunction || value instanceof SearchFunction) return "function" + if (value instanceof ToolReference) return value.path.length > 0 ? "function" : "object" + if (value instanceof GlobalNamespace) { + return value.name === "Math" || value.name === "JSON" || value.name === "console" ? "object" : "function" + } + return typeof value +} diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index 093f577765..5bf574014c 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -1,26 +1,5 @@ -import { parse } from "acorn" -import { Cause, Effect, Exit, Fiber, Semaphore } from "effect" -import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript" -import { - copyIn, - copyOut, - isBlockedMember, - ToolReference, - ToolRuntime, - ToolRuntimeError, - type HostTools, - type SafeObject, - type Services, -} from "../tool-runtime.js" -import { ToolError } from "../tool-error.js" -import type { - DataValue, - Diagnostic, - DiagnosticKind, - ExecuteOptions, - ResolvedExecutionLimits, - Result, -} from "../codemode.js" +import { Cause, Effect } from "effect" +import { isBlockedMember, ToolReference, ToolRuntimeError, type SafeObject } from "../tool-runtime.js" import { type AstNode, asNode, @@ -32,7 +11,6 @@ import { GlobalMethodReference, GlobalNamespace, type GlobalNamespaceName, - formatLocation, getArray, getBoolean, getNode, @@ -43,41 +21,40 @@ import { isRecord, type MemberReference, OptionalShortCircuit, + PromiseCapabilityFunction, + PromiseInstanceMethodReference, PromiseMethodReference, type PromiseMethodName, PromiseNamespace, ProgramThrow, type ProgramNode, + SearchFunction, type StatementResult, - sourceLocation, supportedSyntaxMessage, unsupportedSyntax, UriFunction, } from "./model.js" +import { caughtErrorValue, constructErrorValue } from "./errors.js" +import { arrayStatics, type CallbackRunner, invokeArrayFrom, invokeGlobalMethod, invokeIntrinsic } from "./methods.js" +import { + constructPromise, + invokePromiseInstanceMethod, + invokePromiseMethod, + PromiseRuntime, + selfResolutionError, +} from "./promises.js" +import { containsOpaqueReference, isRuntimeReference, rejectCircularInsertion, typeofValue } from "./references.js" +import { ScopeStack } from "./scope.js" import { arrayMethods, mapMethods, setMethods, spreadItems } from "../stdlib/collections.js" -import { consoleMethods, MAX_CONSOLE_DEPTH } from "../stdlib/console.js" -import { dateMethods, dateStatics, invokeDateMethod, invokeDateStatic } from "../stdlib/date.js" -import { invokeJsonMethod } from "../stdlib/json.js" -import { invokeMathMethod, mathConstants } from "../stdlib/math.js" -import { - invokeNumberMethod, - invokeNumberStatic, - numberConstants, - numberMethods, - numberStatics, -} from "../stdlib/number.js" -import { invokeObjectMethod } from "../stdlib/object.js" -import { promiseStatics, TOOL_CALL_CONCURRENCY } from "../stdlib/promise.js" -import { - escapeRegexHint, - invokeRegExpMethod, - matchToValue, - regexpMethods, - regexpProperties, - regexFailureReason, - toHostRegex, -} from "../stdlib/regexp.js" -import { invokeStringStatic, stringMethods, stringStatics } from "../stdlib/string.js" +import { consoleMethods, formatConsoleMessage } from "../stdlib/console.js" +import { dateMethods, dateStatics } from "../stdlib/date.js" +import { jsonStatics } from "../stdlib/json.js" +import { mathConstants, mathMethods } from "../stdlib/math.js" +import { numberConstants, numberMethods, numberStatics } from "../stdlib/number.js" +import { objectMethodsPreservingIdentity, objectStatics } from "../stdlib/object.js" +import { promiseStatics } from "../stdlib/promise.js" +import { escapeRegexHint, regexpMethods, regexpProperties, regexFailureReason } from "../stdlib/regexp.js" +import { stringMethods, stringStatics } from "../stdlib/string.js" import { urlMethods, urlProperties, @@ -85,8 +62,6 @@ import { urlStatics, urlWritableProperties, invokeUriFunction, - invokeURLMethod, - invokeURLStatic, uriArgument, urlArgument, } from "../stdlib/url.js" @@ -95,206 +70,48 @@ import { coerceToNumber, coerceToString, compoundOperators, - createErrorValue, errorBrandName, errorConstructors, invokeCoercion, valueConstructors, } from "../stdlib/value.js" import { - isSandboxValue, - SandboxDate, - SandboxMap, - SandboxPromise, - SandboxRegExp, - SandboxSet, - SandboxURL, - SandboxURLSearchParams, + isCodeModeValue, + CodeModeDate, + CodeModeMap, + CodeModePromise, + CodeModeRegExp, + CodeModeSet, + CodeModeURL, + CodeModeURLSearchParams, } from "../values.js" -const parseProgram = (code: string): ProgramNode => { - const transpiled = transpileModule(`async function __codemode__() {\n${code}\n}`, { - reportDiagnostics: true, - compilerOptions: { - target: ScriptTarget.ESNext, - module: ModuleKind.ESNext, - }, - }) - const diagnostic = transpiled.diagnostics?.find((item) => item.category === DiagnosticCategory.Error) - - if (diagnostic) { - throw new InterpreterRuntimeError( - `Failed to parse TypeScript: ${flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`, - undefined, - "ParseError", - ) - } - - const bodyStart = transpiled.outputText.indexOf("{") + 1 - const bodyEnd = transpiled.outputText.lastIndexOf("}") - const executableCode = transpiled.outputText.slice(bodyStart, bodyEnd) - const parsed = parse(executableCode, { - ecmaVersion: "latest", - sourceType: "script", - allowReturnOutsideFunction: true, - allowAwaitOutsideFunction: true, - locations: true, - }) as unknown - - if (!isRecord(parsed) || parsed.type !== "Program" || !Array.isArray(parsed.body)) { - throw new InterpreterRuntimeError("Failed to parse script as a Program node.") - } - - return parsed as ProgramNode +const globalStaticMembers: Partial>> = { + Object: objectStatics, + Math: mathMethods, + JSON: jsonStatics, + Array: arrayStatics, + console: consoleMethods, + Date: dateStatics, + URL: urlStatics, } -const publicErrorMessage = (message: string): string => - message.replace(/\/(?:Users|home|private|tmp|var\/folders)\/[^\s"'`]+/g, "") - -const normalizeError = (error: unknown): Diagnostic => { - if (error instanceof InterpreterRuntimeError) { - return { - kind: error.kind, - message: `${error.message}${formatLocation(error.node)}`, - ...(error.node?.loc ? { location: sourceLocation(error.node) } : {}), - ...(error.suggestions ? { suggestions: error.suggestions } : {}), - } - } - - if (error instanceof ToolRuntimeError) { - return { - kind: error.kind, - message: error.message, - ...(error.suggestions.length > 0 ? { suggestions: error.suggestions } : {}), - } - } - - if (error instanceof ToolError) { - return { kind: "ToolFailure", message: publicErrorMessage(error.message) } - } - - if (error instanceof ProgramThrow) { - const value = error.value - let message: string - if (containsRuntimeReference(value)) { - // A thrown tool/function reference must not leak its internal structure. - message = "a non-data value" - } else if (typeof value === "string") { - message = value - } else if ( - value !== null && - typeof value === "object" && - typeof (value as { message?: unknown }).message === "string" - ) { - message = (value as { message: string }).message - } else { - try { - message = JSON.stringify(copyOut(value)) ?? String(value) - } catch { - message = String(value) - } - } - return { kind: "ExecutionFailure", message: `Uncaught: ${message}` } - } - - if (error instanceof RangeError && /call stack|recursion/i.test(error.message)) { - return { - kind: "ExecutionFailure", - message: "Execution exceeded the maximum nesting depth.", - } - } - - if (error instanceof Error) { - return { - kind: error.name === "SyntaxError" ? "ParseError" : "ExecutionFailure", - message: publicErrorMessage(error.message), - } - } - - // A non-Error thrown by a host tool (raw string / number / Symbol) still routes through - // path redaction so filesystem paths can never leak through the catch-all branch. - return { - kind: "ExecutionFailure", - message: publicErrorMessage(String(error)), +const calleeDescription = (callee: AstNode): string => { + if (callee.type === "Identifier") return getString(callee, "name") + if (callee.type === "MemberExpression") { + const object = getNode(callee, "object") + const property = getNode(callee, "property") + const key = + callee.computed !== true && property.type === "Identifier" + ? getString(property, "name") + : property.type === "Literal" && typeof property.value === "string" + ? property.value + : undefined + if (object.type === "Identifier" && key !== undefined) return `${getString(object, "name")}.${key}` } + return "The called value" } -// Shared by catch bindings, Promise.allSettled rejection reasons, and Promise.race losers. -const caughtErrorValue = (thrown: unknown): unknown => { - if (thrown instanceof ProgramThrow) return thrown.value - if (thrown instanceof InterpreterRuntimeError) return createErrorValue(thrown.errorName, thrown.message) - const name = thrown instanceof Error && errorConstructors.has(thrown.name) ? thrown.name : "Error" - return createErrorValue(name, normalizeError(thrown).message) -} - -const isRuntimeReference = (value: unknown): boolean => - value instanceof CodeModeFunction || - value instanceof ToolReference || - value instanceof IntrinsicReference || - value instanceof GlobalNamespace || - value instanceof GlobalMethodReference || - value instanceof PromiseNamespace || - value instanceof PromiseMethodReference || - value instanceof SandboxPromise || - value instanceof CoercionFunction || - value instanceof UriFunction || - value instanceof ErrorConstructorReference || - isSandboxValue(value) - -const containsRuntimeReference = (value: unknown, seen = new Set()): boolean => { - if (isRuntimeReference(value)) return true - if (value === null || typeof value !== "object") return false - if (seen.has(value)) return false - seen.add(value) - const contains = Array.isArray(value) - ? value.some((item) => containsRuntimeReference(item, seen)) - : Object.values(value).some((item) => containsRuntimeReference(item, seen)) - seen.delete(value) - return contains -} - -// Like containsRuntimeReference, but sandbox standard-library values count as data: -// operators and switch treat them as ordinary object operands (identity equality, ToPrimitive -// coercion) rather than rejecting them as opaque interpreter machinery. -const containsOpaqueReference = (value: unknown, seen = new Set()): boolean => { - if (isSandboxValue(value)) return false - if (isRuntimeReference(value)) return true - if (value === null || typeof value !== "object") return false - if (seen.has(value)) return false - seen.add(value) - const contains = Array.isArray(value) - ? value.some((item) => containsOpaqueReference(item, seen)) - : Object.values(value).some((item) => containsOpaqueReference(item, seen)) - seen.delete(value) - return contains -} - -// `typeof` never throws in JS; map every interpreter value to its JS-visible category. -// A SandboxPromise falls through to the final `typeof value` and reports "object", exactly -// like a real JS promise. -const typeofValue = (value: unknown): string => { - if ( - value instanceof CodeModeFunction || - value instanceof CoercionFunction || - value instanceof IntrinsicReference || - value instanceof GlobalMethodReference || - value instanceof PromiseMethodReference || - value instanceof PromiseNamespace || - value instanceof ErrorConstructorReference - ) - return "function" - if (value instanceof UriFunction) return "function" - if (value instanceof ToolReference) return value.path.length > 0 ? "function" : "object" - if (value instanceof GlobalNamespace) { - return value.name === "Math" || value.name === "JSON" || value.name === "console" ? "object" : "function" - } - return typeof value -} - -// `x instanceof C` against the constructors CodeMode knows. Like `typeof`, it observes any -// left-hand value (opaque references included) without coercing it. Error checks use the -// error brand: `instanceof Error` accepts every branded error; a specific error type matches -// its own brand only (as in JS, where TypeError instances are also Error instances). const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean => { if (rhs instanceof ErrorConstructorReference) { const brand = errorBrandName(lhs) @@ -303,26 +120,24 @@ const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean => if (rhs instanceof GlobalNamespace) { switch (rhs.name) { case "Date": - return lhs instanceof SandboxDate + return lhs instanceof CodeModeDate case "RegExp": - return lhs instanceof SandboxRegExp + return lhs instanceof CodeModeRegExp case "Map": - return lhs instanceof SandboxMap + return lhs instanceof CodeModeMap case "Set": - return lhs instanceof SandboxSet + return lhs instanceof CodeModeSet case "URL": - return lhs instanceof SandboxURL + return lhs instanceof CodeModeURL case "URLSearchParams": - return lhs instanceof SandboxURLSearchParams + return lhs instanceof CodeModeURLSearchParams case "Array": return Array.isArray(lhs) case "Object": return lhs !== null && (typeof lhs === "object" || typeofValue(lhs) === "function") } } - if (rhs instanceof PromiseNamespace) return lhs instanceof SandboxPromise - // Number/String/Boolean wrap primitives in JS; no boxed values exist in CodeMode, so - // `x instanceof Number` is always false - exactly what it is for primitives in JS. + if (rhs instanceof PromiseNamespace) return lhs instanceof CodeModePromise if (rhs instanceof CoercionFunction && (rhs.name === "Number" || rhs.name === "String" || rhs.name === "Boolean")) { return false } @@ -332,247 +147,6 @@ const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean => ) } -const invokeStringMethod = (value: string, name: string, args: Array, node: AstNode): unknown => { - const str = (index: number): string => { - const arg = args[index] - if (typeof arg !== "string") - throw new InterpreterRuntimeError(`String.${name} expects argument ${index + 1} to be a string.`, node) - return arg - } - const num = (index: number): number => { - const arg = args[index] - if (typeof arg !== "number") - throw new InterpreterRuntimeError(`String.${name} expects argument ${index + 1} to be a number.`, node) - return arg - } - const optNum = (index: number): number | undefined => (args[index] === undefined ? undefined : num(index)) - const optStr = (index: number): string | undefined => (args[index] === undefined ? undefined : str(index)) - - let result: unknown - switch (name) { - case "toLowerCase": - result = value.toLowerCase() - break - case "toUpperCase": - result = value.toUpperCase() - break - case "trim": - result = value.trim() - break - // trimLeft/trimRight are the legacy aliases of trimStart/trimEnd, kept because models write them. - case "trimStart": - case "trimLeft": - result = value.trimStart() - break - case "trimEnd": - case "trimRight": - result = value.trimEnd() - break - // Locale/options arguments are ignored: comparison runs with the host default locale, and - // the common use is a sort comparator where any consistent order works. - case "localeCompare": - result = value.localeCompare(str(0)) - break - case "normalize": { - const form = optStr(0) - try { - result = value.normalize(form) - } catch { - throw new InterpreterRuntimeError( - `String.normalize expects the form "NFC", "NFD", "NFKC", or "NFKD" (got ${JSON.stringify(form)}).`, - node, - ).as("RangeError") - } - break - } - case "split": { - if (args.length === 0) { - result = [value] - break - } - if (args[0] instanceof SandboxRegExp) { - result = value.split((args[0] as SandboxRegExp).regex, optNum(1)) - break - } - const requestedLimit = optNum(1) - result = value.split(str(0), requestedLimit === undefined ? undefined : requestedLimit >>> 0) - break - } - case "slice": - result = value.slice(optNum(0), optNum(1)) - break - case "includes": - result = value.includes(str(0), optNum(1)) - break - case "startsWith": - result = value.startsWith(str(0), optNum(1)) - break - case "endsWith": - result = value.endsWith(str(0), optNum(1)) - break - case "indexOf": - result = value.indexOf(str(0), optNum(1)) - break - case "lastIndexOf": - result = value.lastIndexOf(str(0), optNum(1)) - break - case "replace": - case "replaceAll": { - if (args[0] instanceof SandboxRegExp) { - const pattern = (args[0] as SandboxRegExp).regex - const replacement = str(1) - if (name === "replaceAll" && !pattern.global) { - throw new InterpreterRuntimeError( - `String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.replace to replace only the first match.`, - node, - ) - } - result = name === "replace" ? value.replace(pattern, replacement) : value.replaceAll(pattern, replacement) - break - } - if (name === "replace") { - result = value.replace(str(0), str(1)) - break - } - result = value.replaceAll(str(0), str(1)) - break - } - case "match": { - const pattern = toHostRegex(args[0], name, node) - const matched = value.match(pattern) - if (matched === null) return null - // A global match is a plain array of matched strings; a non-global match carries - // index/groups own properties, so bypass the copying data checkpoint to keep them. - if (pattern.global) return boundedData(matched, "String.match result") - return matchToValue(matched) - } - case "matchAll": { - const pattern = toHostRegex(args[0], name, node, "g") - if (!pattern.global) { - throw new InterpreterRuntimeError( - `String.matchAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.match for a single match.`, - node, - ) - } - // Materialized as an array (not an iterator); each entry is a match array with - // index/groups own properties. Match count is bounded by the subject length. - return Array.from(value.matchAll(pattern), matchToValue) - } - case "search": { - result = value.search(toHostRegex(args[0], name, node)) - break - } - case "repeat": { - const count = num(0) - if (!Number.isFinite(count) || count < 0) - throw new InterpreterRuntimeError("String.repeat expects a finite non-negative count.", node) - result = value.repeat(count) - break - } - case "padStart": - result = value.padStart(num(0), optStr(1)) - break - case "padEnd": - result = value.padEnd(num(0), optStr(1)) - break - case "charAt": - result = value.charAt(optNum(0) ?? 0) - break - case "at": - result = value.at(optNum(0) ?? 0) - break - case "substring": - result = value.substring(optNum(0) ?? 0, optNum(1)) - break - case "substr": - result = value.substr(optNum(0) ?? 0, optNum(1)) - break - // JS charCodeAt returns NaN out of range; NaN flows as an ordinary in-sandbox value - // (normalized to null only at the data boundary - see copyOut), so return it as-is. - case "charCodeAt": - result = value.charCodeAt(optNum(0) ?? 0) - break - case "codePointAt": - result = value.codePointAt(optNum(0) ?? 0) - break - case "toString": - result = value - break - case "concat": { - result = value.concat(...args.map((_, index) => str(index))) - break - } - default: - throw new InterpreterRuntimeError(`String method '${name}' is not available in CodeMode.`, node) - } - return boundedData(result, `String.${name} result`) -} - -const invokeArrayStatic = (name: string, args: Array, node: AstNode): unknown => { - switch (name) { - case "isArray": - return Array.isArray(args[0]) - case "of": - return [...args] - case "from": { - if (args.length > 1) { - throw new InterpreterRuntimeError( - "Array.from(...) does not support a map function in CodeMode; call .map() on the result instead.", - node, - "UnsupportedSyntax", - [supportedSyntaxMessage], - ) - } - // Map/Set materialize directly (the data checkpoint would serialize them to {}). - if (args[0] instanceof SandboxMap) - return Array.from((args[0] as SandboxMap).map.entries(), ([key, item]) => [key, item]) - if (args[0] instanceof SandboxSet) return Array.from((args[0] as SandboxSet).set.values()) - if (args[0] instanceof SandboxURLSearchParams) { - return Array.from(args[0].params.entries(), ([key, value]) => [key, value]) - } - const source = boundedData(args[0], "Array.from input") - if (typeof source === "string") return Array.from(source) - if (Array.isArray(source)) return [...source] - if ( - source !== null && - typeof source === "object" && - typeof (source as { length?: unknown }).length === "number" - ) { - return Array.from(source as ArrayLike) - } - throw new InterpreterRuntimeError("Array.from expects an array, string, Map, Set, or array-like value.", node) - } - default: - throw new InterpreterRuntimeError(`Array.${name} is not available in CodeMode.`, node) - } -} - -const invokeGlobalMethod = (ref: GlobalMethodReference, args: Array, node: AstNode): unknown => { - if (ref.namespace === "console") - throw new InterpreterRuntimeError(`console.${ref.name} is not available in CodeMode.`, node) - if (ref.namespace === "Object") return invokeObjectMethod(ref.name, args, node) - if (ref.namespace === "Math") return invokeMathMethod(ref.name, args, node) - if (ref.namespace === "Array") return invokeArrayStatic(ref.name, args, node) - if (ref.namespace === "Number") return invokeNumberStatic(ref.name, args, node) - if (ref.namespace === "String") return invokeStringStatic(ref.name, args, node) - if (ref.namespace === "URL") return invokeURLStatic(ref.name, args, node) - if (ref.namespace === "Date") { - if (!dateStatics.has(ref.name)) - throw new InterpreterRuntimeError(`Date.${ref.name} is not available in CodeMode.`, node) - return invokeDateStatic(ref.name, args, node) - } - if ( - ref.namespace === "RegExp" || - ref.namespace === "Map" || - ref.namespace === "Set" || - ref.namespace === "URLSearchParams" - ) { - throw new InterpreterRuntimeError(`${ref.namespace}.${ref.name} is not available in CodeMode.`, node) - } - return invokeJsonMethod(ref.name, args, node) -} - -// Every identifier a parameter pattern binds, used to seed TDZ slots before defaults run. const collectPatternNames = (pattern: AstNode, out: Array = []): Array => { switch (pattern.type) { case "Identifier": @@ -599,34 +173,49 @@ const collectPatternNames = (pattern: AstNode, out: Array = []): Array { - private scopes: Array> +const loopDeclaration = (left: AstNode, statement: "for...of" | "for...in") => { + if (left.type !== "VariableDeclaration") return undefined + const declarations = getArray(left, "declarations") + if (declarations.length !== 1) { + throw new InterpreterRuntimeError(`${statement} supports one declared binding.`, left) + } + const kind = getString(left, "kind") + return { + pattern: getNode(asNode(declarations[0], "declarations[0]"), "id"), + mutable: kind !== "const", + lexical: kind !== "var", + } +} + +export class Interpreter { + private scopes: ScopeStack private readonly invokeTool: (path: ReadonlyArray, args: Array) => Effect.Effect - // Enumerable namespace/tool names at a node of the host tool tree, threaded from - // ToolRuntime.make like invokeTool: the interpreter never holds the tree itself. + private readonly invokeSearch: (args: Array) => Effect.Effect private readonly toolKeys: (path: ReadonlyArray) => ReadonlyArray private readonly logs: Array - private lastValue: unknown - // Caps how many eagerly forked tool calls run at once (the parallel-call concurrency cap). - private readonly callPermits: Semaphore.Semaphore - // Fiber-backed promises whose settlement no program construct has observed yet. Successful - // program completion drains these (like a runtime waiting on in-flight work at exit) and - // surfaces a never-awaited failure as an unhandled-rejection diagnostic. - private readonly pendingSettlements = new Set() + private readonly promises: PromiseRuntime + private readonly runner: CallbackRunner = { + invokeFunction: (fn, args) => this.invokeFunction(fn, args), + invokeCallable: (callable, args, node) => this.invokeCallable(callable, args, node), + settlePromise: (promise) => this.settlePromise(promise), + } constructor( invokeTool: (path: ReadonlyArray, args: Array) => Effect.Effect, + invokeSearch: (args: Array) => Effect.Effect, toolKeys: (path: ReadonlyArray) => ReadonlyArray, + promises: PromiseRuntime, logs: Array = [], ) { const globalScope = new Map() - this.scopes = [globalScope] + this.scopes = new ScopeStack([globalScope]) this.invokeTool = invokeTool + this.invokeSearch = invokeSearch this.toolKeys = toolKeys this.logs = logs - this.lastValue = undefined - this.callPermits = Semaphore.makeUnsafe(TOOL_CALL_CONCURRENCY) + this.promises = promises globalScope.set("tools", { mutable: false, value: new ToolReference([]) }) + globalScope.set("search", { mutable: false, value: new SearchFunction() }) globalScope.set("Promise", { mutable: false, value: new PromiseNamespace() }) globalScope.set("undefined", { mutable: false, value: undefined }) globalScope.set("Object", { mutable: false, value: new GlobalNamespace("Object") }) @@ -639,6 +228,8 @@ class Interpreter { globalScope.set("console", { mutable: false, value: new GlobalNamespace("console") }) globalScope.set("parseInt", { mutable: false, value: new CoercionFunction("parseInt") }) globalScope.set("parseFloat", { mutable: false, value: new CoercionFunction("parseFloat") }) + globalScope.set("isFinite", { mutable: false, value: new CoercionFunction("isFinite") }) + globalScope.set("isNaN", { mutable: false, value: new CoercionFunction("isNaN") }) globalScope.set("Date", { mutable: false, value: new GlobalNamespace("Date") }) globalScope.set("RegExp", { mutable: false, value: new GlobalNamespace("RegExp") }) globalScope.set("Map", { mutable: false, value: new GlobalNamespace("Map") }) @@ -649,136 +240,69 @@ class Interpreter { globalScope.set("encodeURIComponent", { mutable: false, value: new UriFunction("encodeURIComponent") }) globalScope.set("decodeURI", { mutable: false, value: new UriFunction("decodeURI") }) globalScope.set("decodeURIComponent", { mutable: false, value: new UriFunction("decodeURIComponent") }) - // Error constructors are real values, so `x instanceof Error` works and `Error("msg")` - // (with or without `new`) constructs a branded { name, message } error object. for (const name of errorConstructors) { globalScope.set(name, { mutable: false, value: new ErrorConstructorReference(name) }) } - // NaN/Infinity flow as ordinary in-sandbox values (normalized to null only at the data - // boundary - see copyOut), so their global bindings must exist too, e.g. `reduce(max, -Infinity)`. globalScope.set("NaN", { mutable: false, value: NaN }) globalScope.set("Infinity", { mutable: false, value: Infinity }) } run(program: ProgramNode): Effect.Effect { const self = this - // Run the program body in its own module scope on top of the builtin global scope, so - // top-level declarations (`let undefined = 5`, `const Object = ...`) shadow builtins like - // JS module scope, instead of colliding with the seeded globals. - this.pushScope() + // Keep top-level declarations separate so they can shadow builtins. + this.scopes.push() return Effect.gen(function* () { + self.predeclareLexical(program.body) self.hoistFunctions(program.body) let value: unknown = undefined - let returned = false - for (const statement of program.body) { + for (const [index, statement] of program.body.entries()) { + if (index === program.body.length - 1 && statement.type === "ExpressionStatement") { + value = yield* self.evaluateExpression(getNode(statement, "expression")) + break + } const result = yield* self.evaluateStatement(statement) if (result.kind === "return") { value = result.value - returned = true break } if (result.kind === "break" || result.kind === "continue") { throw new InterpreterRuntimeError(`Unexpected '${result.kind}' outside of a loop.`, statement) } - - if (result.kind === "value") { - self.lastValue = result.value - } } - if (!returned) value = self.lastValue - // The program body runs inside an implicit async function, so a returned promise - // resolves before crossing the data boundary - `return tools.ns.tool(...)` works - // without an explicit await, exactly as in JS. - if (value instanceof SandboxPromise) value = yield* self.settlePromise(value) - yield* self.drainPendingSettlements() + // The implicit async body adopts returned promises before copy-out. + if (value instanceof CodeModePromise) value = yield* self.settlePromise(value) return value - }).pipe(Effect.ensuring(Effect.sync(() => self.popScope()))) + }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) } - // Awaits every fiber-backed promise the program abandoned (fire-and-forget tool calls), so - // their work completes before the execution ends - mirroring a JS runtime waiting on - // in-flight I/O at exit. A failure nobody could have handled becomes an unhandled-rejection - // diagnostic (interrupted calls, e.g. Promise.race losers, are ignored). - private drainPendingSettlements(): Effect.Effect { - const self = this - return Effect.gen(function* () { - for (const promise of [...self.pendingSettlements]) { - const exit = yield* self.observePromise(promise) - if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause)) continue - const failure = normalizeError(Cause.squash(exit.cause)) - throw new InterpreterRuntimeError( - `Unhandled rejection from an un-awaited tool call: ${failure.message}`, - undefined, - failure.kind, - ["Await tool calls - `const result = await tools.ns.tool(...)` - so failures can be caught and handled."], - ) - } - }) - } - - // Eagerly starts a tool call on a supervised child fiber (so the execution timeout and - // scope teardown interrupt it) gated by the concurrency semaphore, and wraps the fiber in a - // first-class promise value. `startImmediately` makes the runtime admit the call - charging - // the tool-call budget and firing onToolCallStart - at the call site, before any await. + // Fork at the call site so admission and hooks occur when the call is made. private createToolCallPromise( path: ReadonlyArray, args: Array, - ): Effect.Effect { - const self = this - return Effect.map( - Effect.forkChild(this.callPermits.withPermit(Effect.suspend(() => self.invokeTool(path, args))), { - startImmediately: true, - }), - (fiber) => { - const promise = new SandboxPromise(fiber) - self.pendingSettlements.add(promise) - return promise - }, - ) + ): Effect.Effect { + return this.createPromise(Effect.suspend(() => this.invokeTool(path, args))) } - // The promise's settlement as an Exit, marking it observed for unhandled-rejection tracking. - // Fiber settlement is idempotent, so observing the same promise repeatedly (await twice, - // Promise.all([p, p])) never re-runs the underlying call. - private observePromise(promise: SandboxPromise): Effect.Effect> { - this.pendingSettlements.delete(promise) - return promise.fiber !== undefined ? Fiber.await(promise.fiber) : Effect.exit(promise.immediate ?? Effect.void) + private createPromise(effect: Effect.Effect): Effect.Effect { + return this.promises.create(effect) } - // `await promise`: succeed with the fulfilled value or re-raise the failure so try/catch - // observes it exactly like a synchronous throw at the await site. - private settlePromise(promise: SandboxPromise, node?: AstNode): Effect.Effect { - const self = this - return Effect.flatMap(this.observePromise(promise), (exit) => self.unwrapPromiseExit(promise, exit, node)) - } - - private unwrapPromiseExit( - promise: SandboxPromise | undefined, - exit: Exit.Exit, - node?: AstNode, - ): Effect.Effect { - if (Exit.isSuccess(exit)) return Effect.succeed(exit.value) - // A call Promise.race interrupted after losing settles as a catchable program failure; - // any other interruption is execution teardown (timeout/host) and must keep propagating - // as interruption rather than becoming program-visible data. - if (promise?.interrupted === true && Cause.hasInterruptsOnly(exit.cause)) { - return Effect.fail( - new InterpreterRuntimeError( - "This tool call was interrupted because another value settled a Promise.race first.", - node, - ), - ) - } - return Effect.failCause(exit.cause) + // Fiber exits make settlement idempotent; yielding prevents inline continuation. + private settlePromise(promise: CodeModePromise): Effect.Effect { + const promises = this.promises + return Effect.suspend(() => { + promises.markObserved(promise) + return Effect.flatMap(promises.await(promise), (exit) => Effect.andThen(Effect.yieldNow, exit)) + }) } private evaluateStatement(node: AstNode): Effect.Effect { switch (node.type) { case "ExpressionStatement": - return Effect.map(this.evaluateExpression(getNode(node, "expression")), (value) => ({ kind: "value", value })) + return Effect.as(this.evaluateExpression(getNode(node, "expression")), { kind: "none" }) case "VariableDeclaration": return Effect.map(this.evaluateVariableDeclaration(node), () => ({ kind: "none" })) case "ReturnStatement": { @@ -814,35 +338,31 @@ class Interpreter { case "EmptyStatement": return Effect.succeed({ kind: "none" }) case "FunctionDeclaration": - return Effect.succeed({ kind: "none" }) // bound ahead of time by hoistFunctions + return Effect.succeed({ kind: "none" }) default: throw unsupportedSyntax(node.type, node) } } private evaluateBlock(node: AstNode): Effect.Effect { - this.pushScope() + this.scopes.push() const self = this return Effect.gen(function* () { const body = getArray(node, "body") + self.predeclareLexical(body) self.hoistFunctions(body) for (const statementValue of body) { const statement = asNode(statementValue, "body") const result = yield* self.evaluateStatement(statement) - if (result.kind === "value") { - self.lastValue = result.value - continue - } - if (result.kind !== "none") { return result } } return { kind: "none" } satisfies StatementResult - }).pipe(Effect.ensuring(Effect.sync(() => self.popScope()))) + }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) } private createFunction(node: AstNode): CodeModeFunction { @@ -857,20 +377,38 @@ class Interpreter { return new CodeModeFunction( getArray(node, "params").map((parameter, index) => asNode(parameter, `params[${index}]`)), getNode(node, "body"), - this.scopes.slice(), + this.scopes.capture(), + node.async === true, ) } - // Function declarations are hoisted: bound in their scope before the body runs, so a - // program can call a helper defined further down (matching JavaScript). private hoistFunctions(statements: Array): void { for (const statementValue of statements) { if (!isRecord(statementValue) || statementValue.type !== "FunctionDeclaration") continue const node = statementValue as AstNode - this.declare(getString(getNode(node, "id"), "name"), this.createFunction(node), true, node) + this.scopes.declare(getString(getNode(node, "id"), "name"), this.createFunction(node), true, node) } } + private predeclareLexical(statements: Array): void { + for (const statementValue of statements) { + if (!isRecord(statementValue) || statementValue.type !== "VariableDeclaration") continue + const statement = statementValue as AstNode + const kind = getString(statement, "kind") + if (kind === "var") continue + for (const declarationValue of getArray(statement, "declarations")) { + const declaration = asNode(declarationValue, "declarations") + for (const name of collectPatternNames(getNode(declaration, "id"))) { + this.scopes.reserve(name, kind !== "const", declaration) + } + } + } + } + + private predeclarePattern(pattern: AstNode, mutable: boolean, node: AstNode): void { + for (const name of collectPatternNames(pattern)) this.scopes.reserve(name, mutable, node) + } + private evaluateIfStatement(node: AstNode): Effect.Effect { const testNode = getNode(node, "test") const consequentNode = getNode(node, "consequent") @@ -887,7 +425,6 @@ class Interpreter { private evaluateSwitchStatement(node: AstNode): Effect.Effect { const self = this - this.pushScope() return Effect.gen(function* () { const discriminant = yield* self.evaluateExpression(getNode(node, "discriminant")) if (containsOpaqueReference(discriminant)) { @@ -897,40 +434,43 @@ class Interpreter { "InvalidDataValue", ) } - const cases = getArray(node, "cases").map((value, index) => asNode(value, `cases[${index}]`)) - let defaultIndex: number | undefined - let selected: number | undefined - for (const [index, branch] of cases.entries()) { - const test = getOptionalNode(branch, "test") - if (!test) { - defaultIndex = index - continue + self.scopes.push() + return yield* Effect.gen(function* () { + const cases = getArray(node, "cases").map((value, index) => asNode(value, `cases[${index}]`)) + self.predeclareLexical(cases.flatMap((branch) => getArray(branch, "consequent"))) + let defaultIndex: number | undefined + let selected: number | undefined + for (const [index, branch] of cases.entries()) { + const test = getOptionalNode(branch, "test") + if (!test) { + defaultIndex = index + continue + } + const candidate = yield* self.evaluateExpression(test) + if (containsOpaqueReference(candidate)) { + throw new InterpreterRuntimeError( + "Switch case values must be data values in CodeMode.", + test, + "InvalidDataValue", + ) + } + if (candidate === discriminant) { + selected = index + break + } } - const candidate = yield* self.evaluateExpression(test) - if (containsOpaqueReference(candidate)) { - throw new InterpreterRuntimeError( - "Switch case values must be data values in CodeMode.", - test, - "InvalidDataValue", - ) + const start = selected ?? defaultIndex + if (start === undefined) return { kind: "none" } satisfies StatementResult + for (let index = start; index < cases.length; index += 1) { + for (const statementValue of getArray(cases[index]!, "consequent")) { + const result = yield* self.evaluateStatement(asNode(statementValue, "consequent")) + if (result.kind === "break") return { kind: "none" } satisfies StatementResult + if (result.kind === "return" || result.kind === "continue") return result + } } - if (candidate === discriminant) { - selected = index - break - } - } - const start = selected ?? defaultIndex - if (start === undefined) return { kind: "none" } satisfies StatementResult - for (let index = start; index < cases.length; index += 1) { - for (const statementValue of getArray(cases[index]!, "consequent")) { - const result = yield* self.evaluateStatement(asNode(statementValue, "consequent")) - if (result.kind === "break") return { kind: "none" } satisfies StatementResult - if (result.kind === "return" || result.kind === "continue") return result - if (result.kind === "value") self.lastValue = result.value - } - } - return { kind: "none" } satisfies StatementResult - }).pipe(Effect.ensuring(Effect.sync(() => self.popScope()))) + return { kind: "none" } satisfies StatementResult + }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) + }) } private evaluateWhileStatement(node: AstNode): Effect.Effect { @@ -953,10 +493,6 @@ class Interpreter { if (result.kind === "return") { return result } - - if (result.kind === "value") { - self.lastValue = result.value - } } return { kind: "none" } satisfies StatementResult @@ -983,10 +519,6 @@ class Interpreter { if (result.kind === "return") { return result } - - if (result.kind === "value") { - self.lastValue = result.value - } } while (yield* self.evaluateExpression(testNode)) return { kind: "none" } satisfies StatementResult @@ -994,7 +526,7 @@ class Interpreter { } private evaluateForStatement(node: AstNode): Effect.Effect { - this.pushScope() + this.scopes.push() const self = this return Effect.gen(function* () { const initNode = getOptionalNode(node, "init") @@ -1002,6 +534,10 @@ class Interpreter { const updateNode = getOptionalNode(node, "update") const bodyNode = getNode(node, "body") + if (initNode?.type === "VariableDeclaration" && getString(initNode, "kind") !== "var") { + self.predeclareLexical([initNode]) + } + if (initNode) { if (initNode.type === "VariableDeclaration") { yield* self.evaluateVariableDeclaration(initNode) @@ -1012,24 +548,92 @@ class Interpreter { const perIterationBindings = initNode?.type === "VariableDeclaration" && getString(initNode, "kind") !== "var" - ? Array.from(self.currentScope().keys()) + ? Array.from(self.scopes.current().keys()) : [] + const nextIteration = () => { + if (perIterationBindings.length === 0) return + const current = self.scopes.current() + self.scopes.pop() + self.scopes.push( + new Map(perIterationBindings.map((name): [string, Binding] => [name, { ...current.get(name)! }])), + ) + } + nextIteration() + while (testNode ? yield* self.evaluateExpression(testNode) : true) { - let iterationScope: Map | undefined - if (perIterationBindings.length > 0) { - iterationScope = new Map( - perIterationBindings.map((name) => { - const binding = self.currentScope().get(name)! - return [name, { ...binding }] - }), - ) - self.scopes.push(iterationScope) + const result = yield* self.evaluateStatement(bodyNode) + + if (result.kind === "return") { + return result } - const result = yield* self.evaluateStatement(bodyNode).pipe( + + if (result.kind === "break") { + return { kind: "none" } satisfies StatementResult + } + + nextIteration() + if (updateNode) { + yield* self.evaluateExpression(updateNode) + } + + if (result.kind === "continue") { + continue + } + } + + return { kind: "none" } satisfies StatementResult + }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) + } + + private evaluateForOfStatement(node: AstNode): Effect.Effect { + if (getBoolean(node, "await")) { + throw new InterpreterRuntimeError("for await...of is not supported.", node) + } + + const left = getNode(node, "left") + const declared = loopDeclaration(left, "for...of") + if (declared?.lexical) this.scopes.push() + + const self = this + return Effect.gen(function* () { + if (declared?.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left) + const right = yield* self.evaluateExpression(getNode(node, "right")) + const body = getNode(node, "body") + + const iterable = spreadItems(right) + if (iterable === undefined) { + throw new InterpreterRuntimeError("for...of requires an array, string, Map, or Set value in CodeMode.", node) + } + + let assignment: AstNode | undefined + + if ( + left.type !== "VariableDeclaration" && + (left.type === "Identifier" || + left.type === "MemberExpression" || + left.type === "ArrayPattern" || + left.type === "ObjectPattern") + ) { + assignment = left + } else if (left.type !== "VariableDeclaration") { + throw new InterpreterRuntimeError("Unsupported for...of binding.", left) + } + + for (const value of iterable) { + const result = yield* Effect.gen(function* () { + if (declared) { + self.scopes.push() + if (declared.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left) + yield* self.declarePattern(declared.pattern, value, declared.mutable, left, declared.lexical) + } else if (assignment) { + yield* self.assignPattern(assignment, value, left) + } + return yield* self.evaluateStatement(body) + }).pipe( Effect.ensuring( Effect.sync(() => { - if (iterationScope) self.popScope() + if (declared) self.scopes.pop() }), ), ) @@ -1042,107 +646,21 @@ class Interpreter { return { kind: "none" } satisfies StatementResult } - if (result.kind === "value") { - self.lastValue = result.value - } - - if (iterationScope) { - const loopScope = self.currentScope() - for (const name of perIterationBindings) { - loopScope.set(name, { ...iterationScope.get(name)! }) - } - } - - if (updateNode) { - yield* self.evaluateExpression(updateNode) - } - if (result.kind === "continue") { continue } } return { kind: "none" } satisfies StatementResult - }).pipe(Effect.ensuring(Effect.sync(() => self.popScope()))) + }).pipe( + Effect.ensuring( + Effect.sync(() => { + if (declared?.lexical) self.scopes.pop() + }), + ), + ) } - private evaluateForOfStatement(node: AstNode): Effect.Effect { - if (getBoolean(node, "await")) { - throw new InterpreterRuntimeError("for await...of is not supported.", node) - } - - const self = this - return Effect.gen(function* () { - const left = getNode(node, "left") - const right = yield* self.evaluateExpression(getNode(node, "right")) - const body = getNode(node, "body") - - // Arrays iterate in place; strings iterate code points; Maps iterate [key, value] - // pairs and Sets iterate values over a snapshot (mutation during iteration is safe). - const iterable = Array.isArray(right) ? right : spreadItems(right) - if (iterable === undefined) { - throw new InterpreterRuntimeError("for...of requires an array, string, Map, or Set value in CodeMode.", node) - } - - let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined - let assignmentName: string | undefined - - if (left.type === "VariableDeclaration") { - const declarations = getArray(left, "declarations") - if (declarations.length !== 1) { - throw new InterpreterRuntimeError("for...of supports one declared binding.", left) - } - - const declarator = asNode(declarations[0], "declarations[0]") - declaration = { pattern: getNode(declarator, "id"), mutable: getString(left, "kind") !== "const" } - } else if (left.type === "Identifier") { - assignmentName = getString(left, "name") - } else { - throw new InterpreterRuntimeError("Unsupported for...of binding.", left) - } - - for (const value of iterable) { - if (declaration) { - self.pushScope() - yield* self.declarePattern(declaration.pattern, value, declaration.mutable, left) - } else if (assignmentName) { - self.setIdentifierValue(assignmentName, value, left) - } - - const result = yield* self.evaluateStatement(body).pipe( - Effect.ensuring( - Effect.sync(() => { - if (declaration) self.popScope() - }), - ), - ) - - if (result.kind === "return") { - return result - } - - if (result.kind === "break") { - return { kind: "none" } - } - - if (result.kind === "value") { - self.lastValue = result.value - } - - if (result.kind === "continue") { - continue - } - } - - return { kind: "none" } - }) - } - - // Own enumerable string keys of a value, shared by `for...in` and `Object.keys` over tool - // references: plain data objects enumerate their own keys, arrays their index strings (plus - // any own non-index properties, e.g. match results' index/groups - exactly Object.keys in - // JS), and a tool reference the namespace/tool names at its path in the host tool tree. - // Returns undefined for everything else so callers can raise a contextual error. private enumerableKeys(value: unknown): Array | undefined { if (value instanceof ToolReference) { return [...this.toolKeys(value.path)] @@ -1157,18 +675,16 @@ class Interpreter { } private evaluateForInStatement(node: AstNode): Effect.Effect { + const left = getNode(node, "left") + const declared = loopDeclaration(left, "for...in") + if (declared?.lexical) this.scopes.push() + const self = this return Effect.gen(function* () { - const left = getNode(node, "left") + if (declared?.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left) const right = yield* self.evaluateExpression(getNode(node, "right")) const body = getNode(node, "body") - // Keys are snapshotted up front (mutation during iteration is safe): plain objects - // enumerate their own keys, arrays their index strings, and tool references the - // namespace/tool names at that node - the same enumeration Object.keys performs. - // Anything else (strings, Maps, Sets, numbers, null, ...) is a deliberate error rather - // than real JS's surprising behavior (indices for strings, zero iterations for - // Maps/Sets/null): the hint points at the constructs that do what the program means. const keys = self.enumerableKeys(right) if (keys === undefined) { throw new InterpreterRuntimeError( @@ -1177,35 +693,28 @@ class Interpreter { ) } - let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined let assignmentName: string | undefined - if (left.type === "VariableDeclaration") { - const declarations = getArray(left, "declarations") - if (declarations.length !== 1) { - throw new InterpreterRuntimeError("for...in supports one declared binding.", left) - } - - const declarator = asNode(declarations[0], "declarations[0]") - declaration = { pattern: getNode(declarator, "id"), mutable: getString(left, "kind") !== "const" } - } else if (left.type === "Identifier") { + if (left.type === "Identifier") { assignmentName = getString(left, "name") - } else { + } else if (left.type !== "VariableDeclaration") { throw new InterpreterRuntimeError("Unsupported for...in binding.", left) } for (const key of keys) { - if (declaration) { - self.pushScope() - yield* self.declarePattern(declaration.pattern, key, declaration.mutable, left) - } else if (assignmentName) { - self.setIdentifierValue(assignmentName, key, left) - } - - const result = yield* self.evaluateStatement(body).pipe( + const result = yield* Effect.gen(function* () { + if (declared) { + self.scopes.push() + if (declared.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left) + yield* self.declarePattern(declared.pattern, key, declared.mutable, left, declared.lexical) + } else if (assignmentName) { + self.scopes.set(assignmentName, key, left) + } + return yield* self.evaluateStatement(body) + }).pipe( Effect.ensuring( Effect.sync(() => { - if (declaration) self.popScope() + if (declared) self.scopes.pop() }), ), ) @@ -1215,11 +724,7 @@ class Interpreter { } if (result.kind === "break") { - return { kind: "none" } - } - - if (result.kind === "value") { - self.lastValue = result.value + return { kind: "none" } satisfies StatementResult } if (result.kind === "continue") { @@ -1227,8 +732,14 @@ class Interpreter { } } - return { kind: "none" } - }) + return { kind: "none" } satisfies StatementResult + }).pipe( + Effect.ensuring( + Effect.sync(() => { + if (declared?.lexical) self.scopes.pop() + }), + ), + ) } private evaluateBreakStatement(node: AstNode): StatementResult { @@ -1268,15 +779,13 @@ class Interpreter { return Effect.failCause(cause) } - // The program sees a plain { message } error (or the thrown value itself) - see - // caughtErrorValue, shared with Promise.allSettled rejection reasons. const caught = caughtErrorValue(Cause.squash(cause)) const parameter = getOptionalNode(handler, "param") - self.pushScope() + self.scopes.push() return Effect.gen(function* () { if (parameter) yield* self.declarePattern(parameter, caught, true, handler) return yield* self.evaluateStatement(getNode(handler, "body")) - }).pipe(Effect.ensuring(Effect.sync(() => self.popScope()))) + }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) }, onSuccess: Effect.succeed, }) @@ -1314,7 +823,7 @@ class Interpreter { const init = getOptionalNode(declaration, "init") const value = init ? yield* self.evaluateExpression(init) : undefined - yield* self.declarePattern(getNode(declaration, "id"), value, kind !== "const", declaration) + yield* self.declarePattern(getNode(declaration, "id"), value, kind !== "const", declaration, kind !== "var") } }) } @@ -1324,25 +833,27 @@ class Interpreter { value: unknown, mutable: boolean, node: AstNode, + initialize = false, ): Effect.Effect { const self = this return Effect.gen(function* () { if (pattern.type === "Identifier") { - self.declare(getString(pattern, "name"), value, mutable, node) + const name = getString(pattern, "name") + if (initialize) self.scopes.initialize(name, value, node) + else self.scopes.declare(name, value, mutable, node) return } - // Default values: `x = expr` / `{ a = 1 }` - the default is evaluated only when the value is undefined. if (pattern.type === "AssignmentPattern") { const resolved = value === undefined ? yield* self.evaluateExpression(getNode(pattern, "right")) : value - yield* self.declarePattern(getNode(pattern, "left"), resolved, mutable, node) + yield* self.declarePattern(getNode(pattern, "left"), resolved, mutable, node, initialize) return } if (pattern.type === "ObjectPattern") { - if (value === null || typeof value !== "object" || Array.isArray(value) || isRuntimeReference(value)) { + if (value === null || typeof value !== "object" || isRuntimeReference(value)) { throw new InterpreterRuntimeError( - "Object destructuring requires a data object value.", + "Object destructuring requires a data object or array value.", pattern, "InvalidDataValue", ) @@ -1352,49 +863,45 @@ class Interpreter { for (const propertyValue of getArray(pattern, "properties")) { const property = asNode(propertyValue, "properties") - // Object rest: `{ a, ...others }` - gather the not-yet-consumed own keys. if (property.type === "RestElement") { const rest: SafeObject = Object.create(null) as SafeObject for (const [key, item] of Object.entries(value as SafeObject)) { if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item } - yield* self.declarePattern(getNode(property, "argument"), rest, mutable, property) + yield* self.declarePattern(getNode(property, "argument"), rest, mutable, property, initialize) continue } - if ( - property.type !== "Property" || - getBoolean(property, "computed") || - getString(property, "kind") !== "init" - ) { - throw new InterpreterRuntimeError("Only named object destructuring properties are supported.", property) + const key = yield* self.destructuringPropertyKey(property) + if (isBlockedMember(String(key))) { + throw new InterpreterRuntimeError(`Property '${String(key)}' is not available in CodeMode.`, property) } - - const keyNode = getNode(property, "key") - const key = keyNode.type === "Identifier" ? getString(keyNode, "name") : String(keyNode.value) - if (isBlockedMember(key)) { - throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, keyNode) - } - consumed.add(key) - yield* self.declarePattern(getNode(property, "value"), (value as SafeObject)[key], mutable, property) + consumed.add(String(key)) + yield* self.declarePattern( + getNode(property, "value"), + self.destructuringPropertyValue(value as SafeObject | Array, key), + mutable, + property, + initialize, + ) } return } if (pattern.type === "ArrayPattern") { - if (!Array.isArray(value)) { - throw new InterpreterRuntimeError("Array destructuring requires an array value.", pattern) + const items = spreadItems(value) + if (items === undefined) { + throw new InterpreterRuntimeError("Array destructuring requires a supported iterable value.", pattern) } for (const [index, item] of getArray(pattern, "elements").entries()) { if (item === null) continue const element = asNode(item, `elements[${index}]`) - // Array rest: `[head, ...tail]` - binds the remaining elements (must be last). if (element.type === "RestElement") { - yield* self.declarePattern(getNode(element, "argument"), value.slice(index), mutable, element) + yield* self.declarePattern(getNode(element, "argument"), items.slice(index), mutable, element, initialize) break } - yield* self.declarePattern(element, value[index], mutable, pattern) + yield* self.declarePattern(element, items[index], mutable, pattern, initialize) } return } @@ -1403,11 +910,100 @@ class Interpreter { }) } + private assignPattern(pattern: AstNode, value: unknown, node: AstNode): Effect.Effect { + const self = this + return Effect.gen(function* () { + if (pattern.type === "Identifier") { + self.scopes.set(getString(pattern, "name"), value, pattern) + return + } + + if (pattern.type === "MemberExpression") { + yield* self.writeMember(pattern, value) + return + } + + if (pattern.type === "AssignmentPattern") { + const resolved = value === undefined ? yield* self.evaluateExpression(getNode(pattern, "right")) : value + yield* self.assignPattern(getNode(pattern, "left"), resolved, node) + return + } + + if (pattern.type === "ObjectPattern") { + if (value === null || typeof value !== "object" || isRuntimeReference(value)) { + throw new InterpreterRuntimeError( + "Object destructuring requires a data object or array value.", + pattern, + "InvalidDataValue", + ) + } + + const source = value as SafeObject | Array + const consumed = new Set() + for (const propertyValue of getArray(pattern, "properties")) { + const property = asNode(propertyValue, "properties") + if (property.type === "RestElement") { + const rest: SafeObject = Object.create(null) as SafeObject + for (const [key, item] of Object.entries(source)) { + if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item + } + yield* self.assignPattern(getNode(property, "argument"), rest, property) + continue + } + const key = yield* self.destructuringPropertyKey(property) + if (isBlockedMember(String(key))) { + throw new InterpreterRuntimeError(`Property '${String(key)}' is not available in CodeMode.`, property) + } + consumed.add(String(key)) + yield* self.assignPattern(getNode(property, "value"), self.destructuringPropertyValue(source, key), property) + } + return + } + + if (pattern.type === "ArrayPattern") { + const items = spreadItems(value) + if (items === undefined) { + throw new InterpreterRuntimeError("Array destructuring requires a supported iterable value.", pattern) + } + for (const [index, item] of getArray(pattern, "elements").entries()) { + if (item === null) continue + const element = asNode(item, `elements[${index}]`) + if (element.type === "RestElement") { + yield* self.assignPattern(getNode(element, "argument"), items.slice(index), element) + break + } + yield* self.assignPattern(element, items[index], pattern) + } + return + } + + throw new InterpreterRuntimeError(`Unsupported assignment pattern '${pattern.type}'.`, node) + }) + } + + private destructuringPropertyKey(property: AstNode): Effect.Effect { + if (property.type !== "Property" || getString(property, "kind") !== "init") { + throw new InterpreterRuntimeError("Unsupported object destructuring property.", property) + } + const keyNode = getNode(property, "key") + if (getBoolean(property, "computed")) { + return Effect.map(this.evaluateExpression(keyNode), (value) => this.toPropertyKey(value, keyNode)) + } + return Effect.succeed(keyNode.type === "Identifier" ? getString(keyNode, "name") : String(keyNode.value)) + } + + private destructuringPropertyValue(source: SafeObject | Array, key: string | number): unknown { + if (!Array.isArray(source)) return source[String(key)] + if (key === "length") return source.length + if (typeof key === "number") return source[key] + if (Object.hasOwn(source, key)) return (source as Record & Array)[key] + if (arrayMethods.has(key)) return new IntrinsicReference(source, key) + return undefined + } + private evaluateExpression(node: AstNode): Effect.Effect { switch (node.type) { case "Literal": { - // A regex literal parses as a Literal node carrying { pattern, flags }; construct the - // sandbox regex from those (the host `value` instance is never exposed). const regex = node.regex if (isRecord(regex) && typeof regex.pattern === "string") { return Effect.sync(() => @@ -1417,7 +1013,7 @@ class Interpreter { return Effect.sync(() => boundedData(node.value, "Literal")) } case "Identifier": - return Effect.sync(() => this.getIdentifierValue(getString(node, "name"), node)) + return Effect.sync(() => this.scopes.get(getString(node, "name"), node)) case "BinaryExpression": return this.evaluateBinaryExpression(node) case "LogicalExpression": @@ -1426,6 +1022,16 @@ class Interpreter { return this.evaluateUnaryExpression(node) case "AssignmentExpression": return this.evaluateAssignmentExpression(node) + case "SequenceExpression": { + const self = this + return Effect.gen(function* () { + let result: unknown + for (const expression of getArray(node, "expressions")) { + result = yield* self.evaluateExpression(asNode(expression, "expressions")) + } + return result + }) + } case "CallExpression": return this.evaluateCallExpression(node) case "ArrowFunctionExpression": @@ -1448,11 +1054,10 @@ class Interpreter { case "UpdateExpression": return this.evaluateUpdateExpression(node) case "AwaitExpression": { - // `await` resolves a promise value; awaiting anything else is a passthrough no-op, - // matching real JS semantics for non-thenables. + // Await always suspends, including for plain values. const self = this return Effect.flatMap(this.evaluateExpression(getNode(node, "argument")), (value) => - value instanceof SandboxPromise ? self.settlePromise(value, node) : Effect.succeed(value), + value instanceof CodeModePromise ? self.settlePromise(value) : Effect.as(Effect.yieldNow, value), ) } case "NewExpression": @@ -1471,19 +1076,19 @@ class Interpreter { const argNodes = getArray(node, "arguments") const self = this if (name === "Promise") { - throw new InterpreterRuntimeError( - "new Promise(...) is not supported in CodeMode; tool calls already return promises - call the tool and await the result.", - node, - "UnsupportedSyntax", - [supportedSyntaxMessage], + return Effect.flatMap(this.evaluateCallArguments(argNodes), (args) => + constructPromise(self.runner, self.promises, args[0], node), ) } if (errorConstructors.has(name)) { - return Effect.gen(function* () { - const arg = - argNodes.length > 0 ? yield* self.evaluateExpression(asNode(argNodes[0], "arguments[0]")) : undefined - return createErrorValue(name, arg === undefined ? "" : coerceToString(arg)) - }) + return Effect.map(this.evaluateCallArguments(argNodes), (args) => constructErrorValue(name, args, node)) + } + // Array and Object construct identically with or without new, like JS. + if (name === "Array") { + return Effect.map(this.evaluateCallArguments(argNodes), (args) => self.constructArray(args, node)) + } + if (name === "Object") { + return Effect.map(this.evaluateCallArguments(argNodes), (args) => self.constructObject(args, node)) } if (valueConstructors.has(name)) { return Effect.gen(function* () { @@ -1507,38 +1112,55 @@ class Interpreter { throw unsupportedSyntax("NewExpression", node) } - private constructDate(args: Array): SandboxDate { - if (args.length === 0) return new SandboxDate(Date.now()) - if (args.length === 1) { - const arg = args[0] - if (arg instanceof SandboxDate) return new SandboxDate(arg.time) - if (typeof arg === "number") return new SandboxDate(new Date(arg).getTime()) - if (typeof arg === "string") return new SandboxDate(Date.parse(arg)) - return new SandboxDate(Number.NaN) + private constructArray(args: Array, node: AstNode): Array { + if (args.length !== 1) return [...args] + const first = args[0] + if (typeof first !== "number") return [first] + if (!Number.isInteger(first) || first < 0 || first > 4294967295) { + throw new InterpreterRuntimeError("Invalid array length.", node).as("RangeError") } - // new Date(year, month, day?, hours?, ...) - local-time component form. - const parts = args.map((arg) => coerceToNumber(arg)) - return new SandboxDate(new Date(...(parts as [number, number])).getTime()) + // Sparse like JS: Array(3) has holes, and combinator loops already skip them. + return new Array(first) } - private constructRegExp(args: Array, node: AstNode): SandboxRegExp { + private constructObject(args: Array, node: AstNode): unknown { + const first = args[0] + if (first === null || first === undefined) return {} + if (typeof first === "object") return first + throw new InterpreterRuntimeError( + `Object(${typeof first}) wrapper objects are not supported in CodeMode; use the primitive value directly.`, + node, + ) + } + + private constructDate(args: Array): CodeModeDate { + if (args.length === 0) return new CodeModeDate(Date.now()) + if (args.length === 1) { + const arg = args[0] + if (arg instanceof CodeModeDate) return new CodeModeDate(arg.time) + if (typeof arg === "number") return new CodeModeDate(new Date(arg).getTime()) + if (typeof arg === "string") return new CodeModeDate(Date.parse(arg)) + return new CodeModeDate(Number.NaN) + } + const parts = args.map((arg) => coerceToNumber(arg)) + return new CodeModeDate(new Date(...(parts as [number, number])).getTime()) + } + + private constructRegExp(args: Array, node: AstNode): CodeModeRegExp { const first = args[0] const pattern = - first instanceof SandboxRegExp ? first.regex.source : first === undefined ? "" : coerceToString(first) + first instanceof CodeModeRegExp ? first.regex.source : first === undefined ? "" : coerceToString(first) const flagsArg = args[1] if (flagsArg !== undefined && typeof flagsArg !== "string") { throw new InterpreterRuntimeError( `RegExp flags must be a string of flag characters (e.g. "g", "gi"), not ${flagsArg === null ? "null" : typeof flagsArg}.`, node, - ) + ).as("SyntaxError") } - const flags = flagsArg ?? (first instanceof SandboxRegExp ? first.regex.flags : "") + const flags = flagsArg ?? (first instanceof CodeModeRegExp ? first.regex.flags : "") try { - return new SandboxRegExp(pattern, flags) + return new CodeModeRegExp(pattern, flags) } catch (error) { - // Say which part was rejected and how to fix it, instead of passing the engine - // message through bare. A flags failure names the flags; a pattern failure gets the - // escaping hint (the usual cause is an unescaped metacharacter in a built-up string). const reason = regexFailureReason(error) throw new InterpreterRuntimeError( /flag/i.test(reason) @@ -1549,12 +1171,12 @@ class Interpreter { } } - private constructMap(init: unknown, node: AstNode): SandboxMap { - const target = new SandboxMap() + private constructMap(init: unknown, node: AstNode): CodeModeMap { + const target = new CodeModeMap() if (init === undefined || init === null) return target const entries = Array.isArray(init) ? init - : init instanceof SandboxMap + : init instanceof CodeModeMap ? Array.from(init.map.entries(), ([key, item]): Array => [key, item]) : undefined if (entries === undefined) { @@ -1572,12 +1194,12 @@ class Interpreter { return target } - private constructSet(init: unknown, node: AstNode): SandboxSet { - const target = new SandboxSet() + private constructSet(init: unknown, node: AstNode): CodeModeSet { + const target = new CodeModeSet() if (init === undefined || init === null) return target const items = Array.isArray(init) ? init - : init instanceof SandboxSet + : init instanceof CodeModeSet ? Array.from(init.set.values()) : typeof init === "string" ? Array.from(init) @@ -1589,7 +1211,7 @@ class Interpreter { return target } - private constructURL(args: Array, node: AstNode): SandboxURL { + private constructURL(args: Array, node: AstNode): CodeModeURL { if (args.length === 0) { throw new InterpreterRuntimeError("new URL(...) requires a URL string and an optional base URL.", node).as( "TypeError", @@ -1598,7 +1220,7 @@ class Interpreter { const input = urlArgument(args[0], "new URL input") const base = args[1] === undefined ? undefined : urlArgument(args[1], "new URL base") try { - return new SandboxURL(new URL(input, base)) + return new CodeModeURL(new URL(input, base)) } catch { throw new InterpreterRuntimeError( `new URL(...) received an invalid URL${base === undefined ? "" : " or base URL"}.`, @@ -1607,16 +1229,16 @@ class Interpreter { } } - private constructURLSearchParams(init: unknown, node: AstNode): SandboxURLSearchParams { - if (init === undefined) return new SandboxURLSearchParams(new URLSearchParams()) - if (init instanceof SandboxURLSearchParams) { - return new SandboxURLSearchParams(new URLSearchParams(init.params)) + private constructURLSearchParams(init: unknown, node: AstNode): CodeModeURLSearchParams { + if (init === undefined) return new CodeModeURLSearchParams(new URLSearchParams()) + if (init instanceof CodeModeURLSearchParams) { + return new CodeModeURLSearchParams(new URLSearchParams(init.params)) } - if (typeof init === "string") return new SandboxURLSearchParams(new URLSearchParams(init)) + if (typeof init === "string") return new CodeModeURLSearchParams(new URLSearchParams(init)) if (init === null || typeof init === "number" || typeof init === "boolean") { - return new SandboxURLSearchParams(new URLSearchParams(coerceToString(init))) + return new CodeModeURLSearchParams(new URLSearchParams(coerceToString(init))) } - if (init instanceof SandboxMap) { + if (init instanceof CodeModeMap) { return this.constructURLSearchParams( Array.from(init.map.entries(), ([key, value]) => [key, value]), node, @@ -1635,9 +1257,9 @@ class Interpreter { string, ] }) - return new SandboxURLSearchParams(new URLSearchParams(entries)) + return new CodeModeURLSearchParams(new URLSearchParams(entries)) } - if (isSandboxValue(init)) return new SandboxURLSearchParams(new URLSearchParams()) + if (isCodeModeValue(init)) return new CodeModeURLSearchParams(new URLSearchParams()) const data = boundedData(init, "new URLSearchParams input") if (data === null || typeof data !== "object") { throw new InterpreterRuntimeError( @@ -1645,7 +1267,7 @@ class Interpreter { node, ).as("TypeError") } - return new SandboxURLSearchParams( + return new CodeModeURLSearchParams( new URLSearchParams(Object.fromEntries(Object.entries(data).map(([key, value]) => [key, coerceToString(value)]))), ) } @@ -1656,31 +1278,19 @@ class Interpreter { return Effect.gen(function* () { const lhs = yield* self.evaluateExpression(getNode(node, "left")) const rhs = yield* self.evaluateExpression(getNode(node, "right")) - // Like `typeof`, `instanceof` observes any value without coercing it (a promise or - // function operand is a legitimate question, not an error), so it is handled before - // the data-only operand check. if (operator === "instanceof") return instanceofValue(lhs, rhs, node) return boundedData(self.applyBinaryOperator(operator, lhs, rhs, node), "Binary expression result") }) } - /** - * Applies a binary operator to two already-evaluated operands with CodeMode's coercion - * semantics. Shared by binary expressions and compound assignment (`x op= y` must behave - * exactly like `x = x op y`, coercion included). - */ private applyBinaryOperator(operator: string, lhs: unknown, rhs: unknown, node: AstNode): unknown { if (containsOpaqueReference(lhs) || containsOpaqueReference(rhs)) { throw new InterpreterRuntimeError("Binary operators require data values in CodeMode.", node, "InvalidDataValue") } - // Data objects/arrays are null-prototype, so JS's ToPrimitive throws an opaque host - // "No default value" TypeError when an operator coerces them. Coerce to their JS string - // form first (as String(x) / template literals do) so operators behave like JavaScript. - // A Date follows its ToPrimitive hints: string for `+` (concatenation), its time value - // for arithmetic and ordering - so `end - start` and `a < b` work as in JS. - // Identity (=== / !==) and the right operand of `in` keep their raw object value. + // Null-prototype data needs explicit primitive coercion; identity and `in` retain raw objects. + // Dates use string coercion for `+` and epoch time elsewhere. const coerceOperand = (operand: unknown): unknown => { - if (operand instanceof SandboxDate) return operator === "+" ? coerceToString(operand) : operand.time + if (operand instanceof CodeModeDate) return operator === "+" ? coerceToString(operand) : operand.time return operand !== null && typeof operand === "object" ? coerceToString(operand) : operand } const bothObjects = lhs !== null && typeof lhs === "object" && rhs !== null && typeof rhs === "object" @@ -1699,7 +1309,6 @@ class Interpreter { return (l as number) % (r as number) case "**": return (l as number) ** (r as number) - // Two objects compare by identity in JS (no ToPrimitive); only object-vs-primitive coerces. case "==": return bothObjects ? lhs === rhs : l == r case "===": @@ -1732,7 +1341,7 @@ class Interpreter { if (rhs === null || typeof rhs !== "object") { throw new InterpreterRuntimeError("The 'in' operator requires a data object on the right-hand side.", node) } - // Own properties only, so arrays don't leak the host Array.prototype (map/constructor/...). + // Never expose properties inherited from host prototypes. return Object.hasOwn(rhs as object, coerceOperand(lhs) as PropertyKey) default: throw new InterpreterRuntimeError(`Unsupported binary operator '${operator}'.`, node) @@ -1755,25 +1364,20 @@ class Interpreter { private evaluateUnaryExpression(node: AstNode): Effect.Effect { const operator = getString(node, "operator") const argument = getNode(node, "argument") - // `typeof undeclaredIdentifier` is `"undefined"` in JS (never a ReferenceError), so - // feature-detection guards like `typeof x !== "undefined"` don't crash. Short-circuit before - // evaluating the argument; a declared-but-TDZ binding still falls through to the normal throw. - if (operator === "typeof" && argument.type === "Identifier" && !this.resolveBinding(getString(argument, "name"))) { + if (operator === "delete") return this.evaluateDeleteExpression(argument) + // Undeclared names short-circuit, but declared TDZ bindings must still throw. + if (operator === "typeof" && argument.type === "Identifier" && !this.scopes.resolve(getString(argument, "name"))) { return Effect.succeed("undefined") } return Effect.map(this.evaluateExpression(argument), (value) => { - // `typeof` and `!` never throw in JS - they observe any value (functions and runtime - // references included) without coercing it, so feature detection and negation work. if (operator === "typeof") return typeofValue(value) if (operator === "!") return !value + if (operator === "void") return undefined if (containsOpaqueReference(value)) { throw new InterpreterRuntimeError("Unary operators require data values in CodeMode.", node, "InvalidDataValue") } - // Numeric/bitwise unary operators ToPrimitive their operand; a Date yields its time value - // (`+date` is the epoch-ms idiom), other null-prototype data objects/arrays coerce to - // their JS string form first (see evaluateBinaryExpression). const operand = - value instanceof SandboxDate + value instanceof CodeModeDate ? value.time : value !== null && typeof value === "object" ? coerceToString(value) @@ -1804,25 +1408,36 @@ class Interpreter { if (operator === "??=" || operator === "||=" || operator === "&&=") { return yield* self.evaluateLogicalAssignment(node, left, operator) } - const rightValue = yield* self.evaluateExpression(getNode(node, "right")) + if (operator === "=" && (left.type === "ObjectPattern" || left.type === "ArrayPattern")) { + const rightValue = yield* self.evaluateExpression(getNode(node, "right")) + yield* self.assignPattern(left, rightValue, node) + return rightValue + } if (left.type === "Identifier") { const name = getString(left, "name") - if (operator === "=") return self.setIdentifierValue(name, rightValue, left) - const next = boundedData( - self.applyCompoundAssignment(operator, self.getIdentifierValue(name, left), rightValue, node), - "Assignment result", - ) - return self.setIdentifierValue(name, next, left) - } - if (left.type === "MemberExpression") { - if (operator === "=") return yield* self.writeMember(left, rightValue) - return yield* self.modifyMember(left, (current) => { + if (operator !== "=") { + const current = self.scopes.get(name, left) + const rightValue = yield* self.evaluateExpression(getNode(node, "right")) const next = boundedData( self.applyCompoundAssignment(operator, current, rightValue, node), "Assignment result", ) - return Effect.succeed({ write: true, next, result: next }) - }) + return self.scopes.set(name, next, left) + } + const rightValue = yield* self.evaluateExpression(getNode(node, "right")) + return self.scopes.set(name, rightValue, left) + } + if (left.type === "MemberExpression") { + return yield* self.modifyMember(left, (current) => + Effect.map(self.evaluateExpression(getNode(node, "right")), (rightValue) => { + if (operator === "=") return { write: true, next: rightValue, result: rightValue } + const next = boundedData( + self.applyCompoundAssignment(operator, current, rightValue, node), + "Assignment result", + ) + return { write: true, next, result: next } + }), + ) } throw new InterpreterRuntimeError("Assignment target must be an Identifier or MemberExpression.", left) }) @@ -1839,14 +1454,13 @@ class Interpreter { if (left.type === "Identifier") { const name = getString(left, "name") return Effect.gen(function* () { - const current = self.getIdentifierValue(name, left) + const current = self.scopes.get(name, left) if (!shouldAssign(current)) return current const rightValue = yield* self.evaluateExpression(getNode(node, "right")) - return self.setIdentifierValue(name, rightValue, left) + return self.scopes.set(name, rightValue, left) }) } if (left.type === "MemberExpression") { - // Resolve the member exactly once; evaluate the RHS only if we actually assign. return self.modifyMember(left, (current) => shouldAssign(current) ? Effect.map(self.evaluateExpression(getNode(node, "right")), (rightValue) => ({ @@ -1871,19 +1485,32 @@ class Interpreter { throw new InterpreterRuntimeError(`Unsupported update operator '${operator}'.`, node) } + // CodeMode numeric coercion, not host Number(): null-prototype data objects would make + // the host throw during ToPrimitive, and opaque runtime references must reject clearly. + const operand = (current: unknown): number => { + if (containsOpaqueReference(current)) { + throw new InterpreterRuntimeError( + `'${operator}' requires a data value in CodeMode.`, + argument, + "InvalidDataValue", + ) + } + return coerceToNumber(current) + } + if (argument.type === "Identifier") { return Effect.sync(() => { const name = getString(argument, "name") - const current = Number(this.getIdentifierValue(name, argument)) + const current = operand(this.scopes.get(name, argument)) const next = current + increment - this.setIdentifierValue(name, next, argument) + this.scopes.set(name, next, argument) return prefix ? next : current }) } if (argument.type === "MemberExpression") { return this.modifyMember(argument, (current) => { - const value = Number(current) + const value = operand(current) const next = value + increment return Effect.succeed({ write: true, next, result: prefix ? next : value }) }) @@ -1903,25 +1530,48 @@ class Interpreter { if ((callable === null || callable === undefined) && node.optional === true) return OptionalShortCircuit const args = yield* self.evaluateCallArguments(argNodes) + return yield* self.invokeCallable(callable, args, node, callee) + }) + } + // The single dispatch for every invocation: call expressions and callbacks share it. + private invokeCallable( + callable: unknown, + args: Array, + node: AstNode, + callee: AstNode = node, + ): Effect.Effect { + const self = this + return Effect.gen(function* () { if (callable instanceof ToolReference) { if (callable.path.length === 0) throw new InterpreterRuntimeError("The tools root is not callable.", callee) - // An un-awaited tool call is a first-class promise value; the call itself starts now. return yield* self.createToolCallPromise(callable.path, args) } if (callable instanceof PromiseMethodReference) { - return yield* self.invokePromiseMethod(callable, args, node) + return yield* invokePromiseMethod(self.runner, self.promises, callable, args, node) + } + if (callable instanceof PromiseInstanceMethodReference) { + return yield* invokePromiseInstanceMethod(self.runner, self.promises, callable, args, node) } if (callable instanceof CodeModeFunction) { return yield* self.invokeFunction(callable, args) } if (callable instanceof IntrinsicReference) { - return yield* self.invokeIntrinsic(callable, args, node) + return yield* invokeIntrinsic(self.runner, callable, args, node) } if (callable instanceof GlobalMethodReference) { if (callable.namespace === "console") return self.invokeConsole(callable.name, args, node) if (callable.namespace === "Object" && args[0] instanceof ToolReference) { - return self.invokeObjectMethodOnTools(callable.name, args[0] as ToolReference, node) + return self.invokeObjectMethodOnTools(callable.name, args[0], node) + } + if (callable.namespace === "Object" && objectMethodsPreservingIdentity.has(callable.name)) { + return invokeGlobalMethod(callable, args, node) + } + if (callable.namespace === "Array" && callable.name === "from") { + return yield* invokeArrayFrom(self.runner, args, node) + } + if (callable.namespace === "Array" && callable.name === "of") { + return invokeGlobalMethod(callable, args, node) } return boundedData(invokeGlobalMethod(callable, args, node), `${callable.namespace}.${callable.name} result`) } @@ -1931,24 +1581,45 @@ class Interpreter { if (callable instanceof UriFunction) { return invokeUriFunction(callable, args, node) } - // `Error("msg")` without `new` constructs an error exactly like `new Error("msg")`, as in JS. + if (callable instanceof SearchFunction) { + return yield* self.invokeSearch(args) + } if (callable instanceof ErrorConstructorReference) { - return createErrorValue(callable.name, args[0] === undefined ? "" : coerceToString(args[0])) + return constructErrorValue(callable.name, args, node) + } + if (callable instanceof GlobalNamespace) { + // Real JS permits calling Array, Object, Date, and RegExp without new. + if (callable.name === "Array") return self.constructArray(args, node) + if (callable.name === "Object") return self.constructObject(args, node) + // ISO instead of the host's locale string: CodeMode date strings are + // deterministic and must not leak the host timezone. + if (callable.name === "Date") return new Date().toISOString() + if (callable.name === "RegExp") return self.constructRegExp(args, node) + if (typeofValue(callable) === "function") { + throw new InterpreterRuntimeError(`Constructor ${callable.name} requires 'new'.`, node).as("TypeError") + } + throw new InterpreterRuntimeError(`${callable.name} is not a function.`, node).as("TypeError") + } + if (callable instanceof PromiseNamespace) { + throw new InterpreterRuntimeError("Constructor Promise requires 'new'.", node).as("TypeError") + } + if (callable instanceof PromiseCapabilityFunction) { + callable.settle(args[0]) + return undefined + } + if (callable === undefined || callable === null) { + throw new InterpreterRuntimeError(`${calleeDescription(callee)} is not a function.`, callee).as("TypeError") } throw new InterpreterRuntimeError("Only tools are callable in CodeMode.", callee) }) } - // Object.* over a tool reference: `Object.keys(tools)` / `Object.keys(tools.ns)` enumerate - // namespace/tool names from the host tool tree - the discovery idiom a model reaches for - // first. Every other Object helper cannot produce data from a tool reference, so it fails - // with a pointer at the working idioms instead of the generic plain-objects-only message. private invokeObjectMethodOnTools(name: string, ref: ToolReference, node: AstNode): unknown { if (name === "keys") { return boundedData(this.enumerableKeys(ref)!, "Object.keys result") } throw new InterpreterRuntimeError( - `Object.${name}(...) cannot read tool references: they are not plain data. Use Object.keys(tools) for names, or tools.$codemode.search({ query }) for signatures.`, + `Object.${name}(...) cannot read tool references: they are not plain data. Use Object.keys(tools) for names, or search({ query }) for signatures.`, node, "InvalidDataValue", ) @@ -1957,126 +1628,10 @@ class Interpreter { private invokeConsole(name: string, args: Array, node: AstNode): undefined { if (!consoleMethods.has(name)) throw new InterpreterRuntimeError(`console.${name} is not available in CodeMode.`, node) - this.logs.push(publicErrorMessage(this.formatConsoleMessage(name, args, node))) + this.logs.push(formatConsoleMessage(name, args)) return undefined } - private formatConsoleMessage(name: string, args: Array, node: AstNode): string { - if (name === "dir") return args.length === 0 ? "undefined" : this.formatConsoleArgument(args[0]) - if (name === "table") return this.formatConsoleTable(args[0], args[1], node) - const prefix = name === "warn" ? "[warn] " : name === "error" ? "[error] " : name === "debug" ? "[debug] " : "" - return `${prefix}${args.map((arg) => this.formatConsoleArgument(arg)).join(" ")}` - } - - // Console arguments format deeply and totally: values render as a debugger would show them - // rather than as boundary JSON - numbers keep NaN/Infinity (JSON would say null), sandbox - // values keep their friendly forms at ANY depth (ISO date, /regex/flags, Map(n) [...], - // Set(n) [...]), opaque runtime references become "[CodeMode reference]" markers in place, - // and plain objects/arrays render JSON-style. Formatting never fails the program: cycles - // render "[Circular]" and extreme depth degrades to "...". - private formatConsoleArgument(value: unknown): string { - if (value === undefined) return "undefined" - // A top-level string prints bare; nested strings are JSON-quoted (see formatConsoleValue). - if (typeof value === "string") return value - return this.formatConsoleValue(value, new Set(), 0) - } - - private formatConsoleValue(value: unknown, seen: Set, depth: number): string { - // Nested undefined renders as null, matching what JSON boundary output would show. - if (value === null || value === undefined) return "null" - if (typeof value === "string") return JSON.stringify(value) - // String(value) keeps NaN/Infinity/-Infinity readable; finite numbers match their JSON form. - if (typeof value === "number" || typeof value === "boolean") return String(value) - if (typeof value !== "object") return String(value) - if (value instanceof SandboxPromise) return "[Promise (await it to get its value)]" - if (value instanceof SandboxDate) return coerceToString(value) - if (value instanceof SandboxRegExp) return coerceToString(value) - if (value instanceof SandboxURL) return coerceToString(value) - if (value instanceof SandboxURLSearchParams) return coerceToString(value) - if (depth > MAX_CONSOLE_DEPTH) return "..." - if (seen.has(value)) return "[Circular]" - if (value instanceof SandboxMap) { - seen.add(value) - try { - const entries = Array.from(value.map.entries(), ([key, item]): Array => [key, item]) - return `Map(${value.map.size}) ${this.formatConsoleValue(entries, seen, depth + 1)}` - } finally { - seen.delete(value) - } - } - if (value instanceof SandboxSet) { - seen.add(value) - try { - return `Set(${value.set.size}) ${this.formatConsoleValue(Array.from(value.set.values()), seen, depth + 1)}` - } finally { - seen.delete(value) - } - } - if (isRuntimeReference(value)) return "[CodeMode reference]" - seen.add(value) - try { - if (Array.isArray(value)) { - return `[${value.map((item) => this.formatConsoleValue(item, seen, depth + 1)).join(",")}]` - } - return `{${Object.entries(value) - .map(([key, item]) => `${JSON.stringify(key)}:${this.formatConsoleValue(item, seen, depth + 1)}`) - .join(",")}}` - } finally { - seen.delete(value) - } - } - - private formatConsoleTable(value: unknown, columnsArgument: unknown, node: AstNode): string { - if (value === undefined) return "undefined" - // Sandbox values are legitimate table data (cells render their friendly forms); only - // truly opaque references (functions, tools, promises) collapse to the marker. - if (containsOpaqueReference(value)) return "[CodeMode reference]" - const data = boundedData(value, "console.table argument") - const columns = this.consoleTableColumns(columnsArgument, node) - const rows = this.consoleTableRows(data, columns) - const keys = columns ?? Array.from(new Set(rows.flatMap((row) => Object.keys(row.values)))) - const header = ["(index)", ...keys].join("\t") - return [ - header, - ...rows.map((row) => [row.index, ...keys.map((key) => this.formatConsoleTableCell(row.values[key]))].join("\t")), - ].join("\n") - } - - private consoleTableColumns(value: unknown, node: AstNode): ReadonlyArray | undefined { - if (value === undefined) return undefined - if (containsRuntimeReference(value)) return undefined - const columns = copyOut(copyIn(value, "console.table columns"), true) - return Array.isArray(columns) ? columns.map((column) => String(column)) : undefined - } - - private consoleTableRows( - data: unknown, - columns: ReadonlyArray | undefined, - ): Array<{ readonly index: string; readonly values: Record }> { - if (Array.isArray(data)) { - return data.map((item, index) => ({ index: String(index), values: this.consoleTableValues(item, columns) })) - } - if (data !== null && typeof data === "object" && !isSandboxValue(data)) { - return Object.entries(data).map(([index, item]) => ({ index, values: this.consoleTableValues(item, columns) })) - } - return [{ index: "0", values: { Value: data } }] - } - - private consoleTableValues(value: unknown, columns: ReadonlyArray | undefined): Record { - if (value !== null && typeof value === "object" && !Array.isArray(value) && !isSandboxValue(value)) { - const source = value as Record - if (columns !== undefined) return Object.fromEntries(columns.map((column) => [column, source[column]])) - return Object.fromEntries(Object.entries(source)) - } - return { Value: value } - } - - private formatConsoleTableCell(value: unknown): string { - if (value === undefined) return "" - if (typeof value === "string") return value - return this.formatConsoleValue(value, new Set(), 0) - } - private evaluateCallArguments(argNodes: Array): Effect.Effect, unknown, R> { const self = this return Effect.gen(function* () { @@ -2100,703 +1655,48 @@ class Interpreter { }) } - // Promise.* over ordinary runtime values. Combinators accept ANY array (or spreadable - // collection) mixing promise values and plain data - built inline, beforehand, via spread, - // whatever - because tool calls already run eagerly on their own fibers; the combinators - // only observe settlements. Joining is therefore sequential (no extra fibers) without - // costing parallelism, and the concurrency cap stays where the work is: the fork semaphore. - private invokePromiseMethod( - ref: PromiseMethodReference, - args: Array, - node: AstNode, - ): Effect.Effect { - const self = this - if (ref.name === "resolve") { - // Promise.resolve of a promise is that promise (JS flattens); anything else is a - // promise already fulfilled with the value. - const value = args[0] - return Effect.succeed( - value instanceof SandboxPromise ? value : new SandboxPromise(undefined, Effect.succeed(value)), - ) - } - if (ref.name === "reject") { - return Effect.sync(() => new SandboxPromise(undefined, Effect.fail(new ProgramThrow(args[0])))) - } - - const items = Array.isArray(args[0]) ? args[0] : spreadItems(args[0]) - if (items === undefined) { - throw new InterpreterRuntimeError( - `Promise.${ref.name} expects an array of promises or plain values (e.g. Promise.${ref.name}(items.map((item) => tools.ns.tool(item)))).`, - node, - ) - } - - switch (ref.name) { - case "all": { - // Mark every promise element observed up-front (Promise.all handles all of its - // members' failures, as in JS), then join in index order; the first failure rejects - // the whole call while unrelated in-flight members keep running. - const settles = items.map((item) => - item instanceof SandboxPromise ? this.settlePromise(item, node) : Effect.succeed(item), - ) - return Effect.gen(function* () { - const values: Array = [] - for (const settle of settles) values.push(yield* settle) - return values - }) - } - case "allSettled": { - const observations = items.map((item) => - item instanceof SandboxPromise - ? Effect.map(this.observePromise(item), (exit) => ({ promise: item as SandboxPromise | undefined, exit })) - : Effect.succeed({ promise: undefined as SandboxPromise | undefined, exit: Exit.succeed(item as unknown) }), - ) - return Effect.gen(function* () { - const outcomes: Array = [] - for (const observation of observations) { - const { exit, promise } = yield* observation - if (Exit.isSuccess(exit)) { - outcomes.push( - Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }), - ) - continue - } - const raceInterrupted = promise?.interrupted === true && Cause.hasInterruptsOnly(exit.cause) - if (Cause.hasInterruptsOnly(exit.cause) && !raceInterrupted) { - // Execution teardown (timeout/host interruption), not a program-level rejection. - return yield* Effect.failCause(exit.cause) - } - const thrown = raceInterrupted - ? new InterpreterRuntimeError( - "This tool call was interrupted because another value settled a Promise.race first.", - node, - ) - : Cause.squash(exit.cause) - outcomes.push( - Object.assign(Object.create(null) as SafeObject, { - status: "rejected", - reason: caughtErrorValue(thrown), - }), - ) - } - return outcomes - }) - } - case "race": { - if (items.length === 0) { - throw new InterpreterRuntimeError( - "Promise.race([]) would never settle; provide at least one promise or value.", - node, - ) - } - const observations = items.map((item, index) => - item instanceof SandboxPromise - ? Effect.map(this.observePromise(item), (exit) => ({ index, exit })) - : Effect.succeed({ index, exit: Exit.succeed(item as unknown) }), - ) - return Effect.gen(function* () { - // First settlement (fulfilled OR rejected) wins; the observations never fail, so - // racing them yields exactly that. Losing in-flight calls are then interrupted. - const winner = yield* Effect.raceAll(observations) - for (const [index, item] of items.entries()) { - if (index === winner.index || !(item instanceof SandboxPromise) || item.fiber === undefined) continue - item.interrupted = true - yield* Fiber.interrupt(item.fiber) - } - const winningItem = items[winner.index] - return yield* self.unwrapPromiseExit( - winningItem instanceof SandboxPromise ? winningItem : undefined, - winner.exit, - node, - ) - }) - } - } - } - private invokeFunction(fn: CodeModeFunction, args: Array): Effect.Effect { - const self = this - return Effect.suspend(() => { - const savedScopes = self.scopes - self.scopes = [...fn.capturedScopes, new Map()] - const run = Effect.gen(function* () { - // Seed every parameter name into the scope as a TDZ slot first, so a default that - // references another parameter resolves to that (uninitialized) param rather than - // silently falling through to an outer binding of the same name - matching JS. - const paramScope = self.currentScope() - for (const parameter of fn.parameters) { - for (const name of collectPatternNames(parameter)) { - paramScope.set(name, { mutable: true, value: undefined, initialized: false }) - } + const invocation = new Interpreter(this.invokeTool, this.invokeSearch, this.toolKeys, this.promises, this.logs) + invocation.scopes = new ScopeStack([...fn.capturedScopes, new Map()]) + const run = Effect.gen(function* () { + // Seed all parameters first so defaults cannot fall through to same-named outer bindings. + const paramScope = invocation.scopes.current() + for (const parameter of fn.parameters) { + for (const name of collectPatternNames(parameter)) { + paramScope.set(name, { mutable: true, value: undefined, initialized: false }) } - for (const [index, parameter] of fn.parameters.entries()) { - if (parameter.type === "RestElement") { - yield* self.declarePattern(getNode(parameter, "argument"), args.slice(index), true, parameter) - break - } - yield* self.declarePattern(parameter, args[index], true, parameter) + } + for (const [index, parameter] of fn.parameters.entries()) { + if (parameter.type === "RestElement") { + yield* invocation.declarePattern(getNode(parameter, "argument"), args.slice(index), true, parameter, true) + break } + yield* invocation.declarePattern(parameter, args[index], true, parameter, true) + } - if (fn.body.type === "BlockStatement") { - const result = yield* self.evaluateStatement(fn.body) - return result.kind === "return" || result.kind === "value" ? result.value : undefined - } + if (fn.body.type === "BlockStatement") { + const result = yield* invocation.evaluateStatement(fn.body) + return result.kind === "return" ? result.value : undefined + } - return yield* self.evaluateExpression(fn.body) - }) - return run.pipe( - Effect.ensuring( - Effect.sync(() => { - self.scopes = savedScopes - }), - ), - ) + return yield* invocation.evaluateExpression(fn.body) }) - } - - private invokeIntrinsic( - ref: IntrinsicReference, - args: Array, - node: AstNode, - ): Effect.Effect { - if (typeof ref.receiver === "string") { - if ( - (ref.name === "replace" || ref.name === "replaceAll") && - (args[1] instanceof CodeModeFunction || args[1] instanceof CoercionFunction || args[1] instanceof UriFunction) - ) { - return this.invokeStringReplacer(ref.receiver, ref.name, args, node) - } - return Effect.succeed(invokeStringMethod(ref.receiver, ref.name, args, node)) - } - if (typeof ref.receiver === "number") { - return Effect.succeed(invokeNumberMethod(ref.receiver, ref.name, args, node)) - } - if (Array.isArray(ref.receiver)) { - return this.invokeArrayMethod(ref.receiver, ref.name, args, node) - } - if (ref.receiver instanceof SandboxDate) { - return Effect.succeed(invokeDateMethod(ref.receiver, ref.name, node)) - } - if (ref.receiver instanceof SandboxRegExp) { - return Effect.succeed(invokeRegExpMethod(ref.receiver, ref.name, args, node)) - } - if (ref.receiver instanceof SandboxMap) { - return this.invokeMapMethod(ref.receiver, ref.name, args, node) - } - if (ref.receiver instanceof SandboxSet) { - return this.invokeSetMethod(ref.receiver, ref.name, args, node) - } - if (ref.receiver instanceof SandboxURL) { - return Effect.succeed(invokeURLMethod(ref.receiver, ref.name, node)) - } - if (ref.receiver instanceof SandboxURLSearchParams) { - return this.invokeURLSearchParamsMethod(ref.receiver, ref.name, args, node) - } - throw new InterpreterRuntimeError(`Method '${ref.name}' is not available in CodeMode.`, node) - } - - private invokeStringReplacer( - value: string, - name: "replace" | "replaceAll", - args: Array, - node: AstNode, - ): Effect.Effect { - const apply = this.applyCollectionCallback(args[1], `String.${name}`, node) - const matches: Array<{ readonly match: string; readonly offset: number; readonly args: Array }> = [] - const collect = (...callbackArgs: Array): string => { - const match = callbackArgs[0] - const groups = callbackArgs[callbackArgs.length - 1] - const hasGroups = groups !== null && typeof groups === "object" - const offset = callbackArgs[callbackArgs.length - (hasGroups ? 3 : 2)] - if (typeof match !== "string" || typeof offset !== "number") { - throw new InterpreterRuntimeError(`String.${name} produced an invalid replacement match.`, node) - } - if (hasGroups) { - const safeGroups: SafeObject = Object.create(null) as SafeObject - for (const [key, group] of Object.entries(groups)) { - if (!isBlockedMember(key)) safeGroups[key] = group - } - callbackArgs[callbackArgs.length - 1] = safeGroups - } - matches.push({ match, offset, args: callbackArgs }) - return match - } - - const pattern = args[0] - if (pattern instanceof SandboxRegExp) { - if (name === "replaceAll" && !pattern.regex.global) { - throw new InterpreterRuntimeError( - `String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.regex.source}/${pattern.regex.flags}g, or use String.replace to replace only the first match.`, - node, - ) - } - if (name === "replace") value.replace(pattern.regex, collect) - else value.replaceAll(pattern.regex, collect) - } else { - if (typeof pattern !== "string") { - throw new InterpreterRuntimeError(`String.${name} expects argument 1 to be a string.`, node) - } - if (name === "replace") value.replace(pattern, collect) - else value.replaceAll(pattern, collect) - } - - return Effect.gen(function* () { - const output: Array = [] - let end = 0 - for (const match of matches) { - output.push( - value.slice(end, match.offset), - coerceToString(boundedData(yield* apply(match.args), `String.${name} replacer result`)), - ) - end = match.offset + match.match.length - } - output.push(value.slice(end)) - return boundedData(output.join(""), `String.${name} result`) - }) - } - - // Runs a collection callback accepting a user function or supported builtin callable, - // mirroring the array-method callback contract. - private applyCollectionCallback( - callback: unknown, - name: string, - node: AstNode, - ): (args: Array) => Effect.Effect { - if ( - !(callback instanceof CodeModeFunction) && - !(callback instanceof CoercionFunction) && - !(callback instanceof UriFunction) - ) { - throw new InterpreterRuntimeError(`${name} expects a function callback.`, node) - } - return (callbackArgs) => - callback instanceof CoercionFunction - ? Effect.succeed(invokeCoercion(callback, callbackArgs, node)) - : callback instanceof UriFunction - ? Effect.succeed(invokeUriFunction(callback, callbackArgs, node)) - : this.invokeFunction(callback, callbackArgs) - } - - private invokeMapMethod( - target: SandboxMap, - name: string, - args: Array, - node: AstNode, - ): Effect.Effect { - switch (name) { - case "get": - return Effect.succeed(target.map.get(args[0])) - case "has": - return Effect.succeed(target.map.has(args[0])) - case "set": - return Effect.sync(() => { - target.map.set(args[0], args[1]) - return target - }) - case "delete": - return Effect.sync(() => target.map.delete(args[0])) - case "clear": - return Effect.sync(() => { - target.map.clear() - return undefined - }) - case "keys": - return Effect.sync(() => Array.from(target.map.keys())) - case "values": - return Effect.sync(() => Array.from(target.map.values())) - case "entries": - return Effect.sync(() => Array.from(target.map.entries(), ([key, item]): Array => [key, item])) - case "forEach": { - const apply = this.applyCollectionCallback(args[0], "Map.forEach", node) - return Effect.gen(function* () { - // Snapshot iteration, matching the array-method callback contract. - for (const [key, item] of Array.from(target.map.entries())) yield* apply([item, key, target]) - return undefined - }) - } - default: - throw new InterpreterRuntimeError(`Map method '${name}' is not available in CodeMode.`, node) - } - } - - private invokeSetMethod( - target: SandboxSet, - name: string, - args: Array, - node: AstNode, - ): Effect.Effect { - switch (name) { - case "has": - return Effect.succeed(target.set.has(args[0])) - case "add": - return Effect.sync(() => { - target.set.add(args[0]) - return target - }) - case "delete": - return Effect.sync(() => target.set.delete(args[0])) - case "clear": - return Effect.sync(() => { - target.set.clear() - return undefined - }) - case "keys": - case "values": - return Effect.sync(() => Array.from(target.set.values())) - case "entries": - return Effect.sync(() => Array.from(target.set.values(), (item): Array => [item, item])) - case "forEach": { - const apply = this.applyCollectionCallback(args[0], "Set.forEach", node) - return Effect.gen(function* () { - for (const item of Array.from(target.set.values())) yield* apply([item, item, target]) - return undefined - }) - } - default: - throw new InterpreterRuntimeError(`Set method '${name}' is not available in CodeMode.`, node) - } - } - - private invokeURLSearchParamsMethod( - target: SandboxURLSearchParams, - name: string, - args: Array, - node: AstNode, - ): Effect.Effect { - const arg = (index: number): string => uriArgument(args[index], `URLSearchParams.${name} argument ${index + 1}`) - const requireArgs = (count: number): void => { - if (args.length < count) { - throw new InterpreterRuntimeError( - `URLSearchParams.${name} requires ${count} argument${count === 1 ? "" : "s"}.`, - node, - ).as("TypeError") - } - } - switch (name) { - case "append": { - requireArgs(2) - return Effect.sync(() => { - target.params.append(arg(0), arg(1)) - return undefined - }) - } - case "delete": { - requireArgs(1) - return Effect.sync(() => { - if (args[1] !== undefined) target.params.delete(arg(0), arg(1)) - else target.params.delete(arg(0)) - return undefined - }) - } - case "get": - requireArgs(1) - return Effect.sync(() => target.params.get(arg(0))) - case "getAll": - requireArgs(1) - return Effect.sync(() => target.params.getAll(arg(0))) - case "has": - requireArgs(1) - return Effect.sync(() => - args[1] !== undefined ? target.params.has(arg(0), arg(1)) : target.params.has(arg(0)), - ) - case "set": { - requireArgs(2) - return Effect.sync(() => { - target.params.set(arg(0), arg(1)) - return undefined - }) - } - case "sort": - return Effect.sync(() => { - target.params.sort() - return undefined - }) - case "keys": - return Effect.sync(() => Array.from(target.params.keys())) - case "values": - return Effect.sync(() => Array.from(target.params.values())) - case "entries": - return Effect.sync(() => Array.from(target.params.entries(), ([key, value]): Array => [key, value])) - case "toString": - return Effect.sync(() => target.params.toString()) - case "forEach": { - requireArgs(1) - const apply = this.applyCollectionCallback(args[0], "URLSearchParams.forEach", node) - return Effect.gen(function* () { - for (const [key, value] of Array.from(target.params.entries())) yield* apply([value, key, target]) - return undefined - }) - } - default: - throw new InterpreterRuntimeError(`URLSearchParams method '${name}' is not available in CodeMode.`, node) - } - } - - private invokeArrayMethod( - target: Array, - name: string, - args: Array, - node: AstNode, - ): Effect.Effect { - const optNumber = (value: unknown, label: string): number | undefined => { - if (value === undefined) return undefined - if (typeof value !== "number") - throw new InterpreterRuntimeError(`Array.${name} expects ${label} to be a number.`, node) - return value - } - switch (name) { - case "join": { - if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) { - throw new InterpreterRuntimeError("Array.join expects zero arguments or one string separator.", node) - } - const input = boundedData(target, "Array.join input") as Array - return Effect.succeed( - input.map((item) => coerceToString(item ?? "")).join(args.length === 0 ? "," : (args[0] as string)), - ) - } - case "includes": - if (args.length === 0 || args.length > 2) - throw new InterpreterRuntimeError("Array.includes expects a value and optional start index.", node) - return Effect.succeed(target.includes(args[0], optNumber(args[1], "start index"))) - case "indexOf": - return Effect.succeed(target.indexOf(args[0], optNumber(args[1], "start index"))) - case "lastIndexOf": - return Effect.succeed( - args[1] === undefined - ? target.lastIndexOf(args[0]) - : target.lastIndexOf(args[0], optNumber(args[1], "start index")), - ) - case "at": - return Effect.succeed(target.at(optNumber(args[0], "index") ?? 0)) - case "slice": - return Effect.succeed(target.slice(optNumber(args[0], "start"), optNumber(args[1], "end"))) - case "concat": - return Effect.succeed(target.concat(...args)) - case "flat": - return Effect.succeed(target.flat(optNumber(args[0], "depth") ?? 1)) - case "reverse": - return Effect.succeed([...target].reverse()) - case "sort": - case "toSorted": - return this.sortArray(target, args[0], node) - case "toReversed": - return Effect.succeed([...target].reverse()) - case "with": { - const index = optNumber(args[0], "index") ?? 0 - const resolved = index < 0 ? target.length + index : index - if (resolved < 0 || resolved >= target.length) { - throw new InterpreterRuntimeError("Array.with index is out of range.", node) - } - const copied = [...target] - copied[resolved] = args[1] - return Effect.succeed(copied) - } - case "push": { - // Validate before mutating (so no rollback is needed): inserting a container into - // itself would create a cycle no later walk could survive. - for (const item of args) this.rejectCircularInsertion(target, item, "Array.push result", node) - target.push(...args) - return Effect.succeed(target.length) - } - case "unshift": { - for (const item of args) this.rejectCircularInsertion(target, item, "Array.unshift result", node) - target.unshift(...args) - return Effect.succeed(target.length) - } - case "pop": - return Effect.succeed(target.pop()) - case "shift": - return Effect.succeed(target.shift()) - case "splice": { - // Mutates in place and returns the removed elements, exactly like JS: one argument - // removes to the end, an undefined delete count removes nothing. - if (args.length === 0) return Effect.succeed(target.splice(0, 0)) - const start = optNumber(args[0], "start") ?? 0 - if (args.length === 1) return Effect.succeed(target.splice(start)) - const deleteCount = optNumber(args[1], "delete count") ?? 0 - const inserted = args.slice(2) - for (const item of inserted) this.rejectCircularInsertion(target, item, "Array.splice result", node) - return Effect.succeed(target.splice(start, deleteCount, ...inserted)) - } - case "fill": { - this.rejectCircularInsertion(target, args[0], "Array.fill result", node) - return Effect.succeed(target.fill(args[0], optNumber(args[1], "start"), optNumber(args[2], "end"))) - } - case "copyWithin": - return Effect.succeed( - target.copyWithin( - optNumber(args[0], "target index") ?? 0, - optNumber(args[1], "start") ?? 0, - optNumber(args[2], "end"), - ), - ) - // keys/values/entries return arrays (not iterators), matching the Map/Set convention; - // they work with for...of and spread either way. - case "keys": - return Effect.succeed(Array.from(target.keys())) - case "values": - return Effect.succeed([...target]) - case "entries": - return Effect.succeed(Array.from(target.entries(), ([index, item]): Array => [index, item])) - } - - const callback = args[0] - if ( - !(callback instanceof CodeModeFunction) && - !(callback instanceof CoercionFunction) && - !(callback instanceof UriFunction) - ) { - throw new InterpreterRuntimeError(`Array.${name} expects a function callback.`, node) - } - const self = this - // Accept a user function or supported builtin callable, so idioms such as - // `filter(Boolean)`, `map(String)`, and `map(encodeURIComponent)` work as in JS. Builtins - // are synchronous; only CodeModeFunctions can await tool calls. - const apply = (callbackArgs: Array): Effect.Effect => - callback instanceof CoercionFunction - ? Effect.succeed(invokeCoercion(callback, callbackArgs, node)) - : callback instanceof UriFunction - ? Effect.succeed(invokeUriFunction(callback, callbackArgs, node)) - : self.invokeFunction(callback, callbackArgs) - return Effect.gen(function* () { - // Iterate a snapshot taken at call time so a callback that mutates the array can't - // self-extend the loop - matching JS, where elements appended during iteration are not visited. - const items = target.slice() - switch (name) { - case "map": { - const values: Array = [] - for (const [index, item] of items.entries()) values.push(yield* apply([item, index, items])) - return values - } - case "flatMap": { - const values: Array = [] - for (const [index, item] of items.entries()) { - const mapped = yield* apply([item, index, items]) - if (Array.isArray(mapped)) values.push(...mapped) - else values.push(mapped) - } - return values - } - case "filter": { - const values: Array = [] - for (const [index, item] of items.entries()) { - if (yield* apply([item, index, items])) values.push(item) - } - return values - } - case "find": - for (const [index, item] of items.entries()) { - if (yield* apply([item, index, items])) return item - } - return undefined - case "findIndex": - for (const [index, item] of items.entries()) { - if (yield* apply([item, index, items])) return index - } - return -1 - case "some": - for (const [index, item] of items.entries()) { - if (yield* apply([item, index, items])) return true - } - return false - case "every": - for (const [index, item] of items.entries()) { - if (!(yield* apply([item, index, items]))) return false - } - return true - case "forEach": - for (const [index, item] of items.entries()) yield* apply([item, index, items]) - return undefined - case "reduce": { - let accumulator: unknown - let start: number - if (args.length >= 2) { - accumulator = args[1] - start = 0 - } else { - if (items.length === 0) - throw new InterpreterRuntimeError("Array.reduce of an empty array with no initial value.", node) - accumulator = items[0] - start = 1 - } - for (let index = start; index < items.length; index += 1) { - accumulator = yield* apply([accumulator, items[index], index, items]) - } - return accumulator - } - case "reduceRight": { - let accumulator: unknown - let start: number - if (args.length >= 2) { - accumulator = args[1] - start = items.length - 1 - } else { - if (items.length === 0) - throw new InterpreterRuntimeError("Array.reduceRight of an empty array with no initial value.", node) - accumulator = items[items.length - 1] - start = items.length - 2 - } - for (let index = start; index >= 0; index -= 1) { - accumulator = yield* apply([accumulator, items[index], index, items]) - } - return accumulator - } - case "findLast": - for (let index = items.length - 1; index >= 0; index -= 1) { - if (yield* apply([items[index], index, items])) return items[index] - } - return undefined - case "findLastIndex": - for (let index = items.length - 1; index >= 0; index -= 1) { - if (yield* apply([items[index], index, items])) return index - } - return -1 - } - throw new InterpreterRuntimeError(`Array method '${name}' is not available in CodeMode.`, node) - }) - } - - private sortArray( - target: Array, - comparator: unknown, - node: AstNode, - ): Effect.Effect, unknown, R> { - if (comparator !== undefined && !(comparator instanceof CodeModeFunction)) { - throw new InterpreterRuntimeError("Array.sort expects an arrow function comparator.", node) - } - if (!(comparator instanceof CodeModeFunction)) { - return Effect.sync(() => - [...target].sort((a, b) => { - const left = coerceToString(a) - const right = coerceToString(b) - return left < right ? -1 : left > right ? 1 : 0 + if (!fn.async) return run + // The initial yield assigns `box.own` before the body can self-resolve. + const box: { own?: CodeModePromise } = {} + return Effect.map( + this.createPromise( + Effect.flatMap(run, (value) => { + if (!(value instanceof CodeModePromise)) return Effect.succeed(value) + if (value === box.own) return Effect.fail(selfResolutionError()) + return invocation.settlePromise(value) }), - ) - } - const self = this - const mergeSort = (items: Array): Effect.Effect, unknown, R> => { - if (items.length <= 1) return Effect.succeed(items) - const midpoint = Math.floor(items.length / 2) - return Effect.gen(function* () { - const left = yield* mergeSort(items.slice(0, midpoint)) - const right = yield* mergeSort(items.slice(midpoint)) - const merged: Array = [] - let leftIndex = 0 - let rightIndex = 0 - while (leftIndex < left.length && rightIndex < right.length) { - // Coerce the comparator's result like JS ToNumber (data objects -> NaN, never a host - // crash) and treat NaN as 0 - the spec's "no consistent order" -> keep the left element. - const order = coerceToNumber(yield* self.invokeFunction(comparator, [left[leftIndex], right[rightIndex]])) - if (Number.isNaN(order) || order <= 0) merged.push(left[leftIndex++]) - else merged.push(right[rightIndex++]) - } - return [...merged, ...left.slice(leftIndex), ...right.slice(rightIndex)] - }) - } - // Per spec, undefined elements sort to the end and the comparator is never called on them. - const defined = target.filter((item) => item !== undefined) - const undefinedCount = target.length - defined.length - return Effect.map(mergeSort(defined), (items) => [...items, ...Array(undefinedCount).fill(undefined)]) + ), + (promise) => { + box.own = promise + return promise + }, + ) } private evaluateObjectExpression(node: AstNode): Effect.Effect, unknown, R> { @@ -2809,10 +1709,7 @@ class Interpreter { if (property.type === "SpreadElement") { const spread = yield* self.evaluateExpression(getNode(property, "argument")) - // JS treats `{ ...null }` / `{ ...undefined }` as a no-op, so the common - // `{ ...maybeOpts, override }` merge works when the operand is absent. Sandbox values - // have no own enumerable properties in JS, so they are no-ops too. - if (spread === null || spread === undefined || isSandboxValue(spread)) continue + if (spread === null || spread === undefined || isCodeModeValue(spread)) continue if (typeof spread !== "object" || Array.isArray(spread) || isRuntimeReference(spread)) { throw new InterpreterRuntimeError( "Object spread requires a data object in CodeMode.", @@ -2870,7 +1767,8 @@ class Interpreter { return Effect.gen(function* () { for (const elementValue of elements) { if (elementValue === null) { - values.push(undefined) + // A literal elision is a real hole, like JS: extend length without an own index. + values.length += 1 continue } const element = asNode(elementValue, "elements") @@ -2911,8 +1809,6 @@ class Interpreter { if (index < expressions.length) { const raw = yield* self.evaluateExpression(asNode(expressions[index], "expressions")) - // The preserving checkpoint keeps sandbox values intact, so coerceToString renders - // them directly (ISO date, /regex/ literal form) instead of a JSON-serialized husk. output += coerceToString(boundedData(raw, "Template interpolation")) } } @@ -2928,10 +1824,6 @@ class Interpreter { } private applyCompoundAssignment(operator: string, current: unknown, incoming: unknown, node: AstNode): unknown { - // `x op= y` is `x = x op y`: dispatch through the shared binary operator implementation - // so compound assignment inherits the same coercion semantics (Dates, data objects, ...). - // Only the arithmetic/bitwise operators are compoundable; logical assignments (&&=/||=/??=) - // short-circuit and are handled by evaluateLogicalAssignment before reaching here. if (!compoundOperators.has(operator)) { throw new InterpreterRuntimeError(`Unsupported assignment operator '${operator}'.`, node) } @@ -2940,10 +1832,12 @@ class Interpreter { private getMemberReference( node: AstNode, + operation: "read" | "delete" = "read", ): Effect.Effect< | MemberReference | ToolReference | PromiseMethodReference + | PromiseInstanceMethodReference | IntrinsicReference | GlobalMethodReference | ComputedValue @@ -2969,8 +1863,8 @@ class Interpreter { : self.toPropertyKey(yield* self.evaluateExpression(propertyNode), propertyNode) if (objectValue instanceof ToolReference) { - if (typeof key !== "string" || isBlockedMember(key)) { - throw new InterpreterRuntimeError("Tool paths must use safe string property names.", propertyNode) + if (typeof key !== "string") { + throw new InterpreterRuntimeError("Tool paths must use string property names.", propertyNode) } return new ToolReference([...objectValue.path, key]) } @@ -2980,22 +1874,24 @@ class Interpreter { return new PromiseMethodReference(key as PromiseMethodName) } throw new InterpreterRuntimeError( - `Promise.${String(key)} is not available in CodeMode. Available: Promise.all, Promise.allSettled, Promise.race, Promise.resolve, and Promise.reject; consume promises with await.`, + `Promise.${String(key)} is not available in CodeMode. Available: Promise.all, Promise.allSettled, Promise.race, Promise.any, Promise.resolve, and Promise.reject; consume promises with await.`, propertyNode, ) } if (objectValue instanceof GlobalNamespace) { - if (typeof key !== "string" || isBlockedMember(key)) { - throw new InterpreterRuntimeError( - `${objectValue.name}.${String(key)} is not available in CodeMode.`, - propertyNode, - ) + if (typeof key === "string" && isBlockedMember(key)) { + throw new InterpreterRuntimeError(`${objectValue.name}.${key} is not available in CodeMode.`, propertyNode) } + if (typeof key !== "string") return new ComputedValue(undefined) if (objectValue.name === "Math" && mathConstants.has(key)) { return new ComputedValue((Math as unknown as Record)[key]) } - return new GlobalMethodReference(objectValue.name, key) + if (globalStaticMembers[objectValue.name]?.has(key)) { + return new GlobalMethodReference(objectValue.name, key) + } + // Unknown static members read as undefined so feature detection works like native JS. + return new ComputedValue(undefined) } if (typeof objectValue === "string") { @@ -3003,52 +1899,53 @@ class Interpreter { if (typeof key === "number") return new ComputedValue(objectValue[key]) if (typeof key === "string" && /^\d+$/.test(key)) return new ComputedValue(objectValue[Number(key)]) if (typeof key === "string" && stringMethods.has(key)) return new IntrinsicReference(objectValue, key) - // Unknown property on a string reads as `undefined`, matching JS (`"x".foo === undefined`), - // instead of throwing - so defensive access like `result?.login ?? result` on a JSON-string - // tool result doesn't crash. (Optional chaining only guards null/undefined receivers, so a - // real string still reaches here.) Only the method allowlist above yields callables. return new ComputedValue(undefined) } if (typeof objectValue === "number") { if (typeof key === "string" && numberMethods.has(key)) return new IntrinsicReference(objectValue, key) - // Unknown property on a number reads as `undefined`, matching JS, rather than throwing. return new ComputedValue(undefined) } - // Number / String expose a small allowlist of statics; everything else stays opaque. - if (objectValue instanceof CoercionFunction && typeof key === "string" && !isBlockedMember(key)) { + if (objectValue instanceof CoercionFunction) { + if (typeof key === "string" && isBlockedMember(key)) { + throw new InterpreterRuntimeError(`${objectValue.name}.${key} is not available in CodeMode.`, propertyNode) + } + if (typeof key !== "string") return new ComputedValue(undefined) if (objectValue.name === "Number" && numberConstants.has(key)) { return new ComputedValue((Number as unknown as Record)[key]) } - if (objectValue.name === "Number" && numberStatics.has(key)) return new GlobalMethodReference("Number", key) - if (objectValue.name === "String" && stringStatics.has(key)) return new GlobalMethodReference("String", key) + if (objectValue.name === "Number" && numberStatics.has(key)) { + return new GlobalMethodReference("Number", key) + } + if (objectValue.name === "String" && stringStatics.has(key)) { + return new GlobalMethodReference("String", key) + } + return new ComputedValue(undefined) } - // Sandbox value types expose their method/property allowlists; any other key reads as - // `undefined`, consistent with unknown-property reads on strings/numbers/arrays. - if (objectValue instanceof SandboxDate) { + if (objectValue instanceof CodeModeDate) { if (typeof key === "string" && dateMethods.has(key)) return new IntrinsicReference(objectValue, key) return new ComputedValue(undefined) } - if (objectValue instanceof SandboxRegExp) { + if (objectValue instanceof CodeModeRegExp) { if (typeof key === "string" && regexpProperties.has(key)) { return new ComputedValue((objectValue.regex as unknown as Record)[key]) } if (typeof key === "string" && regexpMethods.has(key)) return new IntrinsicReference(objectValue, key) return new ComputedValue(undefined) } - if (objectValue instanceof SandboxMap) { + if (objectValue instanceof CodeModeMap) { if (key === "size") return new ComputedValue(objectValue.map.size) if (typeof key === "string" && mapMethods.has(key)) return new IntrinsicReference(objectValue, key) return new ComputedValue(undefined) } - if (objectValue instanceof SandboxSet) { + if (objectValue instanceof CodeModeSet) { if (key === "size") return new ComputedValue(objectValue.set.size) if (typeof key === "string" && setMethods.has(key)) return new IntrinsicReference(objectValue, key) return new ComputedValue(undefined) } - if (objectValue instanceof SandboxURL) { + if (objectValue instanceof CodeModeURL) { if (key === "searchParams") { return new ComputedValue(objectValue.searchParams) } @@ -3056,7 +1953,7 @@ class Interpreter { if (typeof key === "string" && urlProperties.has(key)) return { target: objectValue, key } return new ComputedValue(undefined) } - if (objectValue instanceof SandboxURLSearchParams) { + if (objectValue instanceof CodeModeURLSearchParams) { if (key === "size") return new ComputedValue(objectValue.params.size) if (typeof key === "string" && urlSearchParamsMethods.has(key)) { return new IntrinsicReference(objectValue, key) @@ -3064,20 +1961,13 @@ class Interpreter { return new ComputedValue(undefined) } - // Any property access on a promise is a confused program (`p.then(...)`, `p.value`); - // reading `undefined` here would hide the missing await, so both paths get an explicit, - // await-hinting error instead of the forgiving unknown-property fallthrough. - if (objectValue instanceof SandboxPromise) { + // Reject unknown promise properties so a missing await cannot hide. + if (objectValue instanceof CodeModePromise) { if (key === "then" || key === "catch" || key === "finally") { - throw new InterpreterRuntimeError( - `Promise.prototype.${String(key)} is not supported in CodeMode; use await instead (with try/catch to handle failures) - e.g. \`const result = await tools.ns.tool(...)\`.`, - propertyNode, - "UnsupportedSyntax", - [supportedSyntaxMessage], - ) + return new PromiseInstanceMethodReference(objectValue, key) } throw new InterpreterRuntimeError( - "This value is an un-awaited Promise and has no readable properties; await it first - e.g. `const result = await tools.ns.tool(...)`.", + "This value is an un-awaited Promise; await it first - e.g. `const result = await tools.ns.tool(...)`.", objectNode, "InvalidDataValue", ) @@ -3100,19 +1990,16 @@ class Interpreter { } if (Array.isArray(objectValue)) { + if (operation === "delete") return { target: objectValue, key } if ( key !== "length" && !(typeof key === "string" && arrayMethods.has(key)) && typeof key !== "number" && !/^\d+$/.test(key) ) { - // Own non-index properties read through (match results carry index/groups); like JS, - // they are readable in place and dropped by JSON at data boundaries. if (typeof key === "string" && Object.hasOwn(objectValue, key)) { return new ComputedValue((objectValue as Record & Array)[key]) } - // Unknown property on an array reads as `undefined`, matching JS (`[1,2].foo === undefined`), - // instead of throwing - so defensive access under optional chaining behaves as expected. return new ComputedValue(undefined) } return { target: objectValue, key } @@ -3130,6 +2017,7 @@ class Interpreter { reference === undefined || reference instanceof ToolReference || reference instanceof PromiseMethodReference || + reference instanceof PromiseInstanceMethodReference || reference instanceof IntrinsicReference || reference instanceof GlobalMethodReference ) @@ -3140,7 +2028,7 @@ class Interpreter { } return reference.key === "length" ? reference.target.length : reference.target[Number(reference.key)] } - if (reference.target instanceof SandboxURL) { + if (reference.target instanceof CodeModeURL) { return (reference.target.url as unknown as Record)[String(reference.key)] } return reference.target[String(reference.key)] @@ -3151,9 +2039,30 @@ class Interpreter { return this.modifyMember(node, () => Effect.succeed({ write: true, next: value, result: value })) } - // Resolves the member reference EXACTLY ONCE (so a side-effecting object/key expression - // runs once), then lets `compute` decide whether to write - enabling compound assignment, - // updates, plain writes, and short-circuiting logical assignment to share one safe path. + private evaluateDeleteExpression(argument: AstNode): Effect.Effect { + const target = argument.type === "ChainExpression" ? getNode(argument, "expression") : argument + if (target.type !== "MemberExpression") { + throw new InterpreterRuntimeError("Only data fields may be deleted in CodeMode.", argument) + } + return Effect.map(this.getMemberReference(target, "delete"), (reference) => { + if (reference === OptionalShortCircuit) return true + if ( + reference instanceof ComputedValue || + reference === undefined || + reference instanceof ToolReference || + reference instanceof PromiseMethodReference || + reference instanceof PromiseInstanceMethodReference || + reference instanceof IntrinsicReference || + reference instanceof GlobalMethodReference || + reference.target instanceof CodeModeURL + ) { + throw new InterpreterRuntimeError("Only data fields may be deleted in CodeMode.", target, "InvalidDataValue") + } + return Reflect.deleteProperty(reference.target, reference.key) + }) + } + + // Resolve side-effecting object and key expressions exactly once. private modifyMember( node: AstNode, compute: (current: unknown) => Effect.Effect<{ write: boolean; next: unknown; result: unknown }, unknown, R>, @@ -3167,6 +2076,7 @@ class Interpreter { reference === undefined || reference instanceof ToolReference || reference instanceof PromiseMethodReference || + reference instanceof PromiseInstanceMethodReference || reference instanceof IntrinsicReference || reference instanceof GlobalMethodReference ) { @@ -3181,7 +2091,7 @@ class Interpreter { } const key = Array.isArray(reference.target) ? Number(reference.key) : String(reference.key) const current = - reference.target instanceof SandboxURL + reference.target instanceof CodeModeURL ? (reference.target.url as unknown as Record)[key] : (reference.target as Record)[key] const { write, next, result } = yield* compute(current) @@ -3190,24 +2100,6 @@ class Interpreter { }) } - // Rejects inserting a value that (transitively) contains the container it is being inserted - // into - the mutation that would create a circular structure no later walk could survive. - private rejectCircularInsertion( - container: object, - value: unknown, - label: string, - node: AstNode, - seen = new Set(), - ): void { - if (value === container) - throw new InterpreterRuntimeError(`${label} contains a circular value.`, node, "InvalidDataValue") - if (value === null || typeof value !== "object" || isRuntimeReference(value) || seen.has(value)) return - seen.add(value) - const items = Array.isArray(value) ? value : Object.values(value) - for (const item of items) this.rejectCircularInsertion(container, item, label, node, seen) - seen.delete(value) - } - private assignToReference(reference: MemberReference, key: number | string, next: unknown, node: AstNode): void { if (Array.isArray(reference.target)) { const target = reference.target @@ -3219,11 +2111,11 @@ class Interpreter { "InvalidDataValue", ) } - this.rejectCircularInsertion(target, next, "Array assignment result", node) + rejectCircularInsertion(target, next, "Array assignment result", node) target[index] = next return } - if (reference.target instanceof SandboxURL) { + if (reference.target instanceof CodeModeURL) { const property = key as string if (!urlWritableProperties.has(property)) { throw new InterpreterRuntimeError(`URL.${property} is read-only.`, node).as("TypeError") @@ -3239,7 +2131,7 @@ class Interpreter { } const target = reference.target as SafeObject const objectKey = key as string - this.rejectCircularInsertion(target, next, "Object assignment result", node) + rejectCircularInsertion(target, next, "Object assignment result", node) target[objectKey] = next } @@ -3250,216 +2142,4 @@ class Interpreter { throw new InterpreterRuntimeError("Property key must be a string or number.", node) } - - private declare(name: string, value: unknown, mutable: boolean, node: AstNode): void { - const scope = this.currentScope() - - // A pre-seeded parameter slot (initialized === false) is being bound for the first time; - // anything else already present is a genuine duplicate declaration. - const existing = scope.get(name) - if (existing && existing.initialized !== false) { - throw new InterpreterRuntimeError(`Identifier '${name}' has already been declared.`, node) - } - - scope.set(name, { mutable, value, initialized: true }) - } - - private getIdentifierValue(name: string, node: AstNode): unknown { - const binding = this.resolveBinding(name) - - if (!binding) { - throw new InterpreterRuntimeError(`Unknown identifier '${name}'.`, node).as("ReferenceError") - } - - // A parameter default that forward-references a later (not-yet-bound) parameter - JS TDZ. - if (binding.initialized === false) { - throw new InterpreterRuntimeError(`Cannot access '${name}' before initialization.`, node).as("ReferenceError") - } - - return binding.value - } - - private setIdentifierValue(name: string, value: unknown, node: AstNode): unknown { - const binding = this.resolveBinding(name) - - if (!binding) { - throw new InterpreterRuntimeError(`Unknown identifier '${name}'.`, node).as("ReferenceError") - } - - if (!binding.mutable) { - throw new InterpreterRuntimeError(`Cannot assign to constant '${name}'.`, node).as("TypeError") - } - - binding.value = value - return value - } - - private resolveBinding(name: string): Binding | undefined { - for (let index = this.scopes.length - 1; index >= 0; index -= 1) { - const scope = this.scopes[index] - const binding = scope?.get(name) - - if (binding) { - return binding - } - } - - return undefined - } - - private currentScope(): Map { - const scope = this.scopes[this.scopes.length - 1] - - if (!scope) { - throw new InterpreterRuntimeError("Interpreter scope stack is empty.") - } - - return scope - } - - private pushScope(): void { - this.scopes.push(new Map()) - } - - private popScope(): void { - this.scopes.pop() - } -} - -/** - * Executes one Effect-native CodeMode program without constructing a reusable runtime. - * - * @example - * ```ts - * const result = yield* CodeMode.execute({ - * tools: { lookup }, - * code: `return await tools.lookup({ id: "order_42" })`, - * }) - * ``` - */ -export const executeWithLimits = >( - options: ExecuteOptions, - limits: ResolvedExecutionLimits, - searchIndex: ToolRuntime.DiscoveryPlan["searchIndex"], -): Effect.Effect> => { - const hooks = { - ...(options.onToolCallStart === undefined ? {} : { onToolCallStart: options.onToolCallStart }), - ...(options.onToolCallEnd === undefined ? {} : { onToolCallEnd: options.onToolCallEnd }), - } - const tools = ToolRuntime.make( - (options.tools ?? {}) as HostTools>, - limits.maxToolCalls, - searchIndex, - hooks, - ) - const logs: Array = [] - const logged = () => (logs.length > 0 ? { logs: [...logs] } : {}) - - if (options.code.trim().length === 0) { - return Effect.succeed({ - ok: false, - error: { kind: "ParseError", message: "Code cannot be empty." }, - toolCalls: tools.calls, - }) - } - - const operation = Effect.gen(function* () { - const program = parseProgram(options.code) - const interpreter = new Interpreter>(tools.invoke, tools.keys, logs) - const value = yield* interpreter.run(program) - const result = copyOut(copyIn(value, "Execution result"), true) as DataValue - return { - ok: true, - value: result, - ...logged(), - toolCalls: tools.calls, - } satisfies Result - }).pipe((program) => { - const timeoutMs = limits.timeoutMs - if (timeoutMs === undefined) return program - return program.pipe( - Effect.timeoutOrElse({ - duration: timeoutMs, - orElse: () => - Effect.succeed({ - ok: false, - error: { kind: "TimeoutExceeded", message: `Execution timed out after ${timeoutMs}ms.` }, - ...logged(), - toolCalls: tools.calls, - } satisfies Result), - }), - ) - }) - - return operation.pipe( - Effect.catchCause((cause) => - Cause.hasInterruptsOnly(cause) - ? Effect.interrupt - : Effect.succeed({ - ok: false, - error: normalizeError(Cause.squash(cause)), - ...logged(), - toolCalls: tools.calls, - } satisfies Result), - ), - Effect.map((result) => (limits.maxOutputBytes === undefined ? result : boundOutput(result, limits.maxOutputBytes))), - ) -} - -const utf8ByteLength = (value: string): number => new TextEncoder().encode(value).byteLength - -// Truncates to a UTF-8 byte budget without splitting a code point (a split multi-byte -// sequence decodes to a replacement character, which is dropped). -const utf8Truncate = (value: string, maxBytes: number): string => { - const bytes = new TextEncoder().encode(value) - if (bytes.byteLength <= maxBytes) return value - const text = new TextDecoder("utf-8").decode(bytes.slice(0, Math.max(0, maxBytes))) - return text.endsWith("\uFFFD") ? text.slice(0, -1) : text -} - -/** - * Bounds the model-facing output (serialized result value plus logs) to `maxOutputBytes`. - * Oversized values are replaced by their truncated serialized text with an explanatory marker, - * and logs are kept from the start until the remaining budget is exhausted. Truncation never - * fails the execution; `truncated: true` marks affected results. Only runs when the host set - * `maxOutputBytes` - with the limit absent, output passes through unbounded. - */ -const boundOutput = (result: Result, maxOutputBytes: number): Result => { - let truncated = false - - let value: DataValue = null - let valueBytes = 0 - if (result.ok) { - const serialized = JSON.stringify(result.value) ?? "null" - const bytes = utf8ByteLength(serialized) - if (bytes > maxOutputBytes) { - truncated = true - value = `${utf8Truncate(serialized, maxOutputBytes)} [result truncated: ${bytes} bytes exceeds the ${maxOutputBytes}-byte output limit; return a smaller value]` - valueBytes = maxOutputBytes - } else { - value = result.value - valueBytes = bytes - } - } - - const logs = result.logs ?? [] - const kept: Array = [] - const logBudget = Math.max(0, maxOutputBytes - valueBytes) - let logBytes = 0 - for (const line of logs) { - const lineBytes = utf8ByteLength(line) + 1 - if (logBytes + lineBytes > logBudget) break - logBytes += lineBytes - kept.push(line) - } - if (kept.length < logs.length) { - truncated = true - kept.push(`[logs truncated: showing ${kept.length} of ${logs.length} lines]`) - } - - if (!truncated) return result - const logsPart = kept.length > 0 ? { logs: kept } : {} - return result.ok - ? { ok: true, value, ...logsPart, truncated: true, toolCalls: result.toolCalls } - : { ok: false, error: result.error, ...logsPart, truncated: true, toolCalls: result.toolCalls } } diff --git a/packages/codemode/src/interpreter/scope.ts b/packages/codemode/src/interpreter/scope.ts new file mode 100644 index 0000000000..ae3257de3f --- /dev/null +++ b/packages/codemode/src/interpreter/scope.ts @@ -0,0 +1,102 @@ +import { type AstNode, type Binding, InterpreterRuntimeError } from "./model.js" + +export class ScopeStack { + private readonly scopes: Array> + + constructor(scopes: Array>) { + this.scopes = scopes + } + + reserve(name: string, mutable: boolean, node: AstNode): void { + const scope = this.current() + if (scope.has(name)) { + throw new InterpreterRuntimeError(`Identifier '${name}' has already been declared.`, node) + } + scope.set(name, { mutable, value: undefined, initialized: false }) + } + + initialize(name: string, value: unknown, node: AstNode): void { + const binding = this.current().get(name) + if (!binding || binding.initialized !== false) { + throw new InterpreterRuntimeError(`Identifier '${name}' has not been reserved for initialization.`, node) + } + binding.value = value + binding.initialized = true + } + + declare(name: string, value: unknown, mutable: boolean, node: AstNode): void { + const scope = this.current() + if (scope.has(name)) { + throw new InterpreterRuntimeError(`Identifier '${name}' has already been declared.`, node) + } + scope.set(name, { mutable, value, initialized: true }) + } + + get(name: string, node: AstNode): unknown { + const binding = this.resolve(name) + + if (!binding) { + throw new InterpreterRuntimeError(`Unknown identifier '${name}'.`, node).as("ReferenceError") + } + + if (binding.initialized === false) { + throw new InterpreterRuntimeError(`Cannot access '${name}' before initialization.`, node).as("ReferenceError") + } + + return binding.value + } + + set(name: string, value: unknown, node: AstNode): unknown { + const binding = this.resolve(name) + + if (!binding) { + throw new InterpreterRuntimeError(`Unknown identifier '${name}'.`, node).as("ReferenceError") + } + + if (binding.initialized === false) { + throw new InterpreterRuntimeError(`Cannot access '${name}' before initialization.`, node).as("ReferenceError") + } + + if (!binding.mutable) { + throw new InterpreterRuntimeError(`Cannot assign to constant '${name}'.`, node).as("TypeError") + } + + binding.value = value + return value + } + + resolve(name: string): Binding | undefined { + for (let index = this.scopes.length - 1; index >= 0; index -= 1) { + const scope = this.scopes[index] + const binding = scope?.get(name) + + if (binding) { + return binding + } + } + + return undefined + } + + current(): Map { + const scope = this.scopes[this.scopes.length - 1] + + if (!scope) { + throw new InterpreterRuntimeError("Interpreter scope stack is empty.") + } + + return scope + } + + push(scope: Map = new Map()): void { + this.scopes.push(scope) + } + + pop(): void { + this.scopes.pop() + } + + capture(): Array> { + return this.scopes.slice() + } +} diff --git a/packages/codemode/src/openapi/TODO.md b/packages/codemode/src/openapi/TODO.md index cbcfe81a68..05558ace07 100644 --- a/packages/codemode/src/openapi/TODO.md +++ b/packages/codemode/src/openapi/TODO.md @@ -5,11 +5,15 @@ The initial adapter intentionally skips operations it cannot execute correctly. - Cookie parameters, authentication, and cookie-header merging. - Matrix, label, space-delimited, pipe-delimited, `allowReserved`, and parameter `content` serialization. - External references and complete nested `$defs` support. +- `$anchor` and nested `$id` resource resolution in directional (`readOnly`/`writeOnly`) projection. +- Use-site cleanup for `allOf` branches that reference shared component schemas: per-direction component definitions are projected globally, so a directional annotation declared only at one use site cannot remove the property from a referenced component's definition. +- Hidden-name cleanup inside `then`/`else`/`dependentSchemas`/`dependentRequired`, which constrain the same instance as `allOf`; a hidden property may remain named in those keywords. +- Projection inside `not`/`if`/`contains`, whose semantics would invert or shift if constraints were removed; those subschemas pass through unchanged, and a `$ref` from such a context to a projected `$defs` or component definition still observes hiding. +- Iterative traversal for pathologically deep schema nesting: the directional scan and projection recurse per level and overflow the stack around ten thousand levels, below the pre-existing converter limit of roughly fifty thousand; `fromSpec` throws a catchable `RangeError` either way. - Relative or templated server URLs and server variables. - Base URLs containing query strings or fragments. - Runtime response-schema validation and full content negotiation. - Binary response values and explicit byte-oriented return types. -- Request/response projection for `readOnly` and `writeOnly` properties. - SSE, WebSocket, and other streaming transports. - Recovery of responses rejected by a status-filtering `HttpClient`. - Configurable request and response size limits. diff --git a/packages/codemode/src/openapi/index.ts b/packages/codemode/src/openapi/index.ts index 7f1770ef36..5ea688129e 100644 --- a/packages/codemode/src/openapi/index.ts +++ b/packages/codemode/src/openapi/index.ts @@ -3,6 +3,7 @@ import { make, type Definition } from "../tool.js" import { invoke } from "./runtime.js" import { componentDefinitions, + hasDirectionalSchemas, inputSchema, isRecord, methods, @@ -31,16 +32,17 @@ export type { } from "./types.js" /** - * Builds a CodeMode tool subtree from an OpenAPI 3.x document, one tool per - * operation. Auth is resolved host-side via `auth.resolve` and never - * model-visible. Tools require `HttpClient.HttpClient`; unrepresentable - * operations land in `skipped`. + * Builds one CodeMode tool per representable OpenAPI 3.x operation. Auth remains host-side, + * tools require `HttpClient.HttpClient`, and unrepresentable operations land in `skipped`. */ export const fromSpec = (options: Options): Result => { const document = options.spec const schemes = securitySchemes(document) const defaultSecurity = securityRequirements(document.security) - const definitions = componentDefinitions(document) + const requestDefinitions = componentDefinitions(document, "request") + const responseDefinitions = hasDirectionalSchemas(document) + ? componentDefinitions(document, "response") + : requestDefinitions const paths = isRecord(document.paths) ? document.paths : {} const used = new Set() const namespaces = new Set() @@ -59,7 +61,7 @@ export const fromSpec = (options: Options): Result => { summary: nonEmptyString(operationValue.summary), description: nonEmptyString(operationValue.description), } - const output = operationOutput(document, operationValue, definitions) + const output = operationOutput(document, operationValue, responseDefinitions) if (!output.ok) { skipped.push({ method: operation.method, path, reason: output.reason }) continue @@ -104,7 +106,7 @@ export const fromSpec = (options: Options): Result => { segments, make({ description: operation.description ?? operation.summary ?? `${operation.method} ${path}`, - input: inputSchema(input.fields, definitions), + input: inputSchema(input.fields, requestDefinitions), output: output.value, run: (input) => invoke(plan, input), }), diff --git a/packages/codemode/src/openapi/runtime.ts b/packages/codemode/src/openapi/runtime.ts index 2515621792..d64d03e552 100644 --- a/packages/codemode/src/openapi/runtime.ts +++ b/packages/codemode/src/openapi/runtime.ts @@ -56,7 +56,7 @@ const buildRequest = ( input: Readonly>, ): Effect.Effect => Effect.gen(function* () { - // Validate every model-controlled value before auth resolution, which may refresh tokens. + // Validate model input before auth resolution can refresh credentials. const url = buildUrl(plan, input) if (url instanceof ToolError) return yield* Effect.fail(url) const missing = plan.fields.find( @@ -68,16 +68,17 @@ const buildRequest = ( } let request = HttpClientRequest.make(plan.operation.method as HttpMethod.HttpMethod)(url) + const query: Array = [] for (const field of plan.fields) { if (field.location !== "query") continue const item = own(input, field.inputName) if (item === undefined) continue - const serialized = serializeQuery(request, field, item) + const serialized = serializeQuery(field, item) if (serialized instanceof ToolError) return yield* Effect.fail(serialized) - request = serialized + for (const parameter of serialized) query.push(parameter) } + if (query.length > 0) request = HttpClientRequest.appendUrlParams(request, query) - // Host headers first, then declared header parameters. request = HttpClientRequest.setHeaders(request, plan.headers) for (const field of plan.fields) { if (field.location !== "header") continue @@ -169,7 +170,7 @@ const applyCredentials = ( continue } if (credential.type === "basic") { - // Buffer instead of btoa: btoa throws on non-Latin-1 credentials. + // Basic auth credentials are UTF-8; btoa rejects non-Latin-1 input. const duplicate = add( "header", "authorization", @@ -183,7 +184,6 @@ const applyCredentials = ( if (duplicate !== undefined) return duplicate continue } - // apiKey: the carrier comes from the scheme declaration. if (definition.type !== "apiKey") { return toolError( `Security scheme '${name}' is not an apiKey scheme; resolve a bearer, basic, or header credential for it.`, @@ -212,8 +212,7 @@ const buildUrl = (plan: Plan, input: Readonly>): string ), ) if (fieldValue instanceof ToolError) return fieldValue - // '.'/'..' survive encoding and URL normalization collapses them, letting a - // model-supplied value retarget the request to a different endpoint. + // URL normalization collapses encoded `.` and `..`, which could retarget the request. if (fieldValue === "" || fieldValue === "." || fieldValue === "..") { return toolError(`Invalid path parameter '${field.inputName}'.`) } @@ -249,40 +248,46 @@ const serializeSimple = ( } const serializeQuery = ( - request: HttpClientRequest.HttpClientRequest, field: Plan["fields"][number], value: unknown, -): HttpClientRequest.HttpClientRequest | ToolError => { +): ReadonlyArray | ToolError => { if (field.style === "deepObject") { if (!isRecord(value)) return toolError(`Deep-object parameter '${field.inputName}' must be an object.`) - return Object.entries(value).reduce((current, [name, item]) => { - if (current instanceof ToolError) return current + const parameters: Array = [] + for (const [name, item] of Object.entries(value)) { if (item === undefined || (item !== null && typeof item === "object")) { return toolError(`Deep-object parameter '${field.inputName}' contains an unsupported nested value.`) } - return HttpClientRequest.appendUrlParam(current, `${field.name}[${name}]`, String(item)) - }, request) + parameters.push([`${field.name}[${name}]`, String(item)]) + } + return parameters } if (Array.isArray(value)) { - const rendered = serializeSimple(field, value, String) - if (rendered instanceof ToolError) return rendered - if (!field.explode) return HttpClientRequest.appendUrlParam(request, field.name, rendered) - if (value.some((item) => item === undefined || (item !== null && typeof item === "object"))) { - return toolError(`Query parameter '${field.inputName}' contains an unsupported nested value.`) + if (!field.explode) { + const rendered = serializeSimple(field, value, String) + return rendered instanceof ToolError ? rendered : [[field.name, rendered]] } - return value.reduce((current, item) => HttpClientRequest.appendUrlParam(current, field.name, String(item)), request) + const parameters: Array = [] + for (const item of value) { + if (item !== null && typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") { + return toolError(`Parameter '${field.inputName}' contains an unsupported nested value.`) + } + parameters.push([field.name, String(item)]) + } + return parameters } if (isRecord(value) && field.explode) { - return Object.entries(value).reduce((current, [name, item]) => { - if (current instanceof ToolError) return current + const parameters: Array = [] + for (const [name, item] of Object.entries(value)) { if (item === undefined || (item !== null && typeof item === "object")) { return toolError(`Query parameter '${field.inputName}' contains an unsupported nested value.`) } - return HttpClientRequest.appendUrlParam(current, name, String(item)) - }, request) + parameters.push([name, String(item)]) + } + return parameters } const rendered = serializeSimple(field, value, String) - return rendered instanceof ToolError ? rendered : HttpClientRequest.appendUrlParam(request, field.name, rendered) + return rendered instanceof ToolError ? rendered : [[field.name, rendered]] } const readResponseBody = ( diff --git a/packages/codemode/src/openapi/spec.ts b/packages/codemode/src/openapi/spec.ts index bd4dc5aed6..c2443d0323 100644 --- a/packages/codemode/src/openapi/spec.ts +++ b/packages/codemode/src/openapi/spec.ts @@ -23,27 +23,229 @@ const asArray = (value: unknown): ReadonlyArray => (Array.isArray(value export const nonEmptyString = (value: unknown): string | undefined => typeof value === "string" && value !== "" ? value : undefined -// Guards record lookups keyed by spec- or model-controlled names against -// prototype-inherited values (e.g. a parameter named `toString`). +// Spec- and model-controlled keys must not resolve inherited properties. export const own = (record: Readonly>, key: string): T | undefined => Object.hasOwn(record, key) ? record[key] : undefined +const resolvePointer = (root: unknown, ref: string): unknown => + ref + .slice(2) + .split("/") + .map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~")) + .reduce((item, segment) => (isRecord(item) ? own(item, segment) : undefined), root) + export const resolve = (document: Document, value: unknown): unknown => { const next = (current: unknown, seen: ReadonlySet): unknown => { if (!isRecord(current)) return current - const ref = nonEmptyString(current.$ref) + const ref = nonEmptyString(own(current, "$ref")) if (ref === undefined || !ref.startsWith("#/") || seen.has(ref)) return current - const target = ref - .slice(2) - .split("/") - .map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~")) - .reduce((item, segment) => (isRecord(item) ? own(item, segment) : undefined), document) + const target = resolvePointer(document, ref) return target === undefined ? current : next(target, new Set([...seen, ref])) } return next(value, new Set()) } -const projectSchema = (document: Document, value: unknown): JsonSchema => { +// Model-facing directional projection: request schemas omit `readOnly` properties, +// response schemas omit `writeOnly` properties, and `required` stays consistent. +// Runtime values pass through unchanged. +type SchemaDirection = "request" | "response" +type SchemaResource = { readonly value: unknown; readonly root: unknown } + +const hiddenKeyword = { request: "readOnly", response: "writeOnly" } as const + +// Resolves one `$ref` hop so every link of a chain has its own sibling declarations +// inspected; cycles terminate in the callers' cycle solver. Local `$defs`/`definitions` +// pointers resolve against the schema being projected, other pointers rebase onto the target. +const resolveResource = (document: Document, resource: SchemaResource): SchemaResource => { + if (!isRecord(resource.value)) return resource + const ref = nonEmptyString(own(resource.value, "$ref")) + if (ref === undefined || !ref.startsWith("#/")) return resource + const local = ref.startsWith("#/$defs/") || ref.startsWith("#/definitions/") + const target = resolvePointer(local ? resource.root : document, ref) + if (target === undefined) return resource + return { value: target, root: local ? resource.root : target } +} + +// Hidden-ness and hidden names are memoized per schema object and direction so +// diamond-shaped reference graphs stay linear. Documents are assumed immutable once +// projected; a schema reachable under multiple resolution roots reuses the first result. +type Solver = { + readonly values: Map + // Discovery index per schema whose strongly connected component is unresolved. + readonly pending: Map + readonly stack: Array +} +type DirectionCache = { + readonly hidden: Solver + readonly names: Solver> +} + +const emptyCache = (): DirectionCache => ({ + hidden: { values: new Map(), pending: new Map(), stack: [] }, + names: { values: new Map(), pending: new Map(), stack: [] }, +}) + +const projectionCaches = new WeakMap>() + +const projectionCache = (document: Document, direction: SchemaDirection): DirectionCache => { + const existing = projectionCaches.get(document) + if (existing !== undefined) return existing[direction] + const created = { request: emptyCache(), response: emptyCache() } + projectionCaches.set(document, created) + return created[direction] +} + +// Tarjan's strongly connected components: cycle members all reach the same +// declarations, so the component root's value is final for every member. Only resolved +// components are cached, keeping results independent of traversal order. +type CycleScope = { lowlink: number } + +const solveCycles = ( + solver: Solver, + key: unknown, + provisional: T, + scope: CycleScope, + compute: (inner: CycleScope) => T, +): T => { + const cached = solver.values.get(key) + if (cached !== undefined) return cached + const pending = solver.pending.get(key) + if (pending !== undefined) { + scope.lowlink = Math.min(scope.lowlink, pending) + return provisional + } + // Components pop as contiguous stack suffixes, so pending indices stay 0..size-1. + const index = solver.pending.size + const base = solver.stack.length + solver.pending.set(key, index) + solver.stack.push(key) + const inner: CycleScope = { lowlink: Infinity } + const value = compute(inner) + if (inner.lowlink < index) { + scope.lowlink = Math.min(scope.lowlink, inner.lowlink) + return value + } + for (const member of solver.stack.splice(base)) { + solver.pending.delete(member) + solver.values.set(member, value) + } + return value +} + +// Most documents have no directional keywords; one cached scan skips projection entirely. +const directionalDocuments = new WeakMap() + +export const hasDirectionalSchemas = (document: Document): boolean => { + const cached = directionalDocuments.get(document) + if (cached !== undefined) return cached + const contains = (value: unknown): boolean => { + if (Array.isArray(value)) return value.some(contains) + if (!isRecord(value)) return false + if (own(value, "readOnly") === true || own(value, "writeOnly") === true) return true + return Object.values(value).some(contains) + } + const result = contains(document) + directionalDocuments.set(document, result) + return result +} + +// OpenAPI 3.1 allows keywords as siblings of `$ref`, so a schema's own declarations +// are inspected before following the reference. +const isHidden = ( + document: Document, + resource: SchemaResource, + direction: SchemaDirection, + scope: CycleScope = { lowlink: Infinity }, +): boolean => { + const value = resource.value + if (!isRecord(value)) return false + if (own(value, hiddenKeyword[direction]) === true) return true + return solveCycles(projectionCache(document, direction).hidden, value, false, scope, (inner) => { + const target = resolveResource(document, resource) + return ( + asArray(own(value, "allOf")).some((item) => isHidden(document, { ...resource, value: item }, direction, inner)) || + (target.value !== value && isHidden(document, target, direction, inner)) + ) + }) +} + +// Hidden property names declared by a schema itself or inherited through `$ref` and +// `allOf` composition, so sibling `required` lists stay consistent after projection. +const hiddenNames = ( + document: Document, + resource: SchemaResource, + direction: SchemaDirection, + scope: CycleScope = { lowlink: Infinity }, +): ReadonlySet => { + const value = resource.value + if (!isRecord(value)) return new Set() + return solveCycles(projectionCache(document, direction).names, value, new Set(), scope, (inner) => { + const properties = own(value, "properties") + const declared = isRecord(properties) + ? Object.entries(properties) + .filter(([, property]) => isHidden(document, { ...resource, value: property }, direction)) + .map(([name]) => name) + : [] + const composed = asArray(own(value, "allOf")).flatMap((item) => [ + ...hiddenNames(document, { ...resource, value: item }, direction, inner), + ]) + const target = resolveResource(document, resource) + const referenced = target.value === value ? [] : hiddenNames(document, target, direction, inner) + return new Set([...declared, ...composed, ...referenced]) + }) +} + +// `not`/`if`/`contains` subschemas pass through unprojected: they negate or select +// rather than assert, so removing hidden properties would invert their semantics. +const nestedSchemas = new Set([ + "items", + "additionalProperties", + "unevaluatedProperties", + "propertyNames", + "then", + "else", +]) +const nestedSchemaLists = new Set(["anyOf", "oneOf", "prefixItems"]) +const nestedSchemaMaps = new Set(["patternProperties", "dependentSchemas", "$defs", "definitions"]) + +const directionalSchema = ( + document: Document, + resource: SchemaResource, + direction: SchemaDirection, + excluded: ReadonlySet = new Set(), +): unknown => { + if (!isRecord(resource.value)) return resource.value + const hidden = new Set([...excluded, ...hiddenNames(document, resource, direction)]) + const project = (item: unknown, inherited: ReadonlySet = new Set()): unknown => + directionalSchema(document, { ...resource, value: item }, direction, inherited) + return Object.fromEntries( + Object.entries(resource.value).map(([key, item]) => { + if (key === "properties" && isRecord(item)) { + return [ + key, + Object.fromEntries( + Object.entries(item) + .filter(([name]) => !hidden.has(name)) + .map(([name, property]) => [name, project(property)]), + ), + ] + } + if (key === "required" && Array.isArray(item)) { + return [key, item.filter((name) => typeof name !== "string" || !hidden.has(name))] + } + // allOf branches share one object; hidden names apply across every branch. + if (key === "allOf" && Array.isArray(item)) return [key, item.map((entry) => project(entry, hidden))] + if (nestedSchemas.has(key)) return [key, project(item)] + if (nestedSchemaLists.has(key) && Array.isArray(item)) return [key, item.map((entry) => project(entry))] + if (nestedSchemaMaps.has(key) && isRecord(item)) { + return [key, Object.fromEntries(Object.entries(item).map(([name, entry]) => [name, project(entry)]))] + } + return [key, item] + }), + ) +} + +const normalizeSchema = (document: Document, value: unknown): JsonSchema => { if (!isRecord(value)) return {} const normalized = nonEmptyString(document.openapi)?.startsWith("3.0") ? fromSchemaOpenApi3_0(value) @@ -53,10 +255,21 @@ const projectSchema = (document: Document, value: unknown): JsonSchema => { : { ...normalized.schema, $defs: normalized.definitions } } -export const componentDefinitions = (document: Document): Readonly> => { +const projectSchema = (document: Document, value: unknown, direction: SchemaDirection): JsonSchema => + normalizeSchema( + document, + hasDirectionalSchemas(document) ? directionalSchema(document, { value, root: value }, direction) : value, + ) + +export const componentDefinitions = ( + document: Document, + direction: SchemaDirection, +): Readonly> => { const components = isRecord(document.components) ? document.components : {} const schemas = isRecord(components.schemas) ? components.schemas : {} - return Object.fromEntries(Object.entries(schemas).map(([name, value]) => [name, projectSchema(document, value)])) + return Object.fromEntries( + Object.entries(schemas).map(([name, value]) => [name, projectSchema(document, value, direction)]), + ) } const withDefinitions = (schema: JsonSchema, definitions: Readonly>): JsonSchema => { @@ -106,7 +319,7 @@ const operationParameters = ( pathItem: Record, operation: Record, ): Parsed> => { - // Operation-level parameters override path-level ones sharing (location, name). + // OpenAPI operation parameters override path parameters with the same location and name. const declared = new Map< string, { readonly name: string; readonly location: string; readonly parameter: Record } @@ -158,7 +371,7 @@ const operationParameters = ( if (style === "deepObject" && !explode) { return { ok: false, reason: `query parameter '${name}' uses deepObject with explode=false` } } - const base = projectSchema(document, resolved.schema) + const base = projectSchema(document, resolved.schema, "request") const description = nonEmptyString(resolved.description) unordered.push({ name, @@ -192,7 +405,10 @@ const operationBody = ( reason: `request body has no JSON content (declared: ${Object.keys(content).join(", ") || "none"})`, } } - const schema = resolve(document, selected.schema) + const resolvedSchema = resolve(document, selected.schema) + const schema = hasDirectionalSchemas(document) + ? directionalSchema(document, { value: resolvedSchema, root: resolvedSchema }, "request") + : resolvedSchema const required = resolved.required === true if (!isFlattenableObjectBody(schema, required)) { return { @@ -203,7 +419,7 @@ const operationBody = ( name: "body", location: "body", required, - schema: projectSchema(document, selected.schema), + schema: projectSchema(document, selected.schema, "request"), style: undefined, explode: undefined, }, @@ -218,11 +434,13 @@ const operationBody = ( return { ok: true, value: { + // Field schemas were already projected with the body as resolution root; a second + // directional pass rooted at the field would misresolve shadowed local $defs. fields: Object.entries(schema.properties).map(([name, value]) => ({ name, location: "body" as const, required: required && requiredProperties.has(name), - schema: projectSchema(document, value), + schema: normalizeSchema(document, value), style: undefined, explode: undefined, })), @@ -340,7 +558,7 @@ export const operationOutput = ( continue } if (!isRecord(value) || value.schema === undefined) return { ok: true, value: undefined } - outcomes.push(projectSchema(document, value.schema)) + outcomes.push(projectSchema(document, value.schema, "response")) } } if (outcomes.length === 0) return { ok: true, value: undefined } diff --git a/packages/codemode/src/openapi/types.ts b/packages/codemode/src/openapi/types.ts index cab772e701..252f49d86c 100644 --- a/packages/codemode/src/openapi/types.ts +++ b/packages/codemode/src/openapi/types.ts @@ -22,9 +22,8 @@ export type SecurityScheme = | { readonly type: "openIdConnect" } /** - * Credential material returned by a host auth resolver. The carrier for `apiKey` - * comes from the scheme definition, not the credential. `header` is the escape - * hatch for nonstandard schemes. + * Credential material returned by a host auth resolver. `apiKey` uses the scheme's carrier; + * `header` supports nonstandard schemes. */ export type Credential = | { readonly type: "bearer"; readonly token: string } @@ -33,9 +32,7 @@ export type Credential = | { readonly type: "header"; readonly name: string; readonly value: string } /** - * Resolves credential material for one named security scheme at call time. - * `undefined` means unavailable, try the next OR alternative; a failure aborts - * the call rather than falling through. + * Resolves credentials at call time. `undefined` tries the next OR alternative; failure aborts. */ export type AuthResolver = (context: { readonly name: string @@ -64,7 +61,7 @@ export type Skipped = { export type Tools = { [name: string]: Definition | Tools } export type Result = { - /** Tool subtree; the host places it under a key in its `tools` tree. */ + /** Namespaced tools; the host places them under a key in its `tools` object. */ readonly tools: Tools readonly skipped: ReadonlyArray } @@ -74,9 +71,7 @@ export type Parsed = { readonly ok: true; readonly value: T } | { readonly ok export type InputLocation = "path" | "query" | "header" | "body" export type InputField = { - /** Model-visible field name after cross-location collision handling. */ readonly inputName: string - /** Original parameter or body-property name used on the wire. */ readonly name: string readonly location: InputLocation readonly required: boolean @@ -92,7 +87,6 @@ export type OperationInput = { readonly body: Body | undefined } -/** One OR alternative: scheme name -> required scopes. Empty object = unauthenticated is acceptable. */ export type SecurityRequirement = Readonly>> export type Plan = { diff --git a/packages/codemode/src/stdlib/collections.ts b/packages/codemode/src/stdlib/collections.ts index f4760ff706..3851339791 100644 --- a/packages/codemode/src/stdlib/collections.ts +++ b/packages/codemode/src/stdlib/collections.ts @@ -43,9 +43,9 @@ export const setMethods = new Set(["add", "has", "delete", "clear", "forEach", " export const spreadItems = (value: unknown): Array | undefined => { if (Array.isArray(value)) return value if (typeof value === "string") return Array.from(value) - if (value instanceof SandboxMap) return Array.from(value.map.entries(), ([key, item]) => [key, item]) - if (value instanceof SandboxSet) return Array.from(value.set.values()) - if (value instanceof SandboxURLSearchParams) return Array.from(value.params.entries(), ([key, item]) => [key, item]) + if (value instanceof CodeModeMap) return Array.from(value.map.entries(), ([key, item]) => [key, item]) + if (value instanceof CodeModeSet) return Array.from(value.set.values()) + if (value instanceof CodeModeURLSearchParams) return Array.from(value.params.entries(), ([key, item]) => [key, item]) return undefined } -import { SandboxMap, SandboxSet, SandboxURLSearchParams } from "../values.js" +import { CodeModeMap, CodeModeSet, CodeModeURLSearchParams } from "../values.js" diff --git a/packages/codemode/src/stdlib/console.ts b/packages/codemode/src/stdlib/console.ts index 798563128e..1b7c3cbdb9 100644 --- a/packages/codemode/src/stdlib/console.ts +++ b/packages/codemode/src/stdlib/console.ts @@ -1,4 +1,122 @@ +import { containsOpaqueReference, containsRuntimeReference, isRuntimeReference } from "../interpreter/references.js" +import { copyIn, copyOut } from "../tool-runtime.js" +import { + isCodeModeValue, + CodeModeDate, + CodeModeMap, + CodeModePromise, + CodeModeRegExp, + CodeModeSet, + CodeModeURL, + CodeModeURLSearchParams, +} from "../values.js" +import { boundedData, coerceToString } from "./value.js" + export const consoleMethods = new Set(["log", "info", "debug", "warn", "error", "dir", "table"]) -/** Console formatting recursion ceiling; deeper values render as "...". */ -export const MAX_CONSOLE_DEPTH = 32 +const MAX_CONSOLE_DEPTH = 32 + +export const formatConsoleMessage = (name: string, args: Array): string => { + if (name === "dir") return args.length === 0 ? "undefined" : formatConsoleArgument(args[0]) + if (name === "table") return formatConsoleTable(args[0], args[1]) + const prefix = name === "warn" ? "[warn] " : name === "error" ? "[error] " : name === "debug" ? "[debug] " : "" + return `${prefix}${args.map((arg) => formatConsoleArgument(arg)).join(" ")}` +} + +const formatConsoleArgument = (value: unknown): string => { + if (value === undefined) return "undefined" + if (typeof value === "string") return value + return formatConsoleValue(value, new Set(), 0) +} + +const formatConsoleValue = (value: unknown, seen: Set, depth: number): string => { + if (value === null || value === undefined) return "null" + if (typeof value === "string") return JSON.stringify(value) + if (typeof value === "number" || typeof value === "boolean") return String(value) + if (typeof value !== "object") return String(value) + if (value instanceof CodeModePromise) return "[Promise (await it to get its value)]" + if (value instanceof CodeModeDate) return coerceToString(value) + if (value instanceof CodeModeRegExp) return coerceToString(value) + if (value instanceof CodeModeURL) return coerceToString(value) + if (value instanceof CodeModeURLSearchParams) return coerceToString(value) + if (depth > MAX_CONSOLE_DEPTH) return "..." + if (seen.has(value)) return "[Circular]" + if (value instanceof CodeModeMap) { + seen.add(value) + try { + const entries = Array.from(value.map.entries(), ([key, item]): Array => [key, item]) + return `Map(${value.map.size}) ${formatConsoleValue(entries, seen, depth + 1)}` + } finally { + seen.delete(value) + } + } + if (value instanceof CodeModeSet) { + seen.add(value) + try { + return `Set(${value.set.size}) ${formatConsoleValue(Array.from(value.set.values()), seen, depth + 1)}` + } finally { + seen.delete(value) + } + } + if (isRuntimeReference(value)) return "[CodeMode reference]" + seen.add(value) + try { + if (Array.isArray(value)) { + return `[${value.map((item) => formatConsoleValue(item, seen, depth + 1)).join(",")}]` + } + return `{${Object.entries(value) + .map(([key, item]) => `${JSON.stringify(key)}:${formatConsoleValue(item, seen, depth + 1)}`) + .join(",")}}` + } finally { + seen.delete(value) + } +} + +const formatConsoleTable = (value: unknown, columnsArgument: unknown): string => { + if (value === undefined) return "undefined" + if (containsOpaqueReference(value)) return "[CodeMode reference]" + const data = boundedData(value, "console.table argument") + const columns = consoleTableColumns(columnsArgument) + const rows = consoleTableRows(data, columns) + const keys = columns ?? Array.from(new Set(rows.flatMap((row) => Object.keys(row.values)))) + const header = ["(index)", ...keys].join("\t") + return [ + header, + ...rows.map((row) => [row.index, ...keys.map((key) => formatConsoleTableCell(row.values[key]))].join("\t")), + ].join("\n") +} + +const consoleTableColumns = (value: unknown): ReadonlyArray | undefined => { + if (value === undefined) return undefined + if (containsRuntimeReference(value)) return undefined + const columns = copyOut(copyIn(value, "console.table columns"), "nullify") + return Array.isArray(columns) ? columns.map((column) => String(column)) : undefined +} + +const consoleTableRows = ( + data: unknown, + columns: ReadonlyArray | undefined, +): Array<{ readonly index: string; readonly values: Record }> => { + if (Array.isArray(data)) { + return data.map((item, index) => ({ index: String(index), values: consoleTableValues(item, columns) })) + } + if (data !== null && typeof data === "object" && !isCodeModeValue(data)) { + return Object.entries(data).map(([index, item]) => ({ index, values: consoleTableValues(item, columns) })) + } + return [{ index: "0", values: { Value: data } }] +} + +const consoleTableValues = (value: unknown, columns: ReadonlyArray | undefined): Record => { + if (value !== null && typeof value === "object" && !Array.isArray(value) && !isCodeModeValue(value)) { + const source = value as Record + if (columns !== undefined) return Object.fromEntries(columns.map((column) => [column, source[column]])) + return Object.fromEntries(Object.entries(source)) + } + return { Value: value } +} + +const formatConsoleTableCell = (value: unknown): string => { + if (value === undefined) return "" + if (typeof value === "string") return value + return formatConsoleValue(value, new Set(), 0) +} diff --git a/packages/codemode/src/stdlib/date.ts b/packages/codemode/src/stdlib/date.ts index c492f58f94..6ca1009c89 100644 --- a/packages/codemode/src/stdlib/date.ts +++ b/packages/codemode/src/stdlib/date.ts @@ -38,7 +38,7 @@ export const invokeDateStatic = (name: string, args: Array, node: AstNo } } -export const invokeDateMethod = (value: SandboxDate, name: string, node: AstNode): unknown => { +export const invokeDateMethod = (value: CodeModeDate, name: string, node: AstNode): unknown => { const hosted = new Date(value.time) switch (name) { case "getTime": @@ -90,5 +90,5 @@ export const invokeDateMethod = (value: SandboxDate, name: string, node: AstNode } } import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" -import { SandboxDate } from "../values.js" +import { CodeModeDate } from "../values.js" import { coerceToNumber, coerceToString } from "./value.js" diff --git a/packages/codemode/src/stdlib/json.ts b/packages/codemode/src/stdlib/json.ts index 8a479d2c8c..964e6a4fb0 100644 --- a/packages/codemode/src/stdlib/json.ts +++ b/packages/codemode/src/stdlib/json.ts @@ -1,19 +1,14 @@ -import { - type AstNode, - CodeModeFunction, - InterpreterRuntimeError, - supportedSyntaxMessage, -} from "../interpreter/model.js" +import { type AstNode, InterpreterRuntimeError, supportedSyntaxMessage } from "../interpreter/model.js" +import { typeofValue } from "../interpreter/references.js" import { copyIn, copyOut } from "../tool-runtime.js" -export const jsonStatics = new Set(["stringify", "parse"]) +export const jsonStatics = new Set(["parse", "stringify"]) export const invokeJsonMethod = (name: string, args: Array, node: AstNode): unknown => { - if (!jsonStatics.has(name)) throw new InterpreterRuntimeError(`JSON.${name} is not available in CodeMode.`, node) switch (name) { case "stringify": { const replacer = args[1] - if (Array.isArray(replacer) || replacer instanceof CodeModeFunction) { + if (Array.isArray(replacer) || typeofValue(replacer) === "function") { throw new InterpreterRuntimeError( "JSON.stringify replacers are not supported in CodeMode.", node, @@ -23,11 +18,19 @@ export const invokeJsonMethod = (name: string, args: Array, node: AstNo } const space = args[2] const indent = typeof space === "number" || typeof space === "string" ? space : undefined - return JSON.stringify(copyOut(copyIn(args[0], "JSON.stringify value")), null, indent) + return JSON.stringify(copyOut(copyIn(args[0], "JSON.stringify value"), "json"), null, indent) } case "parse": { const text = args[0] if (typeof text !== "string") throw new InterpreterRuntimeError("JSON.parse expects a string.", node) + if (typeofValue(args[1]) === "function") { + throw new InterpreterRuntimeError( + "JSON.parse revivers are not supported in CodeMode.", + node, + "UnsupportedSyntax", + [supportedSyntaxMessage], + ) + } try { return copyIn(JSON.parse(text), "JSON.parse result") } catch (error) { diff --git a/packages/codemode/src/stdlib/math.ts b/packages/codemode/src/stdlib/math.ts index cc8dd0670e..54fa3be91b 100644 --- a/packages/codemode/src/stdlib/math.ts +++ b/packages/codemode/src/stdlib/math.ts @@ -1,9 +1,17 @@ export const mathConstants = new Set(["PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT2", "SQRT1_2"]) export const mathMethods = new Set([ + "random", "max", "min", "abs", + "acos", + "acosh", + "asin", + "asinh", + "atan", + "atan2", + "atanh", "floor", "ceil", "round", @@ -13,26 +21,63 @@ export const mathMethods = new Set([ "cbrt", "pow", "hypot", + "cos", + "cosh", + "sin", + "sinh", + "tan", + "tanh", "log", "log2", "log10", + "log1p", "exp", + "expm1", + "f16round", + "fround", + "clz32", + "imul", ]) export const invokeMathMethod = (name: string, args: Array, node: AstNode): number => { if (!mathMethods.has(name)) throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node) - const nums = args.map((arg) => { + if (name === "random") return Math.random() + // Validate only the arguments the method consumes; like JS, extras are ignored + // (so built-ins work as callbacks receiving (element, index, array)). + const num = (index: number): number => { + if (index >= args.length) return Number.NaN + const arg = args[index] if (typeof arg !== "number") throw new InterpreterRuntimeError(`Math.${name} expects number arguments.`, node) return arg - }) - const [a = Number.NaN, b = Number.NaN] = nums + } + const nums = () => + args.map((arg) => { + if (typeof arg !== "number") throw new InterpreterRuntimeError(`Math.${name} expects number arguments.`, node) + return arg + }) + const a = num(0) + const b = () => num(1) switch (name) { case "max": - return Math.max(...nums) + return Math.max(...nums()) case "min": - return Math.min(...nums) + return Math.min(...nums()) case "abs": return Math.abs(a) + case "acos": + return Math.acos(a) + case "acosh": + return Math.acosh(a) + case "asin": + return Math.asin(a) + case "asinh": + return Math.asinh(a) + case "atan": + return Math.atan(a) + case "atan2": + return Math.atan2(a, b()) + case "atanh": + return Math.atanh(a) case "floor": return Math.floor(a) case "ceil": @@ -48,17 +93,41 @@ export const invokeMathMethod = (name: string, args: Array, node: AstNo case "cbrt": return Math.cbrt(a) case "pow": - return Math.pow(a, b) + return Math.pow(a, b()) case "hypot": - return Math.hypot(...nums) + return Math.hypot(...nums()) + case "cos": + return Math.cos(a) + case "cosh": + return Math.cosh(a) + case "sin": + return Math.sin(a) + case "sinh": + return Math.sinh(a) + case "tan": + return Math.tan(a) + case "tanh": + return Math.tanh(a) case "log": return Math.log(a) case "log2": return Math.log2(a) case "log10": return Math.log10(a) + case "log1p": + return Math.log1p(a) case "exp": return Math.exp(a) + case "expm1": + return Math.expm1(a) + case "f16round": + return Math.f16round(a) + case "fround": + return Math.fround(a) + case "clz32": + return Math.clz32(a) + case "imul": + return Math.imul(a, b()) } throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node) } diff --git a/packages/codemode/src/stdlib/number.ts b/packages/codemode/src/stdlib/number.ts index 79710e1ad2..02dfa953b4 100644 --- a/packages/codemode/src/stdlib/number.ts +++ b/packages/codemode/src/stdlib/number.ts @@ -1,6 +1,15 @@ -export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString"]) +export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString", "valueOf"]) -export const numberConstants = new Set(["MAX_SAFE_INTEGER", "MIN_SAFE_INTEGER", "MAX_VALUE", "MIN_VALUE", "EPSILON"]) +export const numberConstants = new Set([ + "MAX_SAFE_INTEGER", + "MIN_SAFE_INTEGER", + "MAX_VALUE", + "MIN_VALUE", + "EPSILON", + "NaN", + "POSITIVE_INFINITY", + "NEGATIVE_INFINITY", +]) export const numberStatics = new Set(["isInteger", "isFinite", "isNaN", "isSafeInteger", "parseInt", "parseFloat"]) @@ -32,6 +41,9 @@ export const invokeNumberMethod = (value: number, name: string, args: Array, node: AstNode): unknown => { - if (!objectStatics.has(name)) throw new InterpreterRuntimeError(`Object.${name} is not available in CodeMode.`, node) const requireObject = (): Record => { - const value = boundedData(args[0], `Object.${name} input`) - if (isSandboxValue(value)) return {} - if (value === null || typeof value !== "object" || Array.isArray(value)) { - throw new InterpreterRuntimeError(`Object.${name} expects a data object.`, node) + const input = args[0] + if (Array.isArray(input)) return input as unknown as Record + if (isCodeModeValue(input)) return {} + if (input instanceof CodeModePromise) { + throw new InterpreterRuntimeError( + `Object.${name} received an un-awaited Promise; await it before inspecting the result.`, + node, + "InvalidDataValue", + ) } - return value as Record + if (input === null || typeof input !== "object") { + throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue") + } + const prototype = Object.getPrototypeOf(input) + if (prototype !== null && prototype !== Object.prototype) { + throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue") + } + return input as Record } const guardedSet = (out: Record, key: string, item: unknown): void => { if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, node) out[key] = item } + const addEntry = (out: Record, key: unknown, item: unknown): void => { + boundedData(key, "Object.fromEntries key") + boundedData(item, "Object.fromEntries value") + guardedSet(out, coerceToString(key), item) + } switch (name) { - case "keys": { - const value = boundedData(args[0], "Object.keys input") - if (isSandboxValue(value)) return [] - if (Array.isArray(value)) return Object.keys(value) - if (value === null || typeof value !== "object") { - throw new InterpreterRuntimeError("Object.keys expects a data object or array.", node) - } - return Object.keys(value) - } + case "keys": + return Object.keys(requireObject()) case "values": return Object.values(requireObject()) case "entries": return Object.entries(requireObject()).map(([key, item]) => [key, item]) case "hasOwn": return Object.hasOwn(requireObject(), String(args[1])) + case "is": + if (containsOpaqueReference(args[0]) || containsOpaqueReference(args[1])) { + throw new InterpreterRuntimeError("Object.is requires data values in CodeMode.", node, "InvalidDataValue") + } + return Object.is(args[0], args[1]) case "assign": { - const out: Record = Object.create(null) - for (const source of args) { - if (source === null || source === undefined) continue - const value = boundedData(source, "Object.assign input") - if (isSandboxValue(value)) continue - if (value === null || typeof value !== "object" || Array.isArray(value)) { + const target = args[0] + if (target === null || typeof target !== "object" || Array.isArray(target) || isCodeModeValue(target)) { + throw new InterpreterRuntimeError("Object.assign expects a data object target.", node) + } + const out = target as Record + for (const source of args.slice(1)) { + if (source === null || source === undefined || isCodeModeValue(source)) continue + if (typeof source !== "object" || Array.isArray(source)) { throw new InterpreterRuntimeError("Object.assign expects data objects.", node) } - for (const [key, item] of Object.entries(value)) guardedSet(out, key, item) + for (const [key, item] of Object.entries(source)) guardedSet(out, key, item) } return out } case "fromEntries": { - if (args[0] instanceof SandboxMap) { + if (args[0] instanceof CodeModeMap) { const out: Record = Object.create(null) - for (const [key, item] of args[0].map.entries()) guardedSet(out, coerceToString(key), item) + for (const [key, item] of args[0].map.entries()) addEntry(out, key, item) return out } - if (args[0] instanceof SandboxURLSearchParams) { + if (args[0] instanceof CodeModeURLSearchParams) { const out: Record = Object.create(null) for (const [key, value] of args[0].params.entries()) guardedSet(out, key, value) return out } - const pairs = boundedData(args[0], "Object.fromEntries input") + const pairs = args[0] instanceof CodeModeSet ? Array.from(args[0].set.values()) : args[0] if (!Array.isArray(pairs)) { + boundedData(args[0], "Object.fromEntries input") throw new InterpreterRuntimeError("Object.fromEntries expects an array of [key, value] pairs.", node) } const out: Record = Object.create(null) for (const pair of pairs) { - if (!Array.isArray(pair)) { - throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] pairs.", node) - } - guardedSet(out, String(pair[0]), pair[1]) + const validated = boundedData(pair, "Object.fromEntries entry") + if (validated === null || typeof validated !== "object" || isCodeModeValue(validated)) + throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] entry objects.", node) + const entry = pair as Record + addEntry(out, entry[0], entry[1]) } return out } diff --git a/packages/codemode/src/stdlib/promise.ts b/packages/codemode/src/stdlib/promise.ts index d0b442ccf7..73d02d6c0c 100644 --- a/packages/codemode/src/stdlib/promise.ts +++ b/packages/codemode/src/stdlib/promise.ts @@ -1,6 +1,3 @@ import type { PromiseMethodName } from "../interpreter/model.js" -export const promiseStatics = new Set(["all", "allSettled", "race", "resolve", "reject"]) - -/** Maximum number of eagerly forked tool calls that may run concurrently. */ -export const TOOL_CALL_CONCURRENCY = 8 +export const promiseStatics = new Set(["all", "allSettled", "race", "any", "resolve", "reject"]) diff --git a/packages/codemode/src/stdlib/regexp.ts b/packages/codemode/src/stdlib/regexp.ts index eac123bc16..84c0d0425f 100644 --- a/packages/codemode/src/stdlib/regexp.ts +++ b/packages/codemode/src/stdlib/regexp.ts @@ -19,7 +19,9 @@ export const escapeRegexHint = 'To match special characters like ( ) [ ] { } + * ? . literally, escape them with a backslash (e.g. "\\\\(") or test for them with String.includes instead.' export const toHostRegex = (arg: unknown, method: string, node: AstNode, extraFlags = ""): RegExp => { - if (arg instanceof SandboxRegExp) return arg.regex + // Native parity: an undefined pattern behaves as an empty pattern. + if (arg === undefined) return new RegExp("", extraFlags) + if (arg instanceof CodeModeRegExp) return arg.regex if (typeof arg === "string") { try { return new RegExp(arg, extraFlags) @@ -50,7 +52,7 @@ export const matchToValue = (match: RegExpMatchArray): Array => { } export const invokeRegExpMethod = ( - value: SandboxRegExp, + value: CodeModeRegExp, name: string, args: Array, node: AstNode, @@ -70,5 +72,5 @@ export const invokeRegExpMethod = ( } import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" import { isBlockedMember, type SafeObject } from "../tool-runtime.js" -import { SandboxRegExp } from "../values.js" +import { CodeModeRegExp } from "../values.js" import { coerceToString } from "./value.js" diff --git a/packages/codemode/src/stdlib/string.ts b/packages/codemode/src/stdlib/string.ts index 3ac4372e36..ffc33797dd 100644 --- a/packages/codemode/src/stdlib/string.ts +++ b/packages/codemode/src/stdlib/string.ts @@ -4,12 +4,9 @@ export const stringMethods = new Set([ "trim", "trimStart", "trimEnd", - "trimLeft", - "trimRight", "split", "slice", "substring", - "substr", "includes", "startsWith", "endsWith", diff --git a/packages/codemode/src/stdlib/url.ts b/packages/codemode/src/stdlib/url.ts index 583d776d22..39e960539f 100644 --- a/packages/codemode/src/stdlib/url.ts +++ b/packages/codemode/src/stdlib/url.ts @@ -66,7 +66,7 @@ export const invokeUriFunction = (ref: UriFunction, args: Array, node: } export const urlArgument = (value: unknown, label: string): string => - value instanceof SandboxURL ? value.url.href : uriArgument(value, label) + value instanceof CodeModeURL ? value.url.href : uriArgument(value, label) export const invokeURLStatic = (name: string, args: Array, node: AstNode): unknown => { if (!urlStatics.has(name)) throw new InterpreterRuntimeError(`URL.${name} is not available in CodeMode.`, node) @@ -75,16 +75,16 @@ export const invokeURLStatic = (name: string, args: Array, node: AstNod const base = args[1] === undefined ? undefined : urlArgument(args[1], `URL.${name} base`) try { const url = new URL(input, base) - return name === "canParse" ? true : new SandboxURL(url) + return name === "canParse" ? true : new CodeModeURL(url) } catch { return name === "canParse" ? false : null } } -export const invokeURLMethod = (value: SandboxURL, name: string, node: AstNode): string => { +export const invokeURLMethod = (value: CodeModeURL, name: string, node: AstNode): string => { if (name === "toString" || name === "toJSON") return value.url.href throw new InterpreterRuntimeError(`URL method '${name}' is not available in CodeMode.`, node) } import { type AstNode, InterpreterRuntimeError, UriFunction } from "../interpreter/model.js" -import { SandboxURL } from "../values.js" +import { CodeModeURL } from "../values.js" import { boundedData, coerceToString } from "./value.js" diff --git a/packages/codemode/src/stdlib/value.ts b/packages/codemode/src/stdlib/value.ts index ab40dc07dd..68817ea7d7 100644 --- a/packages/codemode/src/stdlib/value.ts +++ b/packages/codemode/src/stdlib/value.ts @@ -6,6 +6,7 @@ export const errorConstructors = new Set([ "ReferenceError", "EvalError", "URIError", + "AggregateError", ]) export const valueConstructors = new Set(["Date", "RegExp", "Map", "Set", "URL", "URLSearchParams"]) @@ -20,6 +21,9 @@ export const createErrorValue = (name: string, message: string): SafeObject => { return value } +export const createAggregateErrorValue = (errors: Array, message: string): SafeObject => + Object.assign(createErrorValue("AggregateError", message), { errors }) + export const errorBrandName = (value: unknown): string | undefined => value !== null && typeof value === "object" ? ((value as Record)[ErrorBrand] as string | undefined) @@ -30,13 +34,22 @@ export const boundedData = (value: unknown, label: string): unknown => copyIn(va export const coerceToString = (value: unknown): string => { if (value === null) return "null" if (value === undefined) return "undefined" - if (value instanceof SandboxDate) + if (value instanceof CodeModeDate) return Number.isFinite(value.time) ? new Date(value.time).toISOString() : "Invalid Date" - if (value instanceof SandboxRegExp) return `/${value.regex.source}/${value.regex.flags}` - if (value instanceof SandboxMap) return "[object Map]" - if (value instanceof SandboxSet) return "[object Set]" - if (value instanceof SandboxURL) return value.url.href - if (value instanceof SandboxURLSearchParams) return value.params.toString() + if (value instanceof CodeModeRegExp) return `/${value.regex.source}/${value.regex.flags}` + if (value instanceof CodeModeMap) return "[object Map]" + if (value instanceof CodeModeSet) return "[object Set]" + if (value instanceof CodeModeURL) return value.url.href + if (value instanceof CodeModeURLSearchParams) return value.params.toString() + if (errorBrandName(value) !== undefined) { + // Match Error.prototype.toString: "name: message", or just one when the other is empty. + const error = value as { name?: unknown; message?: unknown } + const name = typeof error.name === "string" ? error.name : "Error" + const message = typeof error.message === "string" ? error.message : "" + if (message === "") return name + if (name === "") return message + return `${name}: ${message}` + } if (typeof value === "object") { return Array.isArray(value) ? value.map((item) => (item === null || item === undefined ? "" : coerceToString(item))).join(",") @@ -46,23 +59,38 @@ export const coerceToString = (value: unknown): string => { } export const coerceToNumber = (value: unknown): number => { - if (value instanceof SandboxDate) return value.time - if (isSandboxValue(value)) return Number.NaN - return value !== null && typeof value === "object" && !Array.isArray(value) ? Number.NaN : Number(value) + if (value instanceof CodeModeDate) return value.time + if (isCodeModeValue(value)) return Number.NaN + // Arrays coerce through our own string coercion: host Number(array) joins with host + // ToPrimitive, which throws on the null-prototype objects the interpreter produces. + if (Array.isArray(value)) return Number(coerceToString(value)) + return value !== null && typeof value === "object" ? Number.NaN : Number(value) } export const invokeCoercion = (ref: CoercionFunction, args: Array, node: AstNode): unknown => { + // Native: Number() is 0 and String() is "", unlike their undefined-argument forms; the + // other coercers match native through the undefined-argument path below. + if (args.length === 0) { + if (ref.name === "Number") return 0 + if (ref.name === "String") return "" + } const raw = args[0] - if (isSandboxValue(raw)) { + // Error values are plain SafeObjects; the boundedData path below would strip their brand. + if (ref.name === "String" && errorBrandName(raw) !== undefined) return coerceToString(raw) + if (isCodeModeValue(raw)) { if (ref.name === "Boolean") return true if (ref.name === "Number") return coerceToNumber(raw) if (ref.name === "String") return coerceToString(raw) + if (ref.name === "isFinite") return Number.isFinite(coerceToNumber(raw)) + if (ref.name === "isNaN") return Number.isNaN(coerceToNumber(raw)) if (ref.name === "parseInt") return parseInt(coerceToString(raw)) return parseFloat(coerceToString(raw)) } - const value = boundedData(args[0], `${ref.name} input`) + const value = boundedData(raw, `${ref.name} input`) if (ref.name === "Number") return coerceToNumber(value) if (ref.name === "Boolean") return Boolean(value) + if (ref.name === "isFinite") return Number.isFinite(coerceToNumber(value)) + if (ref.name === "isNaN") return Number.isNaN(coerceToNumber(value)) if (ref.name === "parseInt") { const radix = args[1] if (radix !== undefined && typeof radix !== "number") { @@ -76,11 +104,11 @@ export const invokeCoercion = (ref: CoercionFunction, args: Array, node import { type AstNode, CoercionFunction, InterpreterRuntimeError } from "../interpreter/model.js" import { copyIn, type SafeObject } from "../tool-runtime.js" import { - isSandboxValue, - SandboxDate, - SandboxMap, - SandboxRegExp, - SandboxSet, - SandboxURL, - SandboxURLSearchParams, + isCodeModeValue, + CodeModeDate, + CodeModeMap, + CodeModeRegExp, + CodeModeSet, + CodeModeURL, + CodeModeURLSearchParams, } from "../values.js" diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index f4ccc61d4c..a518f37cc7 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -9,71 +9,58 @@ import { outputTypeScript, } from "./tool-schema.js" import { isDefinition as isToolDefinition, type Definition } from "./tool.js" +import type { Tools } from "./tools.js" import { - SandboxDate, - SandboxMap, - SandboxPromise, - SandboxRegExp, - SandboxSet, - SandboxURL, - SandboxURLSearchParams, + CodeModeDate, + CodeModeMap, + CodeModePromise, + CodeModeRegExp, + CodeModeSet, + CodeModeURL, + CodeModeURLSearchParams, } from "./values.js" const estimateTokens = (input: string) => Math.max(0, Math.round(input.length / 4)) -export type HostTool = (...args: Array) => Effect.Effect +export type Services = ServicesOf -export type HostTools = { - [name: string]: HostTool | Definition | HostTools -} - -export type Services = ServicesOf - -type ServicesOf> = Depth["length"] extends 8 +type ServicesOf> = Depth["length"] extends 8 ? never - : Tools extends (...args: Array) => Effect.Effect + : T extends { + readonly _tag: "CodeModeTool" + readonly run: (input: unknown) => Effect.Effect + } ? R - : Tools extends { - readonly _tag: "CodeModeTool" - readonly run: (input: unknown) => Effect.Effect - } - ? R - : Tools extends object - ? string extends keyof Tools - ? ServicesOf - : ServicesOf - : never + : T extends object + ? string extends keyof T + ? ServicesOf + : ServicesOf + : never -/** Minimal audit record retained for each admitted tool call. */ export type ToolCall = { readonly name: string } -/** Decoded tool call observed immediately before tool execution. */ export type ToolCallStarted = { readonly index: number readonly name: string readonly input: unknown } -/** Completed tool call observed immediately after tool execution settles. */ export type ToolCallEnded = { readonly index: number readonly name: string readonly input: unknown readonly durationMs: number readonly outcome: "success" | "failure" - /** Model-safe failure message; present only when `outcome` is `"failure"`. */ readonly message?: string } -/** Non-throwing observation hooks fired around each admitted tool call. */ export type ToolCallHooks = { readonly onToolCallStart?: ((call: ToolCallStarted) => Effect.Effect) | undefined readonly onToolCallEnd?: ((call: ToolCallEnded) => Effect.Effect) | undefined } -/** Model-visible description of one schema-backed tool. */ export type ToolDescription = { readonly path: string readonly description: string @@ -82,7 +69,6 @@ export type ToolDescription = { export type SafeObject = Record -const reservedNamespace = "$codemode" const defaultCatalogBudget = 2_000 const defaultSearchLimit = 10 const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0)) @@ -114,11 +100,6 @@ export class ToolReference { constructor(readonly path: ReadonlyArray) {} } -/** - * Maximum nesting depth for values crossing a data boundary. Fixed (not a configurable - * limit) purely because it produces a clearer diagnostic than a native stack-overflow - * RangeError would. - */ const MAX_VALUE_DEPTH = 32 export class ToolRuntimeError extends Error { @@ -137,8 +118,7 @@ export class ToolRuntimeError extends Error { } } -const isDefinition = (value: HostTool | Definition | HostTools): value is Definition => - isToolDefinition(value) +const isDefinition = (value: Definition | Tools): value is Definition => isToolDefinition(value) const runHost = (effect: Effect.Effect): Effect.Effect => effect.pipe( @@ -153,30 +133,16 @@ const blockedMemberNames = new Set(["__proto__", "constructor", "prototype"]) export const isBlockedMember = (name: string): boolean => blockedMemberNames.has(name) -/** - * Validates and copies a value against the plain-data contract (depth, circularity, plain - * objects only, blocked properties, data-only leaves). - * - * Two modes share the walk: - * - **Boundary** (`preserveSandboxValues` false, the default): the host<->sandbox boundary - - * final results, tool-call arguments, `JSON.stringify`. Sandbox value types serialize - * exactly as JSON.stringify would: Date/URL -> strings, the remaining value types -> {}. - * - **Intra-sandbox checkpoint** (`preserveSandboxValues` true; see `boundedData` in - * codemode.ts): standard-library value instances pass through untouched (treated as leaves, - * contents not walked), so values flowing through `Object.*` helpers, coercion inputs, and - * other in-sandbox checkpoints stay fully usable (`.getTime()`, `.has()`, ...). - * - * Both modes reject un-awaited promises with an await-hinting diagnostic. - */ -export const copyIn = (value: unknown, label: string, preserveSandboxValues = false): unknown => - copyBounded(value, label, 0, new Set(), preserveSandboxValues) +// Checkpoint mode preserves CodeMode values; boundary mode JSON-normalizes them. +export const copyIn = (value: unknown, label: string, preserveCodeModeValues = false): unknown => + copyBounded(value, label, 0, new Set(), preserveCodeModeValues) const copyBounded = ( value: unknown, label: string, depth: number, seen: Set, - preserveSandboxValues: boolean, + preserveCodeModeValues: boolean, ): unknown => { if (depth > MAX_VALUE_DEPTH) { throw new ToolRuntimeError("InvalidDataValue", `${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`) @@ -186,10 +152,6 @@ const copyBounded = ( value === undefined || typeof value === "string" || typeof value === "boolean" || - // NaN/Infinity are allowed to exist as in-sandbox intermediates (matching real JS and a real - // engine) so defensive guards like `Number.isNaN(x)` / `parseInt(x) || 0` can run. They are - // normalized to `null` when the value leaves the sandbox - see copyOut - exactly as - // JSON.stringify already does at any tool boundary. typeof value === "number" ) { return value @@ -199,65 +161,55 @@ const copyBounded = ( throw new ToolRuntimeError("InvalidDataValue", `${label} must contain data only.`) } - // An un-awaited promise never crosses a data checkpoint as `{}`; the diagnostic tells the - // model exactly how to fix the program instead. - if (value instanceof SandboxPromise) { + if (value instanceof CodeModePromise) { throw new ToolRuntimeError( "InvalidDataValue", `${label} contains an un-awaited Promise; await tool calls (e.g. \`const result = await tools.ns.tool(...)\`) before using their results.`, ) } - if (preserveSandboxValues) { - // Intra-sandbox checkpoints keep sandbox value instances alive as leaves; their contents - // are never walked here (Map/Set members are validated where mutation happens, and the - // real boundary still serializes them below). + if (preserveCodeModeValues) { if ( - value instanceof SandboxDate || - value instanceof SandboxRegExp || - value instanceof SandboxMap || - value instanceof SandboxSet || - value instanceof SandboxURL || - value instanceof SandboxURLSearchParams + value instanceof CodeModeDate || + value instanceof CodeModeRegExp || + value instanceof CodeModeMap || + value instanceof CodeModeSet || + value instanceof CodeModeURL || + value instanceof CodeModeURLSearchParams ) { return value } - // Host instances cannot normally reach an intra-sandbox checkpoint (tool results cross - // the boundary first), but wrap them defensively rather than degrading to JSON forms. - if (value instanceof Date) return new SandboxDate(value.getTime()) - if (value instanceof RegExp) return new SandboxRegExp(value.source, value.flags) + if (value instanceof Date) return new CodeModeDate(value.getTime()) + if (value instanceof RegExp) return new CodeModeRegExp(value.source, value.flags) if (value instanceof Map) { - const wrapped = new SandboxMap() + const wrapped = new CodeModeMap() for (const [key, item] of value.entries()) { wrapped.map.set(copyBounded(key, label, depth + 1, seen, true), copyBounded(item, label, depth + 1, seen, true)) } return wrapped } if (value instanceof Set) { - const wrapped = new SandboxSet() + const wrapped = new CodeModeSet() for (const item of value.values()) wrapped.set.add(copyBounded(item, label, depth + 1, seen, true)) return wrapped } - if (value instanceof URL) return new SandboxURL(new URL(value.href)) - if (value instanceof URLSearchParams) return new SandboxURLSearchParams(new URLSearchParams(value)) + if (value instanceof URL) return new CodeModeURL(new URL(value.href)) + if (value instanceof URLSearchParams) return new CodeModeURLSearchParams(new URLSearchParams(value)) } - // Sandbox value types (and their host counterparts, which a host tool may legitimately - // return) serialize exactly as JSON.stringify would at the data boundary: Date/URL use - // toJSON(), while RegExp/Map/Set/URLSearchParams have no JSON form beyond {}. - if (value instanceof SandboxDate) { + if (value instanceof CodeModeDate) { return Number.isFinite(value.time) ? new Date(value.time).toISOString() : null } if (value instanceof Date) { return Number.isFinite(value.getTime()) ? value.toISOString() : null } - if (value instanceof SandboxURL) return value.url.href + if (value instanceof CodeModeURL) return value.url.href if (value instanceof URL) return value.href if ( - value instanceof SandboxRegExp || - value instanceof SandboxMap || - value instanceof SandboxSet || - value instanceof SandboxURLSearchParams || + value instanceof CodeModeRegExp || + value instanceof CodeModeMap || + value instanceof CodeModeSet || + value instanceof CodeModeURLSearchParams || value instanceof RegExp || value instanceof Map || value instanceof Set || @@ -273,7 +225,17 @@ const copyBounded = ( seen.add(value) if (Array.isArray(value)) { - const copied = value.map((item) => copyBounded(item, label, depth + 1, seen, preserveSandboxValues)) + const copied = value.map((item) => copyBounded(item, label, depth + 1, seen, preserveCodeModeValues)) + if (preserveCodeModeValues) { + // Checkpoint copies retain array metadata that boundary copies omit. + for (const [key, item] of Object.entries(value)) { + if (Object.hasOwn(copied, key)) continue + if (isBlockedMember(key)) { + throw new ToolRuntimeError("InvalidDataValue", `${label} contains blocked property '${key}'.`) + } + Reflect.set(copied, key, copyBounded(item, label, depth + 1, seen, true)) + } + } seen.delete(value) return copied } @@ -288,60 +250,91 @@ const copyBounded = ( if (isBlockedMember(key)) { throw new ToolRuntimeError("InvalidDataValue", `${label} contains blocked property '${key}'.`) } - copied[key] = copyBounded(item, label, depth + 1, seen, preserveSandboxValues) + copied[key] = copyBounded(item, label, depth + 1, seen, preserveCodeModeValues) } seen.delete(value) return copied } -export const copyOut = (value: unknown, undefinedAsNull = false): unknown => { - if (value === undefined && undefinedAsNull) return null - // Normalize non-finite numbers to null as the value crosses out of the sandbox (final return - // and tool-call arguments both funnel through here), matching JSON semantics - NaN/Infinity - // have no JSON representation, so JSON.stringify would produce null anyway. +// "json" mirrors JSON.stringify (undefined object values drop, undefined array elements become +// null, a bare undefined passes through): use it wherever data leaves as JSON, like tool +// arguments and stringify-style formatting. "nullify" turns every undefined, including a bare +// one, into null: use it for program results, where the consumer must never see undefined. +export type CopyOutMode = "json" | "nullify" + +export const copyOut = (value: unknown, mode: CopyOutMode): unknown => { + if (value === undefined && mode === "nullify") return null if (typeof value === "number" && !Number.isFinite(value)) { return null } if (Array.isArray(value)) { - return value.map((item) => copyOut(item, undefinedAsNull)) + // Array.from densifies holes so sparse arrays normalize at the boundary like JSON does. + return Array.from(value, (item) => { + const copied = copyOut(item, mode) + return copied === undefined && mode === "json" ? null : copied + }) } if (value !== null && typeof value === "object" && !(value instanceof ToolReference)) { - return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, copyOut(item, undefinedAsNull)])) + return Object.fromEntries( + Object.entries(value) + .map(([key, item]) => [key, copyOut(item, mode)] as const) + .filter(([, item]) => !(item === undefined && mode === "json")), + ) } return value } -const definitions = ( - tools: HostTools, - path: ReadonlyArray = [], -): Array<{ path: string; definition: Definition }> => { - const entries: Array<{ path: string; definition: Definition }> = [] - for (const [name, value] of Object.entries(tools)) { - const next = [...path, name] - if (isDefinition(value)) entries.push({ path: next.join("."), definition: value }) - else if (typeof value !== "function") entries.push(...definitions(value, next)) - } - return entries +// Dots in tool names are namespace separators; the last definition for a canonical path wins. +type ToolNode = { + definition?: Definition + readonly children: Map> } +const toolTrie = (tools: Tools): ToolNode => { + const root: ToolNode = { children: new Map() } + const insert = (node: ToolNode, group: Tools): void => { + for (const [name, value] of Object.entries(group)) { + let current = node + for (const segment of name.split(".")) { + if (segment === "") throw new TypeError(`Tool name '${name}' contains an empty segment.`) + const child = current.children.get(segment) ?? { children: new Map() } + current.children.set(segment, child) + current = child + } + if (isDefinition(value)) current.definition = value + else insert(current, value) + } + } + insert(root, tools) + return root +} + +const canonicalSegments = (path: ReadonlyArray): ReadonlyArray => + path.flatMap((segment) => segment.split(".")) + +const definitions = ( + node: ToolNode, + path: ReadonlyArray = [], +): Array<{ path: string; definition: Definition }> => [ + ...(node.definition === undefined ? [] : [{ path: path.join("."), definition: node.definition }]), + ...Array.from(node.children, ([name, child]) => definitions(child, [...path, name])).flat(), +] + const describeDefinition = (path: string, definition: Definition): ToolDescription => ({ path, description: definition.description, signature: `${toolExpression(path)}(input: ${inputTypeScript(definition, true)}): Promise<${outputTypeScript(definition, true)}>`, }) -const visibleDefinitions = (tools: HostTools) => - definitions(tools).map(({ path, definition }) => ({ +const visibleDefinitions = (tools: Tools) => + definitions(toolTrie(tools)).map(({ path, definition }) => ({ path, definition, description: describeDefinition(path, definition), })) -export const catalog = (tools: HostTools): ReadonlyArray => - visibleDefinitions(tools).map(({ description }) => description) - export type DiscoveryPlan = { readonly catalog: ReadonlyArray readonly instructions: string @@ -350,18 +343,10 @@ export type DiscoveryPlan = { export type SearchEntry = { readonly description: ToolDescription - /** Top-level namespace (first path segment), matched by the search `namespace` option. */ readonly namespace: string - /** Lowercased path + description + input property names/descriptions, for substring matching. */ readonly searchText: string } -/** - * Split a query into lowercased search terms. camelCase boundaries are split - * (`resolveLibrary` -> `resolve library`) and every non-alphanumeric character is a - * separator, so `resolve-library-id`, `resolveLibraryId`, and `resolve library id` all - * tokenize alike. Empties and the `*` wildcard are dropped. - */ const tokenize = (query: string): Array => query .replace(/([a-z0-9])([A-Z])/g, "$1 $2") @@ -369,13 +354,6 @@ const tokenize = (query: string): Array => .split(/[^a-z0-9]+/) .filter((term) => term.length > 0 && term !== "*") -/** - * A term plus its naive singular variants (trailing "s"/"es" stripped), so a plural - * query term ("issues") still matches indexed text that only carries the singular - * ("issue"). Matching is one-directional substring containment, so the variants are - * needed only on the query side; scoring weights are unchanged - each field check - * passes when ANY form matches. - */ const termForms = (term: string): Array => { const forms = [term] if (term.endsWith("es") && term.length > 3) forms.push(term.slice(0, -2)) @@ -397,8 +375,6 @@ const makeSearchTool = (searchIndex: ReadonlyArray): Definition => request.namespace === undefined ? searchIndex : searchIndex.filter((entry) => entry.namespace === request.namespace) - // A query that names one tool path exactly (canonical path or rendered JavaScript - // expression) is a lookup, not a search: return that tool alone. const trimmed = query.trim() const pathQuery = trimmed.startsWith("tools.") ? trimmed.slice("tools.".length) : trimmed const exact = @@ -408,9 +384,6 @@ const makeSearchTool = (searchIndex: ReadonlyArray): Definition => (entry) => entry.description.path === pathQuery || toolExpression(entry.description.path) === trimmed, ) const terms = tokenize(query).map(termForms) - // Additive field-weighted scoring, summed across terms: exact path or path segment - // (20) > path substring (8) > description substring (4) > any searchable text, - // including input parameter names and descriptions (2). const ranked = exact !== undefined ? [exact] @@ -448,10 +421,12 @@ const makeSearchTool = (searchIndex: ReadonlyArray): Definition => }), }) -const searchDescription = describeDefinition(`${reservedNamespace}.search`, makeSearchTool([])) +const searchSignature = (() => { + const definition = makeSearchTool([]) + return `search(input: ${inputTypeScript(definition, true)}): ${outputTypeScript(definition, true)}` +})() const catalogLine = (tool: ToolDescription) => { - // Keep the tool description concise; the full schema documentation remains in the signature. const line = tool.description.split("\n", 1)[0]!.trim() const description = line.length > 120 ? line.slice(0, 119) + "..." : line return description === "" ? ` - ${tool.signature}` : ` - ${tool.signature} // ${description}` @@ -471,28 +446,11 @@ const toSearchEntry = (path: string, definition: Definition, description: .toLowerCase(), }) -/** The runtime search index over every described tool. Search is always registered. */ -export const searchIndex = (tools: HostTools): ReadonlyArray => +export const searchIndex = (tools: Tools): ReadonlyArray => visibleDefinitions(tools).map(({ path, definition, description }) => toSearchEntry(path, definition, description)) -export const assertValidTools = (tools: HostTools): void => { - if (Object.hasOwn(tools, reservedNamespace)) { - throw new Error(`Tool namespace '${reservedNamespace}' is reserved for CodeMode discovery tools.`) - } -} - -/** - * Budgeted catalog: every namespace is always listed with its tool count; full call - * signatures are inlined against the `catalogBudget` (estimated tokens, - * chars/4) round-robin across namespaces - in each round (namespaces alphabetical), every - * namespace still holding un-inlined tools attempts to place its next-cheapest line, and - * a namespace whose next line does not fit is done while the others keep going - so every - * namespace gets some representation before any namespace gets everything. The section - * states exactly how comprehensive it is - overall (COMPLETE vs PARTIAL) and per - * namespace. Namespace stub lines are never budgeted: every namespace appears with its - * tool count even at budget 0. - */ -export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBudget): DiscoveryPlan => { +// Budget signatures round-robin so every namespace remains visible. +export const prepare = (tools: Tools, catalogBudget = defaultCatalogBudget): DiscoveryPlan => { if (!Number.isSafeInteger(catalogBudget) || catalogBudget < 0) { throw new RangeError("discovery.catalogBudget must be a non-negative safe integer") } @@ -508,12 +466,6 @@ export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBu } const ordered = [...namespaces].sort(([left], [right]) => left.localeCompare(right)) - // Select which signatures fit the budget before emitting, so the list can state - // exactly how comprehensive it is. Round-robin fairness: in each round (namespaces - // alphabetical), every namespace still holding un-inlined tools tries to place its - // next-cheapest line against the shared budget; a namespace whose next line does not - // fit is done - the others keep going - so every namespace gets some representation - // before any namespace gets everything. const selections = ordered.map(([namespace, group]) => ({ namespace, picked: new Set(), @@ -545,23 +497,17 @@ export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBu const empty = described.length === 0 - // Section order is deliberate: workflow first (the top is the least likely part of a long - // description to be truncated or skimmed away), then rules, then syntax, with the budgeted - // catalog at the bottom. Example call forms use placeholders - never a real or fabricated - // tool name - and show both dot and bracket notation so non-identifier names are not normalized. const intro = [ empty ? "This is a restricted JavaScript language for calling tools, not a general-purpose runtime." : complete - ? "This is a restricted JavaScript language for calling tools, not a general-purpose runtime. Inside the confined interpreter, `tools` contains the Code Mode tools listed below and internal runtime tools; surrounding agent tools are not available." - : "This is a restricted JavaScript language for calling tools, not a general-purpose runtime. Inside the confined interpreter, `tools` contains the Code Mode tools listed or searchable below and internal runtime tools; surrounding agent tools are not available.", + ? "This is a restricted JavaScript language for calling tools, not a general-purpose runtime. Inside the confined interpreter, `tools` contains the Code Mode tools listed below; surrounding agent tools are not available." + : "This is a restricted JavaScript language for calling tools, not a general-purpose runtime. Inside the confined interpreter, `tools` contains the Code Mode tools listed or searchable below; surrounding agent tools are not available.", ...(empty ? [] : ["Do not infer or normalize tool names; use only exact signatures shown below or returned by search."]), ] - // The search step exists only when search is advertised (PARTIAL catalog); a COMPLETE - // catalog already shows every signature, so step 1 picks from the list instead. const workflow = empty ? [] : [ @@ -575,7 +521,7 @@ export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBu "3. Return only the fields you need from structured results; narrow unknown results before reading fields, and avoid returning large raw payloads.", ] : [ - '1. If needed, discover tools: `return await tools.$codemode.search({ query: "" })`.', + '1. If needed, discover tools with the built-in search function: `return search({ query: "" })`.', "2. In the next execution, copy a returned path exactly, call it, and return only the needed fields.", ]), ] @@ -587,16 +533,17 @@ export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBu "## Rules", "", complete - ? "- Only Code Mode tools listed here and internal runtime tools are available; surrounding agent tools are not implicitly exposed." - : "- Only Code Mode tools listed here or returned by `tools.$codemode.search` and internal runtime tools are available; surrounding agent tools are not implicitly exposed.", + ? "- Only Code Mode tools listed here are available; surrounding agent tools are not implicitly exposed." + : "- Only Code Mode tools listed here or returned by the built-in `search` function are available; surrounding agent tools are not implicitly exposed.", "- Filter, aggregate, and transform collections in code - never return them raw or call a tool per item across messages.", "- A result typed `Promise` may be structured data or text. Before reading fields, check that it is a non-null object and not an array; otherwise handle the returned text or primitive directly.", '- Run independent calls in parallel: `await Promise.all(items.map((item) => tools..(item)))`, or use `tools.["tool-name"](item)` when the listed signature uses bracket notation.', + "- Execution ends when the program returns; pending promises are interrupted, so await every call whose completion matters.", "- `Object.keys(tools)` lists namespaces; `Object.keys(tools.)` lists its tools; `for...in` works on both.", ...(complete ? [] : [ - '- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "" })`.', + '- Browse one namespace: `search({ query: "", namespace: "" })`.', "- If search returns `next`, repeat the same search with `offset: next.offset`.", ]), ] @@ -606,7 +553,8 @@ export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBu "## Language", "", "Use common JavaScript data operations, functions, control flow, selected standard-library methods, and awaited tool calls. Built-ins include Date, RegExp, Map, Set, URL, URLSearchParams, and URI encoding helpers.", - "Modules/imports, classes, generators, timers, fetch, eval, prototype access, unlisted methods, and promise chaining are unavailable. Use Code Mode tools for external operations. Use await with try/catch.", + "Modules/imports, classes, generators, timers, fetch, eval, prototype access, and unlisted methods are unavailable. Use Code Mode tools for external operations. Use await with try/catch.", + "Prefer explicit `return`; otherwise only the final top-level expression becomes the result.", "Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.", ] @@ -617,14 +565,12 @@ export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBu toolSection.push( complete ? "## Available tools (COMPLETE list - every tool is shown below with its full call signature)" - : `## Available tools (PARTIAL - ${totalShown} of ${described.length} shown; find the rest with tools.$codemode.search)`, + : `## Available tools (PARTIAL - ${totalShown} of ${described.length} shown; find the rest with search(...))`, "", ) for (const [namespace, group] of ordered) { const picked = shown.get(namespace)! const count = `${group.length} tool${group.length === 1 ? "" : "s"}` - // Annotate only when a namespace is not fully shown, so a comprehensive - // namespace reads cleanly and a truncated one is unambiguous. const label = picked.size === group.length ? count @@ -635,7 +581,7 @@ export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBu for (const tool of group) if (picked.has(tool)) toolSection.push(catalogLine(tool)) } if (!complete) { - toolSection.push("", "Search returns complete callable signatures:", `- ${searchDescription.signature}`) + toolSection.push("", "Search returns complete callable signatures:", `- ${searchSignature}`) } } @@ -647,79 +593,51 @@ export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBu } } -/** - * The enumerable names at one node of the callable tool tree - namespace names at the root, - * tool/namespace names below - powering `Object.keys(tools)` and `for...in` over tool - * references. A callable tool is a leaf and enumerates as `[]` (like `Object.keys` of a - * function in JS). An unknown path is an `UnknownTool` error pointing at the working - * discovery idioms, mirroring how calling an unknown tool fails. - */ -const namespaceKeys = (tools: HostTools, path: ReadonlyArray): ReadonlyArray => { - let value: HostTool | Definition | HostTools = tools - for (const segment of path) { - if ( - isBlockedMember(segment) || - typeof value === "function" || - isDefinition(value) || - !Object.hasOwn(value, segment) - ) { - throw new ToolRuntimeError("UnknownTool", `Unknown tool namespace '${path.join(".")}'.`, [ - "Object.keys(tools) lists the available namespaces; tools.$codemode.search({ query }) finds described tools.", - ]) - } - value = value[segment] as HostTool | Definition | HostTools +const lookup = (root: ToolNode, segments: ReadonlyArray): ToolNode | undefined => + segments.reduce | undefined>((node, segment) => node?.children.get(segment), root) + +const namespaceKeys = (root: ToolNode, path: ReadonlyArray): ReadonlyArray => { + const segments = canonicalSegments(path) + const node = lookup(root, segments) + if (node === undefined) { + throw new ToolRuntimeError("UnknownTool", `Unknown tool namespace '${segments.join(".")}'.`) } - if (typeof value === "function" || isDefinition(value)) return [] - return Object.keys(value) + return Array.from(node.children.keys()) } -const resolve = (tools: HostTools, path: ReadonlyArray): HostTool | Definition => { - let value: HostTool | Definition | HostTools = tools - - for (const segment of path) { - if ( - isBlockedMember(segment) || - typeof value === "function" || - isDefinition(value) || - !Object.hasOwn(value, segment) - ) { - throw new ToolRuntimeError("UnknownTool", `Unknown tool '${path.join(".")}'.`, [ - "Use tools.$codemode.search({ query }) to find available described tools.", - ]) - } - value = value[segment] as HostTool | Definition | HostTools +const resolve = (root: ToolNode, path: ReadonlyArray): Definition => { + const segments = canonicalSegments(path) + const node = lookup(root, segments) + if (node === undefined) { + throw new ToolRuntimeError("UnknownTool", `Unknown tool '${segments.join(".")}'.`, [ + "Use search({ query }) to find available described tools.", + ]) } - - if (typeof value !== "function" && !isDefinition(value)) { - throw new ToolRuntimeError("UnknownTool", `Tool '${path.join(".")}' is not callable.`) + if (node.definition === undefined) { + throw new ToolRuntimeError("UnknownTool", `Tool '${segments.join(".")}' is not callable.`) } - - return value + return node.definition } export type ToolRuntime = { readonly root: ToolReference readonly calls: Array readonly invoke: (path: ReadonlyArray, args: Array) => Effect.Effect - /** Enumerable namespace/tool names at one node of the callable tool tree; see `namespaceKeys`. */ + readonly search: (args: Array) => Effect.Effect readonly keys: (path: ReadonlyArray) => ReadonlyArray } export const make = ( - tools: HostTools, - /** Undefined means unlimited tool calls. */ + tools: Tools, maxToolCalls: number | undefined, searchIndex: ReadonlyArray, hooks?: ToolCallHooks, ): ToolRuntime => { const calls: Array = [] - const callableTools = { - ...tools, - [reservedNamespace]: { search: makeSearchTool(searchIndex) }, - } + const root = toolTrie(tools) + const searchTool = makeSearchTool(searchIndex) - // Wraps the settling portion of a tool call so onToolCallEnd observes success and failure - // symmetrically. Interruption (e.g. the execution timeout) fires neither outcome. + // End hooks observe settled success or failure; interruption emits neither outcome. const observeEnd = (effect: Effect.Effect, call: ToolCallStarted): Effect.Effect => { const onEnd = hooks?.onToolCallEnd if (onEnd === undefined) return effect @@ -752,53 +670,53 @@ export const make = ( calls.push(call) } + const recordAndObserve = (name: string, input: unknown) => + Effect.sync(() => { + recordCall({ name }) + return calls.length - 1 + }).pipe(Effect.tap((index) => hooks?.onToolCallStart?.({ index, name, input }) ?? Effect.void)) + + const invokeDefinition = (name: string, tool: Definition, externalArgs: Array) => + Effect.gen(function* () { + if (externalArgs.length !== 1) + throw new ToolRuntimeError("InvalidToolInput", `Tool '${name}' expects exactly one input object.`) + const input = yield* Effect.try({ + try: () => decodeToolInput(tool, externalArgs[0]), + catch: (cause) => + new ToolRuntimeError("InvalidToolInput", `Invalid input for tool '${name}': ${String(cause)}`), + }) + const index = yield* recordAndObserve(name, input) + return yield* observeEnd( + Effect.gen(function* () { + const raw = yield* runHost(Effect.suspend(() => tool.run(input))) + const result = yield* Effect.try({ + try: () => decodeToolOutput(tool, raw), + catch: () => new ToolRuntimeError("InvalidToolOutput", `Invalid output from tool '${name}'.`), + }) + return yield* decodeOutput(result, name) + }), + { index, name, input }, + ) + }) + return { root: new ToolReference([]), calls, - keys: (path) => namespaceKeys(callableTools, path), + keys: (path) => namespaceKeys(root, path), + search: (args) => + Effect.suspend(() => + invokeDefinition( + "search", + searchTool, + args.map((arg) => copyOut(copyIn(arg, "Arguments for tool 'search'"), "json")), + ), + ), invoke: (path, args) => Effect.gen(function* () { - const name = path.join(".") - const externalArgs = args.map((arg) => copyOut(copyIn(arg, `Arguments for tool '${name}'`))) - const call = { name } - const recordAndObserve = (input: unknown) => - Effect.sync(() => { - recordCall(call) - return calls.length - 1 - }).pipe(Effect.tap((index) => hooks?.onToolCallStart?.({ index, name, input }) ?? Effect.void)) - const tool = resolve(callableTools, path) - let describedInput: unknown - if (isDefinition(tool)) { - if (externalArgs.length !== 1) - throw new ToolRuntimeError("InvalidToolInput", `Tool '${name}' expects exactly one input object.`) - describedInput = yield* Effect.try({ - try: () => decodeToolInput(tool, externalArgs[0]), - catch: (cause) => - new ToolRuntimeError("InvalidToolInput", `Invalid input for tool '${name}': ${String(cause)}`), - }) - } - const input = isDefinition(tool) ? describedInput : externalArgs - const index = yield* recordAndObserve(input) - const currentCall = { index, name, input } - if (isDefinition(tool)) { - return yield* observeEnd( - Effect.gen(function* () { - const raw = yield* runHost(Effect.suspend(() => tool.run(describedInput))) - const result = yield* Effect.try({ - try: () => decodeToolOutput(tool, raw), - catch: () => new ToolRuntimeError("InvalidToolOutput", `Invalid output from tool '${name}'.`), - }) - return yield* decodeOutput(result, name) - }), - currentCall, - ) - } - return yield* observeEnd( - Effect.gen(function* () { - return yield* decodeOutput(yield* runHost(Effect.suspend(() => tool(...externalArgs))), name) - }), - currentCall, - ) + const name = canonicalSegments(path).join(".") + const externalArgs = args.map((arg) => copyOut(copyIn(arg, `Arguments for tool '${name}'`), "json")) + const tool = resolve(root, path) + return yield* invokeDefinition(name, tool, externalArgs) }), } } diff --git a/packages/codemode/src/tool-schema.ts b/packages/codemode/src/tool-schema.ts index 16213fa8ee..d8b48dc548 100644 --- a/packages/codemode/src/tool-schema.ts +++ b/packages/codemode/src/tool-schema.ts @@ -5,13 +5,8 @@ const isEffectSchema = (schema: SchemaType): schema is Schema.Decoder & const renderLiteral = (value: unknown): string => JSON.stringify(value) ?? "unknown" -/** - * Bare TypeScript identifier - usable unquoted as an object key (and, in the tool runtime, - * with dot access as a tool-path segment). Anything else must be quoted/bracketed. - */ export const identifierSegment = /^[A-Za-z_$][A-Za-z0-9_$]*$/ -/** Renders a property name as a valid TS object key: bare when an identifier, quoted otherwise. */ const renderKey = (name: string): string => (identifierSegment.test(name) ? name : JSON.stringify(name)) const effectNumberSentinel = (schema: JsonSchema) => @@ -23,20 +18,14 @@ const effectNumberSentinel = (schema: JsonSchema) => const intersection = (members: ReadonlyArray): string => { const concrete = members.filter((member) => member !== "unknown") if (concrete.length === 0) return "unknown" - if (concrete.length === 1) return concrete[0] ?? "unknown" + if (concrete.length === 1) return concrete[0] return concrete.map((member) => (member.includes(" | ") ? `(${member})` : member)).join(" & ") } -/** - * Recursion ceiling for schema rendering. Object, array, and union recursion all increment - * depth, so this bounds every recursion path - pathological or structurally cyclic schemas - * degrade to `unknown` instead of overflowing the stack (rendering must never throw). - */ const MAX_RENDER_DEPTH = 8 type RenderContext = { readonly definitions: Readonly> - /** Indented, JSDoc-annotated multiline rendering (search results); compact single line otherwise. */ readonly pretty: boolean } @@ -64,10 +53,6 @@ const hasUnresolvedRef = ( ].some((item) => hasUnresolvedRef(item, definitions, seen, nextVisited)) } -/** - * Schema constraints a TypeScript type cannot express natively but a model benefits from, - * surfaced as JSDoc tags (`@deprecated`, `@default`, `@format`, `@minItems`, `@maxItems`). - */ const docTags = (schema: JsonSchema): Array => { const tags: Array = [] if (schema.deprecated === true) tags.push("@deprecated") @@ -75,9 +60,7 @@ const docTags = (schema: JsonSchema): Array => { try { const rendered = JSON.stringify(schema.default) if (rendered !== undefined) tags.push(`@default ${rendered}`) - } catch { - // unserializable default: skip rather than emit a broken tag - } + } catch {} } if (typeof schema.format === "string") tags.push(`@format ${schema.format}`) if (typeof schema.minItems === "number") tags.push(`@minItems ${schema.minItems}`) @@ -85,13 +68,7 @@ const docTags = (schema: JsonSchema): Array => { return tags } -/** - * Format a schema `description` plus `tags` as a JSDoc comment at the given indent, - * preserving multi-line text (a single line stays `/** ... *\/`; multiple lines become a - * `*`-prefixed block). `*\/` is neutralized so nothing can close the comment early, and - * blank leading/trailing lines are trimmed. Returns "" (else a trailing newline) so - * callers can prepend it directly to the field line. - */ +// Neutralize `*\/` so model-provided schema text cannot terminate generated documentation. const jsdoc = (description: string | undefined, tags: ReadonlyArray, pad: string): string => { const lines = [...(description === undefined ? [] : description.split("\n")), ...tags].map((line) => line.replaceAll("*/", "* /").replace(/\s+$/, ""), @@ -128,17 +105,11 @@ const renderSchema = ( if (schema.enum) return schema.enum.map(renderLiteral).join(" | ") const alternatives = schema.anyOf ?? schema.oneOf if (alternatives) { - // Effect's number schema emits `anyOf: [{ type: "number" }, { const: "NaN" }, - // { const: "Infinity" }, { const: "-Infinity" }]`. Collapse only that artifact; - // real JSON Schema unions such as `string | number` or `number | null` must keep - // every branch. if ( alternatives.some((item) => item.type === "number") && alternatives.every((item) => item.type === "number" || effectNumberSentinel(item)) ) return "number" - // An empty Schema.Struct({}) emits `anyOf: [{ type: "object" }, { type: "array" }]` - // (no properties/items); render the bare shape as {} instead of `{} | Array`. if ( alternatives.length === 2 && alternatives[0]?.type === "object" && @@ -183,7 +154,6 @@ const renderSchema = ( return fields.length === 0 ? "{}" : `{ ${fields.join("; ")} }` } - // Pretty: an indented block, each described field preceded by its JSDoc comment. if (properties.length === 0 && indexType === undefined) return "{}" const pad = " ".repeat(depth + 1) const lines = properties.map( @@ -208,7 +178,6 @@ export const toTypeScript = (schema: Schema.Top, decoded = false, pretty = false } } -/** Renders a raw JSON Schema document as a TypeScript type string. */ export const jsonSchemaToTypeScript = (schema: JsonSchema, pretty = false): string => { try { return renderSchema(schema, { definitions: { ...(schema.definitions ?? {}), ...(schema.$defs ?? {}) }, pretty }) @@ -217,20 +186,12 @@ export const jsonSchemaToTypeScript = (schema: JsonSchema, pretty = false): stri } } -/** One input property of a tool, extracted best-effort from its input schema. */ export type InputProperty = { readonly name: string readonly description: string | undefined readonly required: boolean } -/** - * The property names, descriptions, and required flags of a tool's input schema - the raw - * material for search text. Best-effort: Effect Schemas go through their - * JSON Schema document (the same emission signature rendering uses); JSON Schemas are read - * directly, resolving a trivial top-level `$ref` into `$defs`/`definitions` when present. - * Anything unresolvable yields `[]` (search falls back to path + description). - */ export const inputProperties = (definition: Definition): Array => { try { const document = isEffectSchema(definition.input) @@ -262,20 +223,11 @@ export const inputProperties = (definition: Definition): Array(definition: Definition, pretty = false): string => isEffectSchema(definition.input) ? toTypeScript(definition.input, false, pretty) : jsonSchemaToTypeScript(definition.input, pretty) -/** - * The model-visible TypeScript type of a tool's result; tools without an output schema - * return `unknown`. `pretty` renders the JSDoc-annotated multiline form, as for inputs. - */ export const outputTypeScript = (definition: Definition, pretty = false): string => definition.output === undefined ? "unknown" @@ -283,18 +235,9 @@ export const outputTypeScript = (definition: Definition, pretty = false): ? toTypeScript(definition.output, true, pretty) : jsonSchemaToTypeScript(definition.output, pretty) -/** - * Decodes tool input before `run` is invoked. Effect Schemas validate (throwing on failure); - * JSON-Schema-described inputs pass through unvalidated (render-only). - */ export const decodeInput = (definition: Definition, value: unknown): unknown => isEffectSchema(definition.input) ? Schema.decodeUnknownSync(definition.input)(value) : value -/** - * Decodes a tool result before it is exposed to the program. Effect Schemas validate and - * transform (throwing on failure); JSON Schema outputs and tools without an output schema pass - * the host value through unchanged. - */ export const decodeOutput = (definition: Definition, value: unknown): unknown => definition.output !== undefined && isEffectSchema(definition.output) ? Schema.decodeUnknownSync(definition.output)(value) diff --git a/packages/codemode/src/tool.ts b/packages/codemode/src/tool.ts index 6c6863f99d..e75fa7ba26 100644 --- a/packages/codemode/src/tool.ts +++ b/packages/codemode/src/tool.ts @@ -1,11 +1,8 @@ import { Effect, Schema } from "effect" /** - * JSON Schema subset accepted for render-only tool schemas. - * - * A JSON-Schema-described side of a tool is used to generate the model-visible TypeScript - * signature only - CodeMode performs no validation against it. This is the natural shape for - * adapter-provided tools (e.g. MCP definitions) whose schemas arrive as JSON Schema documents. + * JSON Schema subset for model-visible signatures. CodeMode does not validate values against + * these schemas. */ export type JsonSchema = { readonly type?: string | ReadonlyArray @@ -32,7 +29,7 @@ export type JsonSchema = { /** Either a validating Effect Schema or a render-only JSON Schema document. */ export type SchemaType = Schema.Decoder | JsonSchema -/** Schema-backed tool definition consumed by a CodeMode tool tree. */ +/** Schema-backed tool definition exposed through CodeMode's `tools` object. */ export type Definition = { readonly _tag: "CodeModeTool" readonly description: string @@ -41,10 +38,8 @@ export type Definition = { readonly run: (input: unknown) => Effect.Effect } -/** The value `run` receives: the decoded type for Effect Schemas, `unknown` for JSON Schemas. */ type InputType = S extends Schema.Decoder ? S["Type"] : unknown -/** The value `run` returns: the encoded type for Effect Schemas, `unknown` otherwise. */ type ResultType = S extends Schema.Decoder ? S["Encoded"] : unknown /** Options for defining one CodeMode tool. */ @@ -55,35 +50,20 @@ export type Options) => Effect.Effect, unknown, R> } +// Object.hasOwn: an inherited _tag must not classify a namespace as a Definition. export const isDefinition = (value: unknown): value is Definition => - typeof value === "object" && value !== null && "_tag" in value && value._tag === "CodeModeTool" + typeof value === "object" && + value !== null && + "_tag" in value && + Object.hasOwn(value, "_tag") && + value._tag === "CodeModeTool" /** * Defines one schema-described tool available to a CodeMode program through `tools.*`. * - * `input` and `output` each accept a validating Effect Schema or a render-only JSON Schema - * document. Effect Schema input is decoded before `run` is invoked, and `run` returns the - * encoded representation of an Effect Schema `output`, which CodeMode decodes before returning - * it to the program. JSON Schemas only shape the model-visible signature; values pass through - * unvalidated. `output` is optional - without it the signature advertises `unknown` and the - * host result is exposed as-is. The host tool remains responsible for authorization and - * durable side-effect handling. - * - * @example - * ```ts - * const lookup = Tool.make({ - * description: "Look up an order", - * input: Schema.Struct({ id: Schema.String }), - * output: Schema.Struct({ status: Schema.String }), - * run: ({ id }) => Effect.succeed({ status: "open" }), - * }) - * - * const fromJsonSchema = Tool.make({ - * description: "Call an adapter-described tool", - * input: { type: "object", properties: { id: { type: "string" } }, required: ["id"] }, - * run: (input) => callHost(input), - * }) - * ``` + * Effect Schemas validate values; JSON Schemas only shape the model-visible signature. + * Without `output`, results are exposed as `unknown`. Hosts remain responsible for authorization + * and durable side effects. */ export const make = ( options: Options, diff --git a/packages/codemode/src/tools.ts b/packages/codemode/src/tools.ts new file mode 100644 index 0000000000..04e36dcef2 --- /dev/null +++ b/packages/codemode/src/tools.ts @@ -0,0 +1,5 @@ +import type { Definition } from "./tool.js" + +export type Tools = { + readonly [name: string]: Definition | Tools +} diff --git a/packages/codemode/src/values.ts b/packages/codemode/src/values.ts index 4ca305d815..539aba82e4 100644 --- a/packages/codemode/src/values.ts +++ b/packages/codemode/src/values.ts @@ -1,49 +1,45 @@ -import type { Effect, Fiber } from "effect" +import type { Fiber } from "effect" -export class SandboxPromise { - interrupted = false - constructor( - readonly fiber: Fiber.Fiber | undefined, - readonly immediate?: Effect.Effect, - ) {} +export class CodeModePromise { + constructor(readonly fiber: Fiber.Fiber) {} } -export class SandboxDate { +export class CodeModeDate { constructor(readonly time: number) {} } -export class SandboxRegExp { +export class CodeModeRegExp { readonly regex: RegExp constructor(pattern: string, flags: string) { this.regex = new RegExp(pattern, flags) } } -export class SandboxMap { +export class CodeModeMap { readonly map = new Map() } -export class SandboxSet { +export class CodeModeSet { readonly set = new Set() } -export class SandboxURLSearchParams { +export class CodeModeURLSearchParams { constructor(readonly params: URLSearchParams) {} } -export class SandboxURL { - readonly searchParams: SandboxURLSearchParams +export class CodeModeURL { + readonly searchParams: CodeModeURLSearchParams constructor(readonly url: URL) { - this.searchParams = new SandboxURLSearchParams(url.searchParams) + this.searchParams = new CodeModeURLSearchParams(url.searchParams) } } -export const isSandboxValue = ( +export const isCodeModeValue = ( value: unknown, -): value is SandboxDate | SandboxRegExp | SandboxMap | SandboxSet | SandboxURL | SandboxURLSearchParams => - value instanceof SandboxDate || - value instanceof SandboxRegExp || - value instanceof SandboxMap || - value instanceof SandboxSet || - value instanceof SandboxURL || - value instanceof SandboxURLSearchParams +): value is CodeModeDate | CodeModeRegExp | CodeModeMap | CodeModeSet | CodeModeURL | CodeModeURLSearchParams => + value instanceof CodeModeDate || + value instanceof CodeModeRegExp || + value instanceof CodeModeMap || + value instanceof CodeModeSet || + value instanceof CodeModeURL || + value instanceof CodeModeURLSearchParams diff --git a/packages/codemode/test/LICENSE.test262 b/packages/codemode/test/LICENSE.test262 new file mode 100644 index 0000000000..fb7434013b --- /dev/null +++ b/packages/codemode/test/LICENSE.test262 @@ -0,0 +1,28 @@ +Test262: ECMAScript Test Suite ("Software") is protected by copyright and is being +made available under the "BSD License", included below. This Software may be subject to third party rights (rights +from parties other than Ecma International), including patent rights, and no licenses under such third party rights +are granted under this license even if the third party concerned is a member of Ecma International. SEE THE ECMA +CODE OF CONDUCT IN PATENT MATTERS AVAILABLE AT https://www.ecma-international.org/ipr FOR +INFORMATION REGARDING THE LICENSING OF PATENT CLAIMS THAT ARE REQUIRED TO IMPLEMENT ECMA INTERNATIONAL STANDARDS*. + +Copyright (C) 2012 Ecma International +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following + disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other materials provided with the distribution. +3. Neither the name of the authors nor Ecma International may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE ECMA INTERNATIONAL "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL ECMA INTERNATIONAL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +* Ecma International Standards hereafter means Ecma International Standards as well as Ecma Technical Reports diff --git a/packages/codemode/test/array-callbacks-test262.test.ts b/packages/codemode/test/array-callbacks-test262.test.ts new file mode 100644 index 0000000000..c34137abd0 --- /dev/null +++ b/packages/codemode/test/array-callbacks-test262.test.ts @@ -0,0 +1,380 @@ +/* + * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75: + * - test/built-ins/Array/prototype/map/15.4.4.19-8-1.js + * - test/built-ins/Array/prototype/map/15.4.4.19-8-2.js + * - test/built-ins/Array/prototype/map/15.4.4.19-8-b-1.js + * - test/built-ins/Array/prototype/filter/15.4.4.20-9-1.js + * - test/built-ins/Array/prototype/filter/15.4.4.20-9-2.js + * - test/built-ins/Array/prototype/filter/15.4.4.20-9-b-1.js + * - test/built-ins/Array/prototype/find/predicate-call-parameters.js + * - test/built-ins/Array/prototype/find/predicate-not-called-on-empty-array.js + * - test/built-ins/Array/prototype/find/return-found-value-predicate-result-is-true.js + * - test/built-ins/Array/prototype/find/return-undefined-if-predicate-returns-false-value.js + * - test/built-ins/Array/prototype/findIndex/predicate-call-parameters.js + * - test/built-ins/Array/prototype/findIndex/return-index-predicate-result-is-true.js + * - test/built-ins/Array/prototype/findIndex/return-negative-one-if-predicate-returns-false-value.js + * - test/built-ins/Array/prototype/findLast/predicate-call-parameters.js + * - test/built-ins/Array/prototype/findLast/return-found-value-predicate-result-is-true.js + * - test/built-ins/Array/prototype/findLast/return-undefined-if-predicate-returns-false-value.js + * - test/built-ins/Array/prototype/findLastIndex/predicate-call-parameters.js + * - test/built-ins/Array/prototype/findLastIndex/return-index-predicate-result-is-true.js + * - test/built-ins/Array/prototype/findLastIndex/return-negative-one-if-predicate-returns-false-value.js + * - test/built-ins/Array/prototype/some/15.4.4.17-7-1.js + * - test/built-ins/Array/prototype/some/15.4.4.17-8-1.js + * - test/built-ins/Array/prototype/every/15.4.4.16-7-1.js + * - test/built-ins/Array/prototype/every/15.4.4.16-8-1.js + * - test/built-ins/Array/prototype/forEach/15.4.4.18-7-1.js + * - test/built-ins/Array/prototype/forEach/15.4.4.18-7-2.js + * - test/built-ins/Array/prototype/reduce/15.4.4.21-9-5.js + * - test/built-ins/Array/prototype/reduce/15.4.4.21-9-c-ii-20.js + * - test/built-ins/Array/prototype/reduce/15.4.4.21-9-1.js + * - test/built-ins/Array/prototype/reduce/15.4.4.21-10-1.js + * - test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-5.js + * - test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-c-ii-20.js + * - test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-1.js + * - test/built-ins/Array/prototype/reduceRight/15.4.4.22-10-1.js + * - test/built-ins/Array/prototype/flatMap/depth-always-one.js + * - test/built-ins/Array/prototype/flatMap/mapperfunction-throws.js + * - test/built-ins/Array/prototype/sort/S15.4.4.11_A1.1_T1.js + * - test/built-ins/Array/prototype/sort/S15.4.4.11_A2.1_T1.js + * - test/built-ins/Array/prototype/sort/stability-5-elements.js + * - test/built-ins/Array/prototype/toSorted/comparefn-controls-sort.js + * - test/built-ins/Array/prototype/toSorted/comparefn-default.js + * - test/built-ins/Array/prototype/toSorted/immutable.js + * - test/built-ins/Array/prototype/toSorted/zero-or-one-element.js + * + * Copyright (C) 2015 the V8 project authors. All rights reserved. + * Copyright (C) 2018 Mathias Bynens. All rights reserved. + * Copyright (C) 2018 Shilpi Jain and Michael Ficarra. All rights reserved. + * Copyright (C) 2021 Igalia, S.L. All rights reserved. + * Copyright (C) 2021 Microsoft. All rights reserved. + * Copyright (C) 2025 Google. All rights reserved. + * Copyright (C) 2026 Garham Lee. All rights reserved. + * Copyright (c) 2012 Ecma International. All rights reserved. + * Copyright 2009 the Sputnik authors. All rights reserved. + * Test262 portions are governed by the BSD license in LICENSE.test262. + */ +import { describe, expect, test } from "bun:test" +import { Effect } from "effect" +import { CodeMode } from "../src/index.js" + +const value = async (code: string) => { + const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} })) + if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) + return result.value +} + +const cases = [ + { + path: "test/built-ins/Array/prototype/map/15.4.4.19-8-1.js", + code: `const input = [1, 2]; input[3] = 4; input[4] = 5; const result = input.map((value) => { input[2] = 3; input[5] = 6; return 1 }); return [result.length, result[5] === undefined]`, + expected: [5, true], + }, + { + path: "test/built-ins/Array/prototype/map/15.4.4.19-8-2.js", + code: `const input = [1, 2, 3, 4, 5]; const result = input.map((value) => { input[4] = -1; return value > 0 ? 1 : 0 }); return [result.length, result[4]]`, + expected: [5, 0], + }, + { + path: "test/built-ins/Array/prototype/map/15.4.4.19-8-b-1.js", + code: `const input = []; input[10] = 0; input.pop(); input[1] = undefined; let calls = 0; const result = input.map(() => { calls += 1; return 1 }); return [result.length, calls, 0 in result, 1 in result]`, + expected: [10, 1, false, true], + }, + { + path: "test/built-ins/Array/prototype/filter/15.4.4.20-9-1.js", + code: `const input = [1, 2]; input[3] = 4; input[4] = 5; const result = input.filter(() => { input[2] = 3; input[5] = 6; return true }); return result`, + expected: [1, 2, 3, 4, 5], + }, + { + path: "test/built-ins/Array/prototype/filter/15.4.4.20-9-2.js", + code: `const input = [1, 2, 3, 4, 5]; return input.filter((value) => { input[2] = -1; input[4] = -1; return value > 0 })`, + expected: [1, 2, 4], + }, + { + path: "test/built-ins/Array/prototype/filter/15.4.4.20-9-b-1.js", + code: `const input = []; input[9] = 0; input.pop(); input[1] = undefined; let calls = 0; const result = input.filter(() => { calls += 1; return false }); return [result, calls]`, + expected: [[], 1], + }, + { + path: "test/built-ins/Array/prototype/find/predicate-call-parameters.js", + code: `const input = [10, 20]; const seen = []; input.find((value, index, receiver) => { seen.push([value, index, receiver === input]); return false }); return seen`, + expected: [ + [10, 0, true], + [20, 1, true], + ], + }, + { + path: "test/built-ins/Array/prototype/find/return-found-value-predicate-result-is-true.js", + code: `return [1, 2, 3].find((value) => value > 1)`, + expected: 2, + }, + { + path: "test/built-ins/Array/prototype/find/return-undefined-if-predicate-returns-false-value.js", + code: `return [1, 2, 3].find((value) => value > 4) === undefined`, + expected: true, + }, + { + path: "test/built-ins/Array/prototype/find/predicate-not-called-on-empty-array.js", + code: `let calls = 0; const result = [].find(() => { calls += 1; return true }); return [result === undefined, calls]`, + expected: [true, 0], + }, + { + path: "test/built-ins/Array/prototype/findIndex/predicate-call-parameters.js", + code: `const input = [10, 20]; const seen = []; input.findIndex((value, index, receiver) => { seen.push([value, index, receiver === input]); return false }); return seen`, + expected: [ + [10, 0, true], + [20, 1, true], + ], + }, + { + path: "test/built-ins/Array/prototype/findIndex/return-index-predicate-result-is-true.js", + code: `return [1, 2, 3].findIndex((value) => value > 1)`, + expected: 1, + }, + { + path: "test/built-ins/Array/prototype/findIndex/return-negative-one-if-predicate-returns-false-value.js", + code: `return [1, 2, 3].findIndex((value) => value > 4)`, + expected: -1, + }, + { + path: "test/built-ins/Array/prototype/findLast/predicate-call-parameters.js", + code: `const input = [10, 20]; const seen = []; input.findLast((value, index, receiver) => { seen.push([value, index, receiver === input]); return false }); return seen`, + expected: [ + [20, 1, true], + [10, 0, true], + ], + }, + { + path: "test/built-ins/Array/prototype/findLast/return-found-value-predicate-result-is-true.js", + code: `return [1, 2, 3].findLast((value) => value < 3)`, + expected: 2, + }, + { + path: "test/built-ins/Array/prototype/findLast/return-undefined-if-predicate-returns-false-value.js", + code: `return [1, 2, 3].findLast((value) => value > 4) === undefined`, + expected: true, + }, + { + path: "test/built-ins/Array/prototype/findLastIndex/predicate-call-parameters.js", + code: `const input = [10, 20]; const seen = []; input.findLastIndex((value, index, receiver) => { seen.push([value, index, receiver === input]); return false }); return seen`, + expected: [ + [20, 1, true], + [10, 0, true], + ], + }, + { + path: "test/built-ins/Array/prototype/findLastIndex/return-index-predicate-result-is-true.js", + code: `return [1, 2, 3].findLastIndex((value) => value < 3)`, + expected: 1, + }, + { + path: "test/built-ins/Array/prototype/findLastIndex/return-negative-one-if-predicate-returns-false-value.js", + code: `return [1, 2, 3].findLastIndex((value) => value > 4)`, + expected: -1, + }, + { + path: "test/built-ins/Array/prototype/some/15.4.4.17-7-1.js", + code: `const input = [1, 2]; input[3] = 4; input[4] = 5; const seen = []; const result = input.some((value) => { input[2] = 3; seen.push(value); return false }); return [result, seen.includes(3)]`, + expected: [false, true], + }, + { + path: "test/built-ins/Array/prototype/some/15.4.4.17-8-1.js", + code: `return [].some(() => true)`, + expected: false, + }, + { + path: "test/built-ins/Array/prototype/every/15.4.4.16-7-1.js", + code: `const input = [1, 2]; input[3] = 4; input[4] = 5; const seen = []; const result = input.every((value) => { input[2] = 3; seen.push(value); return true }); return [result, seen.includes(3)]`, + expected: [true, true], + }, + { + path: "test/built-ins/Array/prototype/every/15.4.4.16-8-1.js", + code: `return [].every(() => false)`, + expected: true, + }, + { + path: "test/built-ins/Array/prototype/forEach/15.4.4.18-7-1.js", + code: `const input = [1, 2]; input[3] = 4; input[4] = 5; let calls = 0; input.forEach(() => { calls += 1; input[2] = 3; input[5] = 6 }); return calls`, + expected: 5, + }, + { + path: "test/built-ins/Array/prototype/forEach/15.4.4.18-7-2.js", + code: `const input = [1, 2, 3]; const seen = []; input.forEach((value, index) => { seen.push(value); if (index === 0) input.pop() }); return seen`, + expected: [1, 2], + }, + { + path: "test/built-ins/Array/prototype/reduce/15.4.4.21-9-1.js", + code: `const input = [1, 2]; input[3] = 4; input[4] = "5"; return input.reduce((accumulator, value) => { input[2] = 3; input[5] = 6; return accumulator + value })`, + expected: "105", + }, + { + path: "test/built-ins/Array/prototype/reduce/15.4.4.21-9-5.js", + code: `let calls = 0; const result = [1].reduce(() => { calls += 1; return 2 }); return [result, calls]`, + expected: [1, 0], + }, + { + path: "test/built-ins/Array/prototype/reduce/15.4.4.21-9-c-ii-20.js", + code: `let accessed = false; const result = [11].reduce((previous) => { accessed = true; return previous === undefined }, undefined); return [result, accessed]`, + expected: [true, true], + }, + { + path: "test/built-ins/Array/prototype/reduce/15.4.4.21-10-1.js", + code: `const input = [1, 2, 3, 4, 5]; input.reduce(() => 1); return input`, + expected: [1, 2, 3, 4, 5], + }, + { + path: "test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-1.js", + code: `const input = ["1", 2]; input[3] = 4; input[4] = "5"; return input.reduceRight((accumulator, value) => { input[2] = 3; input[5] = 6; return accumulator + value })`, + expected: "54321", + }, + { + path: "test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-5.js", + code: `let calls = 0; const result = [1].reduceRight(() => { calls += 1; return 2 }); return [result, calls]`, + expected: [1, 0], + }, + { + path: "test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-c-ii-20.js", + code: `let accessed = false; const result = [11].reduceRight((previous) => { accessed = true; return previous === undefined }, undefined); return [result, accessed]`, + expected: [true, true], + }, + { + path: "test/built-ins/Array/prototype/reduceRight/15.4.4.22-10-1.js", + code: `const input = [1, 2, 3, 4, 5]; input.reduceRight(() => 1); return input`, + expected: [1, 2, 3, 4, 5], + }, + { + path: "test/built-ins/Array/prototype/flatMap/depth-always-one.js", + code: `return [1, 2, 3].flatMap((value) => [[value * 2]])`, + expected: [[2], [4], [6]], + }, + { + path: "test/built-ins/Array/prototype/flatMap/mapperfunction-throws.js", + code: `try { [1, 2].flatMap(() => { throw "stop" }) } catch (error) { return error === "stop" } return false`, + expected: true, + }, + { + path: "test/built-ins/Array/prototype/sort/S15.4.4.11_A1.1_T1.js", + code: `const input = []; input[2] = 0; input.pop(); input.sort(); return [input.length, input[0] === undefined, input[1] === undefined]`, + expected: [2, true, true], + }, + { + path: "test/built-ins/Array/prototype/sort/S15.4.4.11_A2.1_T1.js", + code: `return ["z", "y", "x", "w", "v", "u", "t", "s", "r", "q", "p", "o", "n", "M", "L", "K", "J", "I", "H", "G", "F", "E", "D", "C", "B", "A"].sort()`, + expected: [ + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + ], + }, + { + path: "test/built-ins/Array/prototype/sort/stability-5-elements.js", + code: `const input = [{ n: "A", r: 2 }, { n: "B", r: 3 }, { n: "C", r: 2 }, { n: "D", r: 3 }, { n: "E", r: 3 }]; return input.sort((left, right) => right.r - left.r).map((item) => item.n).join("")`, + expected: "BDEAC", + }, + { + path: "test/built-ins/Array/prototype/toSorted/comparefn-controls-sort.js", + code: `const mixed = [333, 33, 3, 222, 22, 2, 111, 11, 1]; return [[1, 2, 3, 4].toSorted((a, b) => a - b), [4, 3, 2, 1].toSorted((a, b) => a - b), mixed.toSorted((a, b) => a - b), [1, 2, 3, 4].toSorted((a, b) => b - a), [4, 3, 2, 1].toSorted((a, b) => b - a), mixed.toSorted((a, b) => b - a)]`, + expected: [ + [1, 2, 3, 4], + [1, 2, 3, 4], + [1, 2, 3, 11, 22, 33, 111, 222, 333], + [4, 3, 2, 1], + [4, 3, 2, 1], + [333, 222, 111, 33, 22, 11, 3, 2, 1], + ], + }, + { + path: "test/built-ins/Array/prototype/toSorted/comparefn-default.js", + code: `return [[1, 2, 3, 4].toSorted(), [4, 3, 2, 1].toSorted(), ["a", 2, 1, "z"].toSorted(), [333, 33, 3, 222, 22, 2, 111, 11, 1].toSorted()]`, + expected: [ + [1, 2, 3, 4], + [1, 2, 3, 4], + [1, 2, "a", "z"], + [1, 11, 111, 2, 22, 222, 3, 33, 333], + ], + }, + { + path: "test/built-ins/Array/prototype/toSorted/immutable.js", + code: `const input = [2, 0, 1]; const result = input.toSorted(); return [input, result !== input]`, + expected: [[2, 0, 1], true], + }, + { + path: "test/built-ins/Array/prototype/toSorted/zero-or-one-element.js", + code: `const zero = []; const one = [1]; const zeroResult = zero.toSorted(); const oneResult = one.toSorted(); return [zeroResult, oneResult, zeroResult !== zero, oneResult !== one]`, + expected: [[], [1], true, true], + }, +] as const + +describe("Test262 Array callback adaptations", () => { + for (const item of cases) { + test(item.path, async () => { + expect(await value(item.code)).toEqual(item.expected) + }) + } +}) + +describe("Array callback regressions", () => { + test("reduce and reduceRight find the first present element", async () => { + expect( + await value(` + const left = [] + left[2] = 3 + const right = [] + right[0] = 4 + right[3] = 1 + right.pop() + return [left.reduce((a, b) => a + b), right.reduceRight((a, b) => a + b)] + `), + ).toEqual([3, 4]) + }) + + test("reduce and reduceRight reject arrays containing only holes", async () => { + expect( + await value(` + const values = [] + values[2] = 1 + values.pop() + let left + let right + try { values.reduce((a, b) => a + b) } catch (error) { left = error.name } + try { values.reduceRight((a, b) => a + b) } catch (error) { right = error.name } + return [left, right] + `), + ).toEqual(["TypeError", "TypeError"]) + }) + + test("findLast returns the value observed before predicate mutation", async () => { + expect( + await value(` + const values = [1] + return values.findLast((item, index, array) => { + array[index] = 2 + return true + }) + `), + ).toBe(1) + }) +}) diff --git a/packages/codemode/test/array-core-test262.test.ts b/packages/codemode/test/array-core-test262.test.ts new file mode 100644 index 0000000000..a04e1c2566 --- /dev/null +++ b/packages/codemode/test/array-core-test262.test.ts @@ -0,0 +1,323 @@ +/* + * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75: + * - test/built-ins/Array/prototype/includes/samevaluezero.js + * - test/built-ins/Array/prototype/includes/using-fromindex.js + * - test/built-ins/Array/prototype/join/S15.4.4.5_A3.1_T1.js + * - test/built-ins/Array/prototype/join/S15.4.4.5_A3.2_T1.js + * - test/built-ins/Array/prototype/slice/S15.4.4.10_A1.2_T2.js + * - test/built-ins/Array/prototype/concat/S15.4.4.4_A1_T1.js + * - test/built-ins/Array/prototype/concat/S15.4.4.4_A1_T2.js + * - test/built-ins/Array/prototype/concat/S15.4.4.4_A1_T3.js + * - test/built-ins/Array/prototype/indexOf/fromindex-zero-conversion.js + * - test/built-ins/Array/prototype/indexOf/length-zero-returns-minus-one.js + * - test/built-ins/Array/prototype/lastIndexOf/fromindex-zero-conversion.js + * - test/built-ins/Array/prototype/lastIndexOf/length-zero-returns-minus-one.js + * - test/built-ins/Array/prototype/at/returns-item.js + * - test/built-ins/Array/prototype/at/returns-item-relative-index.js + * - test/built-ins/Array/prototype/at/returns-undefined-for-out-of-range-index.js + * - test/built-ins/Array/prototype/flat/null-undefined-elements.js + * - test/built-ins/Array/prototype/flat/positive-infinity.js + * - test/built-ins/Array/prototype/reverse/S15.4.4.8_A1_T1.js + * - test/built-ins/Array/prototype/toReversed/immutable.js + * - test/built-ins/Array/prototype/toReversed/zero-or-one-element.js + * - test/built-ins/Array/prototype/with/immutable.js + * - test/built-ins/Array/prototype/with/index-negative.js + * - test/built-ins/Array/prototype/push/S15.4.4.7_A1_T1.js + * - test/built-ins/Array/prototype/pop/S15.4.4.6_A1.1_T1.js + * - test/built-ins/Array/prototype/shift/S15.4.4.9_A1.1_T1.js + * - test/built-ins/Array/prototype/unshift/S15.4.4.13_A1_T1.js + * - test/built-ins/Array/prototype/splice/S15.4.4.12_A1.1_T1.js + * - test/built-ins/Array/prototype/splice/S15.4.4.12_A1.2_T1.js + * - test/built-ins/Array/prototype/splice/called_with_one_argument.js + * - test/built-ins/Array/prototype/fill/fill-values.js + * - test/built-ins/Array/prototype/fill/fill-values-custom-start-and-end.js + * - test/built-ins/Array/prototype/fill/return-this.js + * - test/built-ins/Array/prototype/copyWithin/non-negative-target-start-and-end.js + * - test/built-ins/Array/prototype/copyWithin/return-this.js + * - test/built-ins/Array/prototype/keys/iteration.js + * - test/built-ins/Array/prototype/values/iteration.js + * - test/built-ins/Array/prototype/entries/iteration.js + * - test/built-ins/Array/isArray/15.4.3.2-0-3.js + * - test/built-ins/Array/isArray/15.4.3.2-0-4.js + * - test/built-ins/Array/from/from-array.js + * - test/built-ins/Array/from/from-string.js + * - test/built-ins/Array/from/array-like-has-length-but-no-indexes-with-values.js + * - test/built-ins/Array/of/creates-a-new-array-from-arguments.js + * + * Copyright (C) 2015 André Bargull. All rights reserved. + * Copyright (C) 2015 the V8 project authors. All rights reserved. + * Copyright (C) 2016 the V8 project authors. All rights reserved. + * Copyright (C) 2018 Shilpi Jain and Michael Ficarra. All rights reserved. + * Copyright (C) 2020 Alexey Shvayka. All rights reserved. + * Copyright (C) 2020 Rick Waldron. All rights reserved. + * Copyright (C) 2021 Igalia, S.L. All rights reserved. + * Copyright (c) 2012 Ecma International. All rights reserved. + * Copyright (c) 2014 Hank Yates. All rights reserved. + * Copyright (c) 2015 the V8 project authors. All rights reserved. + * Copyright (c) 2021 Rick Waldron. All rights reserved. + * Copyright 2009 the Sputnik authors. All rights reserved. + * Copyright 2015 Microsoft Corporation. All rights reserved. + * Copyright 2016 The V8 project authors. All rights reserved. + * Test262 portions are governed by the BSD license in LICENSE.test262. + */ +import { describe, expect, test } from "bun:test" +import { Effect } from "effect" +import { CodeMode } from "../src/index.js" + +const value = async (code: string) => { + const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} })) + if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) + return result.value +} + +const cases = [ + { + path: "test/built-ins/Array/prototype/includes/samevaluezero.js", + code: `const input = [42, 0, 1, NaN]; return [input.includes(42), input.includes("42"), input.includes([42]), input.includes(true), input.includes(NaN), input.includes(0), input.includes(-0), input.includes(null), input.includes("")]`, + expected: [true, false, false, false, true, true, true, false, false], + }, + { + path: "test/built-ins/Array/prototype/includes/using-fromindex.js", + code: `const input = ["a", "b", "c"]; return [input.includes("a", 0), input.includes("a", 1), input.includes("a", -4), input.includes("a", -3), input.includes("a", -2), input.includes("b", 0), input.includes("b", 1), input.includes("b", 2), input.includes("b", -3), input.includes("b", -2), input.includes("b", -1), input.includes("c", 0), input.includes("c", 2), input.includes("c", 3), input.includes("c", -3), input.includes("c", -1)]`, + expected: [true, false, true, true, false, true, true, false, true, true, false, true, true, false, true, true], + }, + { + path: "test/built-ins/Array/prototype/join/S15.4.4.5_A3.1_T1.js", + code: `return [[0, 1, 2, 3].join("&"), [0, 1, 2, 3].join("")]`, + expected: ["0&1&2&3", "0123"], + }, + { + path: "test/built-ins/Array/prototype/join/S15.4.4.5_A3.2_T1.js", + code: `return [ + ["", "", ""].join(""), + ["&", "&", "&"].join("&"), + [true, true, true].join(), + [null, null, null].join(), + [undefined, undefined, undefined].join(), + [Infinity, Infinity, Infinity].join(), + [NaN, NaN, NaN].join(), + ]`, + expected: ["", "&&&&&", "true,true,true", ",,", ",,", "Infinity,Infinity,Infinity", "NaN,NaN,NaN"], + }, + { + path: "test/built-ins/Array/prototype/slice/S15.4.4.10_A1.2_T2.js", + code: `return [0, 1, 2, 3, 4].slice(-1, 5)`, + expected: [4], + }, + { + path: "test/built-ins/Array/prototype/concat/S15.4.4.4_A1_T1.js", + code: `return [].concat([0, 1], [2, 3, 4])`, + expected: [0, 1, 2, 3, 4], + }, + { + path: "test/built-ins/Array/prototype/concat/S15.4.4.4_A1_T2.js", + code: `const object = { value: 1 }; const result = [0].concat(object, [1, 2], -1, true, "NaN"); return [result, result[1] === object]`, + expected: [[0, { value: 1 }, 1, 2, -1, true, "NaN"], true], + }, + { + path: "test/built-ins/Array/prototype/concat/S15.4.4.4_A1_T3.js", + code: `const input = [0, 1]; const result = input.concat(); return [result, result !== input]`, + expected: [[0, 1], true], + }, + { + path: "test/built-ins/Array/prototype/indexOf/fromindex-zero-conversion.js", + code: `const result = [true].indexOf(true, -0); return [result, 1 / result === Infinity]`, + expected: [0, true], + }, + { + path: "test/built-ins/Array/prototype/indexOf/length-zero-returns-minus-one.js", + code: `return [].indexOf(1)`, + expected: -1, + }, + { + path: "test/built-ins/Array/prototype/lastIndexOf/fromindex-zero-conversion.js", + code: `const result = [true].lastIndexOf(true, -0); return [result, 1 / result === Infinity]`, + expected: [0, true], + }, + { + path: "test/built-ins/Array/prototype/lastIndexOf/length-zero-returns-minus-one.js", + code: `return [].lastIndexOf(1)`, + expected: -1, + }, + { + path: "test/built-ins/Array/prototype/at/returns-item.js", + code: `const input = [1, 2, 3, 4, undefined, 5]; return [input.at(0), input.at(1), input.at(2), input.at(3), input.at(4) === undefined, input.at(5)]`, + expected: [1, 2, 3, 4, true, 5], + }, + { + path: "test/built-ins/Array/prototype/at/returns-item-relative-index.js", + code: `const input = [1, 2, 3, 4, undefined, 5]; return [input.at(0), input.at(-1), input.at(-2) === undefined, input.at(-3), input.at(-4)]`, + expected: [1, 5, true, 4, 3], + }, + { + path: "test/built-ins/Array/prototype/at/returns-undefined-for-out-of-range-index.js", + code: `const input = []; return [input.at(-2) === undefined, input.at(0) === undefined, input.at(1) === undefined]`, + expected: [true, true, true], + }, + { + path: "test/built-ins/Array/prototype/flat/null-undefined-elements.js", + code: `const result = [1, [null, [undefined]]].flat(2); return [result.length, result[0], result[1] === null, result[2] === undefined]`, + expected: [3, 1, true, true], + }, + { + path: "test/built-ins/Array/prototype/flat/positive-infinity.js", + code: `return [1, [2, [3, [4]]]].flat(Infinity)`, + expected: [1, 2, 3, 4], + }, + { + path: "test/built-ins/Array/prototype/reverse/S15.4.4.8_A1_T1.js", + code: `const empty = []; const one = [1]; const input = [1, 2]; const emptyResult = empty.reverse(); const oneResult = one.reverse(); const result = input.reverse(); return [emptyResult === empty, oneResult === one, result === input, input]`, + expected: [true, true, true, [2, 1]], + }, + { + path: "test/built-ins/Array/prototype/toReversed/immutable.js", + code: `const input = [0, 1, 2]; const result = input.toReversed(); return [input, result !== input]`, + expected: [[0, 1, 2], true], + }, + { + path: "test/built-ins/Array/prototype/toReversed/zero-or-one-element.js", + code: `const zero = []; const one = [1]; const zeroResult = zero.toReversed(); const oneResult = one.toReversed(); return [zeroResult, oneResult, zeroResult !== zero, oneResult !== one]`, + expected: [[], [1], true, true], + }, + { + path: "test/built-ins/Array/prototype/with/immutable.js", + code: `const input = [0, 1, 2]; const result = input.with(1, 3); return [input, result !== input, input.with(1, 1) !== input]`, + expected: [[0, 1, 2], true, true], + }, + { + path: "test/built-ins/Array/prototype/with/index-negative.js", + code: `const input = [0, 1, 2]; return [input.with(-1, 4), input.with(-3, 4)]`, + expected: [ + [0, 1, 4], + [4, 1, 2], + ], + }, + { + path: "test/built-ins/Array/prototype/push/S15.4.4.7_A1_T1.js", + code: `const input = []; return [input.push(1), input.push(), input.push(-1), input]`, + expected: [1, 1, 2, [1, -1]], + }, + { + path: "test/built-ins/Array/prototype/pop/S15.4.4.6_A1.1_T1.js", + code: `const input = []; return [input.pop() === undefined, input.length]`, + expected: [true, 0], + }, + { + path: "test/built-ins/Array/prototype/shift/S15.4.4.9_A1.1_T1.js", + code: `const input = []; return [input.shift() === undefined, input.length]`, + expected: [true, 0], + }, + { + path: "test/built-ins/Array/prototype/unshift/S15.4.4.13_A1_T1.js", + code: `const input = []; return [input.unshift(1), input[0], input.unshift(), input.unshift(-1), input]`, + expected: [1, 1, 1, 2, [-1, 1]], + }, + { + path: "test/built-ins/Array/prototype/splice/S15.4.4.12_A1.1_T1.js", + code: `const input = [0, 1, 2, 3]; const removed = input.splice(0, 3); return [input, removed]`, + expected: [[3], [0, 1, 2]], + }, + { + path: "test/built-ins/Array/prototype/splice/S15.4.4.12_A1.2_T1.js", + code: `const input = [0, 1]; const removed = input.splice(-2, -1); return [input, removed]`, + expected: [[0, 1], []], + }, + { + path: "test/built-ins/Array/prototype/splice/called_with_one_argument.js", + code: `const input = ["first", "second", "third"]; const removed = input.splice(1); return [input, removed]`, + expected: [["first"], ["second", "third"]], + }, + { + path: "test/built-ins/Array/prototype/fill/fill-values-custom-start-and-end.js", + code: `const input = [0, 0, 0, 0, 0]; input.fill(8, -3, 4); const sparse = []; sparse[4] = 0; sparse.fill(8, 1, 3); return [[0, 0, 0].fill(8, 1, 2), input, [0, 0, 0, 0, 0].fill(8, -2, -1), [0, 0, 0, 0, 0].fill(8, -1, -3), [0 in sparse, sparse[1], sparse[2], 3 in sparse, sparse[4]]]`, + expected: [ + [0, 8, 0], + [0, 0, 8, 8, 0], + [0, 0, 0, 8, 0], + [0, 0, 0, 0, 0], + [false, 8, 8, false, 0], + ], + }, + { + path: "test/built-ins/Array/prototype/fill/return-this.js", + code: `const input = []; return input.fill(1) === input`, + expected: true, + }, + { + path: "test/built-ins/Array/prototype/fill/fill-values.js", + code: `const omitted = [0, 0].fill(); return [[].fill(8), omitted.map((value) => value === undefined), [0, 0, 0].fill(8)]`, + expected: [[], [true, true], [8, 8, 8]], + }, + { + path: "test/built-ins/Array/prototype/copyWithin/non-negative-target-start-and-end.js", + code: `return [[0, 1, 2, 3].copyWithin(0, 0, 0), [0, 1, 2, 3].copyWithin(0, 0, 2), [0, 1, 2, 3].copyWithin(0, 1, 2), [0, 1, 2, 3].copyWithin(1, 0, 2), [0, 1, 2, 3, 4, 5].copyWithin(1, 3, 5)]`, + expected: [ + [0, 1, 2, 3], + [0, 1, 2, 3], + [1, 1, 2, 3], + [0, 0, 1, 3], + [0, 3, 4, 3, 4, 5], + ], + }, + { + path: "test/built-ins/Array/prototype/copyWithin/return-this.js", + code: `const input = [0, 1, 2, 3]; const result = input.copyWithin(1, 0, 2); return [input, result === input]`, + expected: [[0, 0, 1, 3], true], + }, + { + path: "test/built-ins/Array/prototype/keys/iteration.js", + code: `return ["a", "b", "c"].keys()`, + expected: [0, 1, 2], + }, + { + path: "test/built-ins/Array/prototype/values/iteration.js", + code: `return ["a", "b", "c"].values()`, + expected: ["a", "b", "c"], + }, + { + path: "test/built-ins/Array/prototype/entries/iteration.js", + code: `return ["a", "b"].entries()`, + expected: [ + [0, "a"], + [1, "b"], + ], + }, + { + path: "test/built-ins/Array/isArray/15.4.3.2-0-3.js", + code: `return [Array.isArray([]), Array.isArray([1]), Array.isArray(Array.of(1))]`, + expected: [true, true, true], + }, + { + path: "test/built-ins/Array/isArray/15.4.3.2-0-4.js", + code: `return [Array.isArray(42), Array.isArray({}), Array.isArray(null), Array.isArray("array")]`, + expected: [false, false, false, false], + }, + { + path: "test/built-ins/Array/from/from-array.js", + code: `const input = [0, "foo", undefined, Infinity]; const result = Array.from(input); return [result.length, result[0], result[1], result[2] === undefined, result[3] === Infinity, result !== input, result instanceof Array]`, + expected: [4, 0, "foo", true, true, true, true], + }, + { + path: "test/built-ins/Array/from/from-string.js", + code: `return Array.from("Test")`, + expected: ["T", "e", "s", "t"], + }, + { + path: "test/built-ins/Array/from/array-like-has-length-but-no-indexes-with-values.js", + code: `const result = Array.from({ length: 5 }); const mapped = result.map(() => 1); return [result.length, result.map((value) => value === undefined), mapped.length, mapped]`, + expected: [5, [true, true, true, true, true], 5, [1, 1, 1, 1, 1]], + }, + { + path: "test/built-ins/Array/of/creates-a-new-array-from-arguments.js", + code: `const mixed = Array.of(undefined, false, null, undefined); return [Array.of("Mike", "Rick", "Leo"), mixed.length, mixed[0] === undefined, mixed[1], mixed[2], mixed[3] === undefined, Array.of()]`, + expected: [["Mike", "Rick", "Leo"], 4, true, false, null, true, []], + }, +] as const + +describe("Test262 Array core adaptations", () => { + for (const item of cases) { + test(item.path, async () => { + expect(await value(item.code)).toEqual(item.expected) + }) + } +}) diff --git a/packages/codemode/test/callbacks.test.ts b/packages/codemode/test/callbacks.test.ts new file mode 100644 index 0000000000..6391448a70 --- /dev/null +++ b/packages/codemode/test/callbacks.test.ts @@ -0,0 +1,258 @@ +import { describe, expect, test } from "bun:test" +import { Effect, Schema } from "effect" +import { CodeMode, Tool } from "../src/index.js" + +// Callback acceptance is one gate shared by array methods, sort, string replacers, +// Array.from mappers, Map/Set/URLSearchParams forEach, and promise reactions: +// interpreter functions, coercion/URI builtins, resolver capabilities, and built-in +// method references are callable; tools and other opaque callables get a wrap hint. +const run = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools: {} })) +const value = async (code: string) => { + const result = await run(code) + if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) + return result.value +} +const error = async (code: string) => { + const result = await run(code) + if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`) + return result.error +} +const logsOf = async (code: string) => { + const result = await run(code) + if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) + return result.logs ?? [] +} + +const echo = Tool.make({ + description: "Echo the input", + input: Schema.Struct({ id: Schema.Number }), + output: Schema.Number, + run: (input: { id: number }) => Effect.succeed(input.id), +}) +const withTool = (code: string) => Effect.runPromise(CodeMode.make({ tools: { host: { echo } } }).execute(code)) +const toolError = async (code: string) => { + const result = await withTool(code) + if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`) + return result.error +} + +describe("built-in method references as callbacks", () => { + test("map accepts Math methods", async () => { + expect(await value(`return [-1, 2, -3].map(Math.abs)`)).toEqual([1, 2, 3]) + expect(await value(`return [1.5, 2.7].map(Math.floor)`)).toEqual([1, 2]) + }) + + test("map(JSON.stringify) matches JS: the index replacer and array space are ignored", async () => { + expect(await value(`return [{ a: 1 }, [2]].map(JSON.stringify)`)).toEqual(['{"a":1}', "[2]"]) + }) + + test("map(Number.parseInt) reproduces the JS radix footgun", async () => { + // parseInt("2", 1) is NaN in real JS; NaN serializes to null at the result boundary. + expect(await value(`return ["1", "2"].map(Number.parseInt)`)).toEqual([1, null]) + }) + + test("filter and find accept built-in predicates", async () => { + expect(await value(`return [0, 1, NaN, 2].filter(Number.isInteger)`)).toEqual([0, 1, 2]) + expect(await value(`return [1.5, 3, 2.5].find(Number.isInteger)`)).toBe(3) + }) + + test("forEach(console.log) captures one log line per element", async () => { + const logs = await logsOf(`["a", "b"].forEach(console.log); return null`) + expect(logs).toHaveLength(2) + expect(logs[0]).toContain("a") + expect(logs[1]).toContain("b") + }) + + test("intrinsic method references keep their receiver, unlike detached JS methods", async () => { + expect(await value(`return ["a", "z"].filter("abc".includes)`)).toEqual(["a"]) + }) + + test("promise reactions accept built-in references", async () => { + expect(await value(`return await Promise.resolve(-5).then(Math.abs)`)).toBe(5) + const logs = await logsOf(`await Promise.resolve("done").then(console.log); return null`) + expect(logs).toHaveLength(1) + expect(logs[0]).toContain("done") + }) +}) + +describe("constructors callable without new, like JS", () => { + test("Error constructors work as callbacks and direct calls", async () => { + expect(await value(`return ["boom"].map(Error)[0].message`)).toBe("boom") + expect(await value(`return TypeError("bad").name`)).toBe("TypeError") + }) + + test("error values stringify like JS Error.prototype.toString", async () => { + expect(await value(`return String(TypeError("bad"))`)).toBe("TypeError: bad") + expect(await value(`return String(Error(""))`)).toBe("Error") + expect(await value(`return "x" + RangeError("oops")`)).toBe("xRangeError: oops") + expect(await value(`return "a1b2".replace(/\\d/, Error)`)).toBe("aError: 1b2") + }) + + test("literal elisions are real holes, like JS", async () => { + expect(await value(`return (0 in [, 1])`)).toBe(false) + expect(await value(`return Object.keys([, 1, ,])`)).toEqual(["1"]) + expect(await value(`return [, 1, ,].filter(() => true).length`)).toBe(1) + expect(await value(`return [, ,].every((x) => false)`)).toBe(true) + }) + + test("Array constructs from arguments or a length", async () => { + expect(await value(`return Array(1, 2, 3)`)).toEqual([1, 2, 3]) + expect(await value(`return Array("3")`)).toEqual(["3"]) + expect(await value(`return Array(3).length`)).toBe(3) + expect(await value(`return new Array(2).length`)).toBe(2) + // Holes stay holes, like JS: map skips them (length preserved, normalized to + // null at the host boundary), spread materializes undefined. + expect(await value(`return Array(3).map((x) => 1)`)).toEqual([null, null, null]) + expect(await value(`return Array(3).map((x) => 1).length`)).toBe(3) + expect(await value(`return [...Array(3)].map((_, i) => i)`)).toEqual([0, 1, 2]) + expect((await error(`return Array(-1)`)).message).toContain("Invalid array length") + expect((await error(`return Array(1.5)`)).message).toContain("Invalid array length") + }) + + test("Object returns objects unchanged and rejects primitive wrappers", async () => { + expect(await value(`return Object()`)).toEqual({}) + expect(await value(`const o = { a: 1 }; return Object(o) === o`)).toBe(true) + expect((await error(`return Object(1)`)).message).toContain("wrapper objects are not supported") + }) + + test("Date() without new returns a deterministic ISO string and ignores arguments", async () => { + expect(await value(`return /^\\d{4}-\\d{2}-\\d{2}T.*Z$/.test(Date(1000))`)).toBe(true) + expect(await value(`return "abc".replace(RegExp("b"), "x")`)).toBe("axc") + }) + + test("map(Array) matches the JS 3-argument call", async () => { + expect(await value(`return [7].map(Array)`)).toEqual([[7, 0, [7]]]) + }) + + test("array length boundaries match JS", async () => { + expect(await value(`return Array(4294967295).length`)).toBe(4294967295) + const diagnostic = await error(`return Array(4294967296)`) + expect(diagnostic.message).toContain("Invalid array length") + expect((await error(`try { Array(-1) } catch (e) { throw Error(e.name) }`)).message).toContain("RangeError") + }) + + test("sort densifies trailing holes into undefined (documented divergence)", async () => { + expect(await value(`return Array(2).sort().map(() => 1)`)).toEqual([1, 1]) + }) + + test("returned sparse arrays normalize holes to null at the host boundary", async () => { + expect(await value(`return Array(3)`)).toEqual([null, null, null]) + }) + + test("RegExp with non-string flags throws a SyntaxError, like JS", async () => { + expect((await error(`try { RegExp("a", 0) } catch (e) { throw Error(e.name) }`)).message).toContain("SyntaxError") + }) + + test("new-requiring constructors throw a TypeError when called", async () => { + expect((await error(`return Map()`)).message).toContain("Constructor Map requires 'new'") + expect((await error(`return [1].map(Set)`)).message).toContain("Constructor Set requires 'new'") + expect((await error(`return Promise(() => 1)`)).message).toContain("Constructor Promise requires 'new'") + // As a reaction handler the TypeError rejects the derived promise catchably, like JS. + expect(await value(`return await Promise.resolve(1).then(Map).catch((e) => e.name)`)).toBe("TypeError") + }) +}) + +describe("sort accepts the unified callback set", () => { + test("sort and toSorted take built-in comparators", async () => { + expect(await value(`return [0, 1, 0].sort(Boolean)`)).toEqual([0, 0, 1]) + expect(await value(`return [0, 1, 0].toSorted(Boolean)`)).toEqual([0, 0, 1]) + }) + + test("a non-callable comparator is rejected", async () => { + expect((await error(`return [2, 1].sort(42)`)).message).toContain("Array.sort expects a function callback") + expect((await error(`return [2, 1].toSorted(42)`)).message).toContain("Array.toSorted expects a function callback") + }) +}) + +describe("Array.from mapper", () => { + test("maps with (value, index) over arrays, strings, and Sets", async () => { + expect(await value(`return Array.from([1, 2, 3], (x) => x * 2)`)).toEqual([2, 4, 6]) + expect(await value(`return Array.from("ab", (c, i) => c + i)`)).toEqual(["a0", "b1"]) + expect(await value(`return Array.from(new Set([1, 2]), (x) => x * 10)`)).toEqual([10, 20]) + }) + + test("accepts coercion builtins and an explicit undefined mapper", async () => { + expect(await value(`return Array.from(["5", "7"], Number)`)).toEqual([5, 7]) + expect(await value(`return Array.from([1, 2], undefined)`)).toEqual([1, 2]) + }) + + test("rejects a non-callable mapper", async () => { + expect((await error(`return Array.from([1], 42)`)).message).toContain("Array.from expects a function callback") + }) +}) + +describe("thisArg is accepted and ignored, like JS arrows", () => { + // CodeMode functions have no `this`, so a thisArg can never change behavior — + // exactly like passing one alongside an arrow function in real JS. + test("iteration methods and Array.from ignore a thisArg", async () => { + expect(await value(`return [1, 2].map((x) => x * 2, {})`)).toEqual([2, 4]) + expect(await value(`return [1, 2].map((x) => x, undefined)`)).toEqual([1, 2]) + expect(await value(`return Array.from([1], (x) => x + 1, {})`)).toEqual([2]) + }) + + test("Map, Set, and URLSearchParams forEach ignore a thisArg", async () => { + expect(await value(`const o = []; new Map([["a", 1]]).forEach((v, k) => o.push(k), {}); return o`)).toEqual(["a"]) + expect(await value(`const o = []; new Set([1]).forEach((v) => o.push(v), "self"); return o`)).toEqual([1]) + expect(await value(`const o = []; new URLSearchParams("a=1").forEach((v) => o.push(v), 0); return o`)).toEqual([ + "1", + ]) + }) +}) + +describe("still-rejected callables get the wrap hint", () => { + test("tool references as callbacks suggest an arrow wrapper", async () => { + const diagnostic = await toolError(`return [1, 2].map(tools.host.echo)`) + expect(diagnostic.message).toContain("wrap it in an arrow function") + expect(await withTool(`return await Promise.all([1, 2].map((id) => tools.host.echo({ id })))`)).toMatchObject({ + ok: true, + value: [1, 2], + }) + }) + + test("detached Promise statics as callbacks suggest an arrow wrapper", async () => { + expect((await error(`return [1].map(Promise.resolve)`)).message).toContain("wrap it in an arrow function") + }) + + test("string replacers reject opaque callables with the wrap hint, not a type error", async () => { + const diagnostic = await toolError(`return "abc".replace(/b/, tools.host.echo)`) + expect(diagnostic.message).toContain("wrap it in an arrow function") + expect(diagnostic.message).not.toContain("argument 2") + }) + + test("built-in references work as replacers", async () => { + // Like real JS: JSON.stringify(match, offset, string) quotes the match. + expect(await value(`return "abc".replace(/b/, JSON.stringify)`)).toBe('a"b"c') + // Math methods stay strict about consumed arguments: a match string is not coerced. + expect((await error(`return "3.7".replace(/\\d\\.\\d/, Math.floor)`)).message).toContain( + "Math.floor expects number arguments", + ) + }) + + test("non-callables still get the plain callback error", async () => { + expect((await error(`return [1].map(42)`)).message).toContain("Array.map expects a function callback") + }) + + test("promise handlers reject opaque callables with the wrap hint", async () => { + const diagnostic = await toolError(`return await Promise.resolve(1).then(tools.host.echo)`) + expect(diagnostic.message).toContain("Promise.prototype.then cannot use this callable as a handler") + expect(diagnostic.message).toContain("wrap it in an arrow function") + }) + + test("callable JSON.stringify replacers are rejected, never silently ignored", async () => { + expect((await error(`return JSON.stringify({ a: 1 }, Math.abs)`)).message).toContain( + "JSON.stringify replacers are not supported", + ) + expect((await toolError(`return JSON.stringify({ a: 1 }, tools.host.echo)`)).message).toContain( + "JSON.stringify replacers are not supported", + ) + }) + + test("callable JSON.parse revivers are rejected, never silently ignored", async () => { + expect((await error(`return JSON.parse('{"a":1}', (key, v) => 99)`)).message).toContain( + "JSON.parse revivers are not supported", + ) + expect(await value(`return JSON.parse('{"a":1}', undefined)`)).toEqual({ a: 1 }) + // A non-callable reviver is silently ignored, matching JS's IsCallable check. + expect(await value(`return JSON.parse('{"a":1}', 42)`)).toEqual({ a: 1 }) + }) +}) diff --git a/packages/codemode/test/codemode.test.ts b/packages/codemode/test/codemode.test.ts index 221b5e07df..e2c91e7f4e 100644 --- a/packages/codemode/test/codemode.test.ts +++ b/packages/codemode/test/codemode.test.ts @@ -26,6 +26,22 @@ describe("CodeMode host failure boundary", () => { }) }) + test("does not rewrite explicit safe tool failures", async () => { + const result = await run( + Tool.make({ + description: "Fail safely", + input: Schema.Struct({}), + output: Schema.String, + run: () => Effect.fail(toolError("File not found: /tmp/report.json")), + }), + ) + + expect(result.ok ? undefined : result.error).toStrictEqual({ + kind: "ToolFailure", + message: "File not found: /tmp/report.json", + }) + }) + test("sanitizes unknown host failures and defects", async () => { for (const failure of [ Effect.fail(new UnsafeHostError({ reason: "Authorization: Bearer typed-secret" })), @@ -260,7 +276,7 @@ describe("CodeMode console capture", () => { expect(result.logs).toStrictEqual(["NaN", "Infinity -Infinity", '{"ratio":NaN,"bounds":[Infinity]}']) }) - test("renders sandbox values nested inside logged containers", async () => { + test("renders CodeMode values nested inside logged containers", async () => { const result = await Effect.runPromise( CodeMode.execute({ code: ` @@ -295,7 +311,7 @@ describe("CodeMode console capture", () => { expect(result.logs).toStrictEqual(['{"box":Map(1) [["self",[Circular]]]}', '{"fn":[CodeMode reference],"ok":1}']) }) - test("console.table renders sandbox value cells", async () => { + test("console.table renders CodeMode value cells", async () => { const result = await Effect.runPromise( CodeMode.execute({ code: ` @@ -437,6 +453,56 @@ describe("CodeMode schema flexibility", () => { expect(observed).toStrictEqual([{ id: 42 }]) }) + test("outbound tool arguments follow JSON serialization semantics", async () => { + const observed: Array = [] + const call = Tool.make({ + description: "Observe raw input", + input: { type: "object" }, + run: (input) => + Effect.sync(() => { + observed.push(input) + return "ok" + }), + }) + const runtime = CodeMode.make({ tools: { adapter: { call } } }) + + const result = await Effect.runPromise( + runtime.execute( + `return await tools.adapter.call({ q: undefined, limit: 0 / 0, rate: 1 / 0, items: [1, undefined, 2], holes: [1, , 3] })`, + ), + ) + expect(result.ok).toBe(true) + const received = observed[0] as Record + expect(received).toStrictEqual({ limit: null, rate: null, items: [1, null, 2], holes: [1, null, 3] }) + // The undefined-valued property is dropped like JSON.stringify, not delivered as undefined. + expect(Object.hasOwn(received, "q")).toBe(false) + }) + + test("dropping undefined values lets optionalKey schemas accept conditional arguments", async () => { + const observed: Array = [] + const find = Tool.make({ + description: "Find things", + input: Schema.Struct({ query: Schema.optionalKey(Schema.String), limit: Schema.optionalKey(Schema.Number) }), + run: (input) => + Effect.sync(() => { + observed.push(input) + return "ok" + }), + }) + const runtime = CodeMode.make({ tools: { things: { find } } }) + + // The `cond ? value : undefined` idiom: optionalKey rejects a present undefined, so the + // JSON boundary must drop the key before the schema decodes. + const result = await Effect.runPromise( + runtime.execute(`return await tools.things.find({ query: undefined, limit: 5 })`), + ) + expect(result.ok).toBe(true) + expect(observed).toStrictEqual([{ limit: 5 }]) + + const search = await Effect.runPromise(runtime.execute(`return (await search({ query: undefined })).items.length`)) + expect(search.ok).toBe(true) + }) + test("renders JSON Schema outputs and $defs references", async () => { const lookup = Tool.make({ description: "Look up a user", @@ -506,6 +572,26 @@ describe("CodeMode public contract", () => { expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(reusable)))).toStrictEqual(reusable) }) + test("a reused execution Effect starts from a clean slate", async () => { + const echo = Tool.make({ + description: "echo", + input: Schema.Struct({}), + output: Schema.Number, + run: () => Effect.succeed(1), + }) + const effect = CodeMode.execute({ + tools: { host: { echo } }, + code: `console.log("hi"); return await tools.host.echo({})`, + limits: { maxToolCalls: 1 }, + }) + const first = await Effect.runPromise(effect) + const second = await Effect.runPromise(effect) + // Per-execution state (tool-call budget and audit list, logs, timeout bookkeeping) must + // bind at run time, so the second run neither exhausts the budget nor leaks run 1's logs. + expect(first).toStrictEqual(second) + expect(second).toStrictEqual({ ok: true, value: 1, logs: ["hi"], toolCalls: [{ name: "host.echo" }] }) + }) + test("inlines a COMPLETE small catalog and keeps search registered but unadvertised", async () => { const runtime = CodeMode.make({ tools }) expect(runtime.catalog()).toStrictEqual([ @@ -521,11 +607,11 @@ describe("CodeMode public contract", () => { " - tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}> // Look up an order by ID", ) // A fully inlined catalog does not advertise search in the instructions... - expect(runtime.instructions()).not.toMatch(/\$codemode/) + expect(runtime.instructions()).not.toContain("search(") - // ...but the search tool stays registered, so a speculative call still works with the + // ...but the search built-in stays available, so a speculative call still works with the // same signature as the inline catalog. - const result = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({ query: "order" })`)) + const result = await Effect.runPromise(runtime.execute(`return search({ query: "order" })`)) expect(result.ok).toBe(true) if (result.ok) { expect(result.value).toStrictEqual({ @@ -563,9 +649,7 @@ describe("CodeMode public contract", () => { 'tools.context7["resolve-library-id"](input: {\n libraryName: string,\n}): Promise', ) - const search = await Effect.runPromise( - runtime.execute(`return await tools.$codemode.search({ query: "resolve library id" })`), - ) + const search = await Effect.runPromise(runtime.execute(`return search({ query: "resolve library id" })`)) expect(search.ok).toBe(true) if (search.ok) { expect(search.value).toStrictEqual({ @@ -588,7 +672,7 @@ describe("CodeMode public contract", () => { if (call.ok) expect(call.value).toBe("/resolved/TypeScript") const exact = await Effect.runPromise( - runtime.execute(`return await tools.$codemode.search({ query: 'tools.context7["resolve-library-id"]' })`), + runtime.execute(`return search({ query: 'tools.context7["resolve-library-id"]' })`), ) expect(exact.ok).toBe(true) if (exact.ok) expect(exact.value).toMatchObject({ remaining: 0, next: null }) @@ -612,7 +696,7 @@ describe("CodeMode public contract", () => { expect(instructions).toContain("Do not infer or normalize tool names") expect(instructions).toContain("bracket notation and quotes are part of the path") expect(instructions).toContain("surrounding agent tools are not available") - expect(instructions).toContain("Only Code Mode tools listed here and internal runtime tools") + expect(instructions).toContain("Only Code Mode tools listed here are available") // Placeholders use generic namespace/tool/field names only - no fabricated real tools // and no real catalog tools cherry-picked into example lines. expect(instructions).toContain("`const result = await tools..(input)`") @@ -631,15 +715,11 @@ describe("CodeMode public contract", () => { // PARTIAL: the workflow starts with search (with query-style guidance that is clearly // a query string, never a tool name) and the browse-namespace rule appears. expect(partial).toContain( - '1. If needed, discover tools: `return await tools.$codemode.search({ query: "" })`.', + '1. If needed, discover tools with the built-in search function: `return search({ query: "" })`.', ) expect(partial).toContain("In the next execution, copy a returned path exactly") - expect(partial).toContain( - "Only Code Mode tools listed here or returned by `tools.$codemode.search` and internal runtime tools", - ) - expect(partial).toContain( - '- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "" })`.', - ) + expect(partial).toContain("Only Code Mode tools listed here or returned by the built-in `search` function") + expect(partial).toContain('- Browse one namespace: `search({ query: "", namespace: "" })`.') expect(partial).toContain("repeat the same search with `offset: next.offset`") expect(partial).toContain(" limit?: number,\n offset?: number,") expect(partial).not.toContain("total_count") @@ -652,12 +732,17 @@ describe("CodeMode public contract", () => { expect(instructions).toContain("not a general-purpose runtime") expect(instructions).not.toContain("Standard modern JavaScript works") expect(instructions).not.toContain("TypeScript type annotations") - for (const missing of ["Modules/imports", "classes", "generators", "fetch", "promise chaining"]) { + for (const missing of ["Modules/imports", "classes", "generators", "fetch"]) { expect(instructions).toContain(missing) } + expect(instructions).not.toContain("new Promise(...) are unavailable") + expect(instructions).not.toContain("promise chaining") expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers") expect(instructions).not.toContain("host globals") expect(instructions).toContain("Use Code Mode tools for external operations") + expect(instructions).toContain( + "Prefer explicit `return`; otherwise only the final top-level expression becomes the result.", + ) expect(instructions).toContain( "Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.", ) @@ -671,7 +756,7 @@ describe("CodeMode public contract", () => { expect(instructions).toContain("## Available tools") expect(instructions).not.toContain("## Workflow") expect(instructions).not.toContain("## Rules") - expect(instructions).not.toMatch(/\$codemode/) + expect(instructions).not.toContain("search(") }) test("uses one ranked search returning complete definitions for large catalogs", async () => { @@ -691,17 +776,15 @@ describe("CodeMode public contract", () => { tools: { thread: { uploadFile: upload, generateImage: generate }, orders: { lookup } }, discovery: { catalogBudget: 0 }, }) - expect(runtime.instructions()).toContain( - "Available tools (PARTIAL - 0 of 3 shown; find the rest with tools.$codemode.search)", - ) + expect(runtime.instructions()).toContain("Available tools (PARTIAL - 0 of 3 shown; find the rest with search(...))") expect(runtime.instructions()).toContain("- thread (2 tools, none shown)") expect(runtime.instructions()).toContain("- orders (1 tool, none shown)") - expect(runtime.instructions()).toMatch(/\$codemode\.search/) + expect(runtime.instructions()).toContain("Search returns complete callable signatures:\n- search(input: {") expect(runtime.instructions()).not.toMatch(/tools\.thread\.uploadFile\(input/) const result = await Effect.runPromise( runtime.execute(` - return await tools.$codemode.search({ + return search({ query: "send message attachment upload file to current Discord thread", limit: 2 }) @@ -725,14 +808,14 @@ describe("CodeMode public contract", () => { remaining: 0, next: null, }) - expect(result.toolCalls).toStrictEqual([{ name: "$codemode.search" }]) + expect(result.toolCalls).toStrictEqual([{ name: "search" }]) const variants = await Effect.runPromise( runtime.execute(` - return await Promise.all([ - tools.$codemode.search({ query: "file" }), - tools.$codemode.search({ query: "image" }) - ]) + return [ + search({ query: "file" }), + search({ query: "image" }) + ] `), ) expect(variants.ok).toBe(true) @@ -744,12 +827,35 @@ describe("CodeMode public contract", () => { "tools.thread.generateImage", ) } + }) - const removed = await Effect.runPromise( - runtime.execute(`return await tools.$codemode.describe({ path: "thread.uploadFile" })`), - ) - expect(removed.ok).toBe(false) - if (!removed.ok) expect(removed.error.kind).toBe("UnknownTool") + test("search is a counted tool call: it burns maxToolCalls and fires the hooks", async () => { + const started: Array = [] + const ended: Array = [] + const limited = CodeMode.make({ + tools, + limits: { maxToolCalls: 1 }, + onToolCallStart: (call) => Effect.sync(() => void started.push(call.name)), + onToolCallEnd: (call) => Effect.sync(() => void ended.push(`${call.name}:${call.outcome}`)), + }) + const result = await Effect.runPromise(limited.execute(`search({}); return search({})`)) + expect(result.ok).toBe(false) + if (!result.ok) expect(result.error.kind).toBe("ToolCallLimitExceeded") + expect(started).toEqual(["search"]) + expect(ended).toEqual(["search:success"]) + }) + + test("search is an opaque, shadowable global like other built-ins", async () => { + const runtime = CodeMode.make({ tools }) + expect(await Effect.runPromise(runtime.execute(`return typeof search`))).toMatchObject({ value: "function" }) + // A program-level declaration shadows the global, as JS module scope does. + const shadowed = await Effect.runPromise(runtime.execute(`const search = () => "local"; return search()`)) + expect(shadowed.ok).toBe(true) + if (shadowed.ok) expect(shadowed.value).toBe("local") + // The reference itself cannot cross the data boundary. + const escaped = await Effect.runPromise(runtime.execute(`return { search }`)) + expect(escaped.ok).toBe(false) + if (!escaped.ok) expect(escaped.error.kind).toBe("InvalidDataValue") }) test("search defaults to 10 results and resolves exact tool paths", async () => { @@ -766,7 +872,7 @@ describe("CodeMode public contract", () => { }, }) - const browse = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({})`)) + const browse = await Effect.runPromise(runtime.execute(`return search({})`)) expect(browse.ok).toBe(true) if (browse.ok) { const value = browse.value as { @@ -780,9 +886,7 @@ describe("CodeMode public contract", () => { } for (const query of ["many.tool13", "tools.many.tool13"]) { - const exact = await Effect.runPromise( - runtime.execute(`return await tools.$codemode.search({ query: ${JSON.stringify(query)} })`), - ) + const exact = await Effect.runPromise(runtime.execute(`return search({ query: ${JSON.stringify(query)} })`)) expect(exact.ok).toBe(true) if (exact.ok) { expect(exact.value).toStrictEqual({ @@ -816,9 +920,7 @@ describe("CodeMode public contract", () => { }) // Empty query + namespace browses just that namespace, alphabetical by path. - const browse = await Effect.runPromise( - runtime.execute(`return await tools.$codemode.search({ query: "", namespace: "github" })`), - ) + const browse = await Effect.runPromise(runtime.execute(`return search({ query: "", namespace: "github" })`)) expect(browse.ok).toBe(true) if (browse.ok) { const value = browse.value as { items: Array<{ path: string }>; remaining: number } @@ -830,9 +932,7 @@ describe("CodeMode public contract", () => { } // A query + namespace ranks within that namespace only. - const scoped = await Effect.runPromise( - runtime.execute(`return await tools.$codemode.search({ query: "issues", namespace: "linear" })`), - ) + const scoped = await Effect.runPromise(runtime.execute(`return search({ query: "issues", namespace: "linear" })`)) expect(scoped.ok).toBe(true) if (scoped.ok) { const value = scoped.value as { items: Array<{ path: string }>; remaining: number } @@ -840,9 +940,7 @@ describe("CodeMode public contract", () => { expect(value.items[0]?.path).toBe("tools.linear.list_issues") } - const invalid = await Effect.runPromise( - runtime.execute(`return await tools.$codemode.search({ query: "issues", namespace: 7 })`), - ) + const invalid = await Effect.runPromise(runtime.execute(`return search({ query: "issues", namespace: 7 })`)) expect(invalid.ok).toBe(false) if (!invalid.ok) expect(invalid.error.kind).toBe("InvalidToolInput") }) @@ -867,9 +965,7 @@ describe("CodeMode public contract", () => { // "attachment" appears in neither path nor description - only in the input schema's // property names, which the searchable text includes. - const byParameter = await Effect.runPromise( - runtime.execute(`return await tools.$codemode.search({ query: "attachment" })`), - ) + const byParameter = await Effect.runPromise(runtime.execute(`return search({ query: "attachment" })`)) expect(byParameter.ok).toBe(true) if (byParameter.ok) { const value = byParameter.value as { items: Array<{ path: string }>; remaining: number } @@ -878,9 +974,7 @@ describe("CodeMode public contract", () => { } // Substring matching: a partial word ("docum") still hits the description. - const bySubstring = await Effect.runPromise( - runtime.execute(`return await tools.$codemode.search({ query: "docum" })`), - ) + const bySubstring = await Effect.runPromise(runtime.execute(`return search({ query: "docum" })`)) expect(bySubstring.ok).toBe(true) if (bySubstring.ok) { const value = bySubstring.value as { items: Array<{ path: string }>; remaining: number } @@ -907,9 +1001,7 @@ describe("CodeMode public contract", () => { }) // "issues" still finds the singular-only tool (term OR singular(term) per field)... - const plural = await Effect.runPromise( - runtime.execute(`return await tools.$codemode.search({ query: "issues", namespace: "tracker" })`), - ) + const plural = await Effect.runPromise(runtime.execute(`return search({ query: "issues", namespace: "tracker" })`)) expect(plural.ok).toBe(true) if (plural.ok) { const value = plural.value as { items: Array<{ path: string }>; remaining: number } @@ -918,7 +1010,7 @@ describe("CodeMode public contract", () => { } // ...while a true "issues" path match still outranks the singular-only description match. - const ranked = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({ query: "issues" })`)) + const ranked = await Effect.runPromise(runtime.execute(`return search({ query: "issues" })`)) expect(ranked.ok).toBe(true) if (ranked.ok) { const value = ranked.value as { items: Array<{ path: string }>; remaining: number } @@ -945,7 +1037,7 @@ describe("CodeMode public contract", () => { alpha: { beta: simple("Middle"), aardvark: simple("First") }, }, }) - const browse = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({})`)) + const browse = await Effect.runPromise(runtime.execute(`return search({})`)) expect(browse.ok).toBe(true) if (browse.ok) { const value = browse.value as { items: Array<{ path: string }>; remaining: number; next: unknown } @@ -958,9 +1050,7 @@ describe("CodeMode public contract", () => { expect(value.next).toBeNull() } - const middle = await Effect.runPromise( - runtime.execute(`return await tools.$codemode.search({ limit: 1, offset: 1 })`), - ) + const middle = await Effect.runPromise(runtime.execute(`return search({ limit: 1, offset: 1 })`)) expect(middle.ok).toBe(true) if (middle.ok) { expect(middle.value).toMatchObject({ @@ -970,9 +1060,7 @@ describe("CodeMode public contract", () => { }) } - const exhausted = await Effect.runPromise( - runtime.execute(`return await tools.$codemode.search({ limit: 1, offset: 3 })`), - ) + const exhausted = await Effect.runPromise(runtime.execute(`return search({ limit: 1, offset: 3 })`)) expect(exhausted.ok).toBe(true) if (exhausted.ok) expect(exhausted.value).toStrictEqual({ items: [], remaining: 0, next: null }) }) @@ -1003,16 +1091,14 @@ describe("CodeMode public contract", () => { }) const instructions = runtime.instructions() - expect(instructions).toContain( - "Available tools (PARTIAL - 2 of 3 shown; find the rest with tools.$codemode.search)", - ) + expect(instructions).toContain("Available tools (PARTIAL - 2 of 3 shown; find the rest with search(...))") expect(instructions).toContain("- alpha (2 tools, 1 shown)") expect(instructions).toContain(" - tools.alpha.cheap(input: {\n q: string,\n}): Promise // Cheap") expect(instructions).not.toContain("tools.alpha.expensive(") // Fully shown namespaces read cleanly (no "shown" annotation). expect(instructions).toContain("- beta (1 tool)") expect(instructions).toContain(" - tools.beta.cheap(input: {\n q: string,\n}): Promise // Cheap") - expect(instructions).toMatch(/\$codemode\.search/) + expect(instructions).toContain("Search returns complete callable signatures:\n- search(input: {") }) test("charges inline JSDoc against the catalog token budget", () => { @@ -1033,9 +1119,7 @@ describe("CodeMode public contract", () => { }) expect(runtime.catalog()[0]?.signature).toContain("/** A detailed identifier description.") - expect(runtime.instructions()).toContain( - "Available tools (PARTIAL - 0 of 1 shown; find the rest with tools.$codemode.search)", - ) + expect(runtime.instructions()).toContain("Available tools (PARTIAL - 0 of 1 shown; find the rest with search(...))") expect(runtime.instructions()).not.toContain("tools.records.lookup(input:") }) @@ -1081,6 +1165,24 @@ describe("CodeMode public contract", () => { expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result) }) + test("returns the final top-level expression when return is omitted", async () => { + const result = await Effect.runPromise(CodeMode.execute({ code: `1; 2` })) + + expect(result).toStrictEqual({ ok: true, value: 2, toolCalls: [] }) + }) + + test("does not implicitly return expressions nested in control flow", async () => { + const result = await Effect.runPromise(CodeMode.execute({ code: `if (true) { 2 }` })) + + expect(result).toStrictEqual({ ok: true, value: null, toolCalls: [] }) + }) + + test("returns null when the final top-level statement is not an expression", async () => { + const result = await Effect.runPromise(CodeMode.execute({ code: `1; const value = 2` })) + + expect(result).toStrictEqual({ ok: true, value: null, toolCalls: [] }) + }) + test("rejects invalid configuration and discovery limits", async () => { expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: 0 } })).toThrow(RangeError) expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: Number.POSITIVE_INFINITY } })).toThrow( @@ -1095,7 +1197,7 @@ describe("CodeMode public contract", () => { CodeMode.make({ tools, discovery: { catalogBudget: 0 }, - }).execute(`return await tools.$codemode.search({ query: "order", limit: 0.5 })`), + }).execute(`return search({ query: "order", limit: 0.5 })`), ) expect(result.ok).toBe(false) if (result.ok) return @@ -1103,9 +1205,7 @@ describe("CodeMode public contract", () => { for (const offset of [-1, 0.5, Number.MAX_SAFE_INTEGER + 1, "1"]) { const invalidOffset = await Effect.runPromise( - CodeMode.make({ tools }).execute( - `return await tools.$codemode.search({ query: "order", offset: ${JSON.stringify(offset)} })`, - ), + CodeMode.make({ tools }).execute(`return search({ query: "order", offset: ${JSON.stringify(offset)} })`), ) expect(invalidOffset.ok).toBe(false) if (!invalidOffset.ok) expect(invalidOffset.error.kind).toBe("InvalidToolInput") @@ -1156,8 +1256,4 @@ describe("CodeMode public contract", () => { } expect(elapsedMs).toBeLessThan(3_000) }) - - test("reserves the discovery namespace", () => { - expect(() => CodeMode.make({ tools: { $codemode: { lookup } } })).toThrow(/reserved for CodeMode discovery tools/) - }) }) diff --git a/packages/codemode/test/enumeration.test.ts b/packages/codemode/test/enumeration.test.ts index 0de3dc3ea0..116fbab0d9 100644 --- a/packages/codemode/test/enumeration.test.ts +++ b/packages/codemode/test/enumeration.test.ts @@ -3,7 +3,7 @@ import { Effect, Schema } from "effect" import { CodeMode, Tool } from "../src/index.js" // Key enumeration: Object.keys and for...in share one surface over plain objects, arrays -// (index strings), and tool references (namespace/tool names from the host tool tree), so a +// (index strings), and tool references (namespace/tool names from the supplied tools), so a // model can discover what it may call instead of guessing names from the instructions. The // motivating transcript: `Object.keys(tools)` failed with the generic plain-objects-only // message and `for (const key in tools)` was unsupported syntax, forcing blind guesses. @@ -41,7 +41,7 @@ describe("Object.keys over tool references", () => { const namespaces = Object.keys(tools) return { namespaces, count: namespaces.length } `), - ).toEqual({ namespaces: ["github", "memory", "playwright", "$codemode"], count: 4 }) + ).toEqual({ namespaces: ["github", "memory", "playwright"], count: 3 }) }) test("enumerates tool names at a nested namespace", async () => { @@ -52,15 +52,14 @@ describe("Object.keys over tool references", () => { expect(await value(`return Object.keys(tools.github.list_issues)`)).toEqual([]) }) - test("the internal discovery namespace enumerates its callable surface", async () => { - expect(await value(`return Object.keys(tools.$codemode)`)).toEqual(["search"]) + test("search is a global built-in function", async () => { + expect(await value(`return typeof search`)).toBe("function") }) - test("an unknown namespace is an UnknownTool error pointing at the discovery idioms", async () => { + test("an unknown namespace is an UnknownTool error", async () => { const failure = await error(`return Object.keys(tools.nonexistent)`) expect(failure.kind).toBe("UnknownTool") expect(failure.message).toContain("Unknown tool namespace 'nonexistent'") - expect(failure.suggestions?.join(" ")).toContain("Object.keys(tools)") }) test("Object.values/entries on a tool reference explain the working idioms", async () => { @@ -68,7 +67,7 @@ describe("Object.keys over tool references", () => { const failure = await error(`return Object.${method}(tools)`) expect(failure.kind).toBe("InvalidDataValue") expect(failure.message).toContain( - `Object.${method}(...) cannot read tool references: they are not plain data. Use Object.keys(tools) for names, or tools.$codemode.search({ query }) for signatures.`, + `Object.${method}(...) cannot read tool references: they are not plain data. Use Object.keys(tools) for names, or search({ query }) for signatures.`, ) } const nested = await error(`return Object.entries(tools.github)`) @@ -137,7 +136,7 @@ describe("for...in", () => { ).toBe("only") }) - test("enumerates namespaces and tools from the callable tool tree", async () => { + test("enumerates namespaces and tools from the supplied tools", async () => { expect( await value(` const names = [] @@ -146,7 +145,7 @@ describe("for...in", () => { } return names `), - ).toEqual(["github.list_issues", "github.get_issue", "memory.search", "playwright.navigate", "$codemode.search"]) + ).toEqual(["github.list_issues", "github.get_issue", "memory.search", "playwright.navigate"]) }) test("unsupported values fail with a hint at for...of and Object.keys", async () => { diff --git a/packages/codemode/test/fixtures/openapi-happy-path.json b/packages/codemode/test/fixtures/openapi-happy-path.json index 8052dd7391..dc3cbe5f8f 100644 --- a/packages/codemode/test/fixtures/openapi-happy-path.json +++ b/packages/codemode/test/fixtures/openapi-happy-path.json @@ -93,10 +93,16 @@ }, "role": { "type": "string", - "enum": ["admin", "member"] + "enum": [ + "admin", + "member" + ] } }, - "required": ["name", "email"], + "required": [ + "name", + "email" + ], "additionalProperties": false } } @@ -137,7 +143,9 @@ "type": "integer" } }, - "required": ["query"], + "required": [ + "query" + ], "additionalProperties": false } }, @@ -208,10 +216,17 @@ }, "role": { "type": "string", - "enum": ["admin", "member"] + "enum": [ + "admin", + "member" + ] } }, - "required": ["id", "name", "email"], + "required": [ + "id", + "name", + "email" + ], "additionalProperties": false } }, diff --git a/packages/codemode/test/fixtures/opencode-v2-openapi.json b/packages/codemode/test/fixtures/opencode-v2-openapi.json index c78194e669..543a418173 100644 --- a/packages/codemode/test/fixtures/opencode-v2-openapi.json +++ b/packages/codemode/test/fixtures/opencode-v2-openapi.json @@ -8,7 +8,9 @@ "paths": { "/api/health": { "get": { - "tags": ["server.health"], + "tags": [ + "health" + ], "operationId": "v2.health.get", "parameters": [], "security": [], @@ -22,10 +24,27 @@ "properties": { "healthy": { "type": "boolean", - "enum": [true] + "enum": [ + true + ] + }, + "version": { + "type": "string" + }, + "pid": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] } }, - "required": ["healthy"], + "required": [ + "healthy", + "version", + "pid" + ], "additionalProperties": false } } @@ -56,9 +75,67 @@ "summary": "Check server health" } }, + "/api/server": { + "get": { + "tags": [ + "server" + ], + "operationId": "v2.server.get", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "urls": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "urls" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Return the URLs that can be used to connect to this server.", + "summary": "Get server information" + } + }, "/api/location": { "get": { - "tags": ["server.location"], + "tags": [ + "location" + ], "operationId": "v2.location.get", "parameters": [ { @@ -141,7 +218,9 @@ }, "/api/agent": { "get": { - "tags": ["server.agent"], + "tags": [ + "agent" + ], "operationId": "v2.agent.list", "parameters": [ { @@ -200,11 +279,14 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/AgentV2.Info" + "$ref": "#/components/schemas/Agent.Info" } } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -237,7 +319,9 @@ }, "/api/plugin": { "get": { - "tags": ["plugins"], + "tags": [ + "plugin" + ], "operationId": "v2.plugin.list", "parameters": [ { @@ -300,7 +384,10 @@ } } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -333,7 +420,9 @@ }, "/api/session": { "get": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.list", "parameters": [ { @@ -379,7 +468,10 @@ "anyOf": [ { "type": "string", - "enum": ["asc", "desc"] + "enum": [ + "asc", + "desc" + ] }, { "type": "null" @@ -421,7 +513,9 @@ }, { "type": "string", - "enum": ["null"] + "enum": [ + "null" + ] } ], "description": "Filter by parent session. Use null to return only root sessions." @@ -542,7 +636,9 @@ "summary": "List sessions" }, "post": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.create", "parameters": [], "security": [], @@ -555,10 +651,12 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/SessionV2.Info" + "$ref": "#/components/schemas/Session.Info" } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -649,7 +747,9 @@ }, "/api/session/active": { "get": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.active", "parameters": [], "security": [], @@ -668,12 +768,11 @@ "$ref": "#/components/schemas/SessionActive" } } - }, - "watermarks": { - "$ref": "#/components/schemas/SessionWatermarks" } }, - "required": ["data", "watermarks"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -700,13 +799,15 @@ } } }, - "description": "Retrieve foreground Session drains currently owned by this OpenCode process. Sessions absent from the result are inactive. Watermarks are the durable log positions read alongside the activity snapshot; activity itself is process state, so the pairing is advisory rather than transactional.", + "description": "Retrieve foreground Session drains currently owned by this OpenCode process. Sessions absent from the result are inactive.", "summary": "List active sessions" } }, "/api/session/{sessionID}": { "get": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.get", "parameters": [ { @@ -733,10 +834,12 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/SessionV2.Info" + "$ref": "#/components/schemas/Session.Info" } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -782,11 +885,79 @@ }, "description": "Retrieve a session by ID.", "summary": "Get session" + }, + "delete": { + "tags": [ + "session" + ], + "operationId": "v2.session.remove", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "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", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Delete a session and its child sessions.", + "summary": "Delete session" } }, "/api/session/{sessionID}/fork": { "post": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.fork", "parameters": [ { @@ -813,10 +984,12 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/SessionV2.Info" + "$ref": "#/components/schemas/Session.Info" } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -897,7 +1070,9 @@ }, "/api/session/{sessionID}/agent": { "post": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.switchAgent", "parameters": [ { @@ -969,7 +1144,9 @@ "type": "string" } }, - "required": ["agent"], + "required": [ + "agent" + ], "additionalProperties": false } } @@ -980,7 +1157,9 @@ }, "/api/session/{sessionID}/model": { "post": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.switchModel", "parameters": [ { @@ -1052,7 +1231,9 @@ "$ref": "#/components/schemas/Model.Ref" } }, - "required": ["model"], + "required": [ + "model" + ], "additionalProperties": false } } @@ -1063,7 +1244,9 @@ }, "/api/session/{sessionID}/rename": { "post": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.rename", "parameters": [ { @@ -1135,7 +1318,122 @@ "type": "string" } }, - "required": ["title"], + "required": [ + "title" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/move": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.move", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Move a session to another project directory, optionally transferring local changes.", + "summary": "Move session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "destination": { + "type": "object", + "properties": { + "directory": { + "type": "string" + } + }, + "required": [ + "directory" + ], + "additionalProperties": false + }, + "moveChanges": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "destination" + ], "additionalProperties": false } } @@ -1146,7 +1444,9 @@ }, "/api/session/{sessionID}/prompt": { "post": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.prompt", "parameters": [ { @@ -1176,7 +1476,9 @@ "$ref": "#/components/schemas/SessionInput.Admitted" } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -1187,7 +1489,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] } } } @@ -1260,7 +1569,10 @@ "anyOf": [ { "type": "string", - "enum": ["steer", "queue"] + "enum": [ + "steer", + "queue" + ] }, { "type": "null" @@ -1278,7 +1590,9 @@ ] } }, - "required": ["prompt"], + "required": [ + "prompt" + ], "additionalProperties": false } } @@ -1289,7 +1603,9 @@ }, "/api/session/{sessionID}/command": { "post": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.command", "parameters": [ { @@ -1319,7 +1635,9 @@ "$ref": "#/components/schemas/SessionInput.Admitted" } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -1330,7 +1648,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvalidRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] } } } @@ -1458,7 +1783,10 @@ "anyOf": [ { "type": "string", - "enum": ["steer", "queue"] + "enum": [ + "steer", + "queue" + ] }, { "type": "null" @@ -1476,7 +1804,9 @@ ] } }, - "required": ["command"], + "required": [ + "command" + ], "additionalProperties": false } } @@ -1487,7 +1817,9 @@ }, "/api/session/{sessionID}/skill": { "post": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.skill", "parameters": [ { @@ -1587,7 +1919,9 @@ ] } }, - "required": ["skill"], + "required": [ + "skill" + ], "additionalProperties": false } } @@ -1598,7 +1932,9 @@ }, "/api/session/{sessionID}/synthetic": { "post": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.synthetic", "parameters": [ { @@ -1681,9 +2017,21 @@ }, "metadata": { "type": "object" + }, + "resume": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] } }, - "required": ["text"], + "required": [ + "text" + ], "additionalProperties": false } } @@ -1692,10 +2040,12 @@ } } }, - "/api/session/{sessionID}/compact": { + "/api/session/{sessionID}/shell": { "post": { - "tags": ["sessions"], - "operationId": "v2.session.compact", + "tags": [ + "session" + ], + "operationId": "v2.session.shell", "parameters": [ { "name": "sessionID", @@ -1736,6 +2086,124 @@ } } }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Execute one shell command in the session's working directory. Emits a shell.started event before execution and a shell.ended event with the merged output after.", + "summary": "Run shell command", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + { + "type": "null" + } + ] + }, + "command": { + "type": "string" + } + }, + "required": [ + "command" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/compact": { + "post": { + "tags": [ + "session" + ], + "operationId": "v2.session.compact", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SessionInput.Compaction" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "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", "content": { @@ -1754,43 +2222,53 @@ } }, "409": { - "description": "SessionBusyError", + "description": "ConflictError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionBusyError" - } - } - } - }, - "500": { - "description": "UnknownError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnknownError" - } - } - } - }, - "503": { - "description": "ServiceUnavailableError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServiceUnavailableError" + "$ref": "#/components/schemas/ConflictError" } } } } }, - "description": "Compact a session conversation.", - "summary": "Compact session" + "description": "Queue a durable session compaction request.", + "summary": "Compact session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } + }, + "required": true + } } }, "/api/session/{sessionID}/wait": { "post": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.wait", "parameters": [ { @@ -1866,7 +2344,9 @@ }, "/api/session/{sessionID}/revert/stage": { "post": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.revert.stage", "parameters": [ { @@ -1893,10 +2373,12 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/Revert.State" + "$ref": "#/components/schemas/Session.Revert" } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -1990,7 +2472,9 @@ ] } }, - "required": ["messageID"], + "required": [ + "messageID" + ], "additionalProperties": false } } @@ -2001,7 +2485,9 @@ }, "/api/session/{sessionID}/revert/clear": { "post": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.revert.clear", "parameters": [ { @@ -2086,7 +2572,9 @@ }, "/api/session/{sessionID}/revert/commit": { "post": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.revert.commit", "parameters": [ { @@ -2161,7 +2649,9 @@ }, "/api/session/{sessionID}/context": { "get": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.context", "parameters": [ { @@ -2190,11 +2680,13 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Session.Message" + "$ref": "#/components/schemas/Session.Message.Info" } } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -2252,10 +2744,12 @@ "summary": "Get session context" } }, - "/api/session/{sessionID}/context-entry": { + "/api/session/{sessionID}/instructions/entries": { "get": { - "tags": ["sessions"], - "operationId": "v2.session.context.entry.list", + "tags": [ + "session" + ], + "operationId": "v2.session.instructions.entry.list", "parameters": [ { "name": "sessionID", @@ -2283,11 +2777,13 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/SessionContextEntry.Info" + "$ref": "#/components/schemas/InstructionEntry.Info" } } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -2331,14 +2827,16 @@ } } }, - "description": "List API-managed context entries attached to the session's system context.", - "summary": "List context entries" + "description": "List API-managed instruction entries attached to the session.", + "summary": "List instruction entries" } }, - "/api/session/{sessionID}/context-entry/{key}": { + "/api/session/{sessionID}/instructions/entries/{key}": { "put": { - "tags": ["sessions"], - "operationId": "v2.session.context.entry.put", + "tags": [ + "session" + ], + "operationId": "v2.session.instructions.entry.put", "parameters": [ { "name": "sessionID", @@ -2357,7 +2855,7 @@ "name": "key", "in": "path", "schema": { - "$ref": "#/components/schemas/SessionContextEntry.Key" + "$ref": "#/components/schemas/InstructionEntry.Key" }, "required": true } @@ -2405,8 +2903,8 @@ } } }, - "description": "Attach or replace one durable context entry. The value is rendered into the session's system context; changes announce as updates at the next turn boundary.", - "summary": "Put context entry", + "description": "Attach or replace one durable instruction entry. Changes announce as updates at the next step boundary.", + "summary": "Put instruction entry", "requestBody": { "content": { "application/json": { @@ -2415,7 +2913,9 @@ "properties": { "value": {} }, - "required": ["value"], + "required": [ + "value" + ], "additionalProperties": false } } @@ -2424,8 +2924,10 @@ } }, "delete": { - "tags": ["sessions"], - "operationId": "v2.session.context.entry.remove", + "tags": [ + "session" + ], + "operationId": "v2.session.instructions.entry.remove", "parameters": [ { "name": "sessionID", @@ -2444,7 +2946,7 @@ "name": "key", "in": "path", "schema": { - "$ref": "#/components/schemas/SessionContextEntry.Key" + "$ref": "#/components/schemas/InstructionEntry.Key" }, "required": true } @@ -2492,13 +2994,15 @@ } } }, - "description": "Remove one context entry; the removal is announced to the model at the next turn boundary.", - "summary": "Remove context entry" + "description": "Remove one instruction entry; the removal is announced to the model at the next step boundary.", + "summary": "Remove instruction entry" } }, - "/api/session/{sessionID}/log": { + "/api/experimental/session/{sessionID}/log": { "get": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.log", "parameters": [ { @@ -2536,7 +3040,10 @@ "anyOf": [ { "type": "string", - "enum": ["true", "false"] + "enum": [ + "true", + "false" + ] }, { "type": "null" @@ -2572,7 +3079,11 @@ "$ref": "#/components/schemas/SessionLogItemStream" } }, - "required": ["id", "event", "data"], + "required": [ + "id", + "event", + "data" + ], "additionalProperties": false }, "x-effect-stream": { @@ -2586,13 +3097,18 @@ "properties": { "_tag": { "type": "string", - "enum": ["Fail"] + "enum": [ + "Fail" + ] }, "error": { "not": {} } }, - "required": ["_tag", "error"], + "required": [ + "_tag", + "error" + ], "additionalProperties": false }, { @@ -2600,11 +3116,16 @@ "properties": { "_tag": { "type": "string", - "enum": ["Die"] + "enum": [ + "Die" + ] }, "defect": {} }, - "required": ["_tag", "defect"], + "required": [ + "_tag", + "defect" + ], "additionalProperties": false }, { @@ -2612,7 +3133,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["Interrupt"] + "enum": [ + "Interrupt" + ] }, "fiberId": { "anyOf": [ @@ -2625,7 +3148,10 @@ ] } }, - "required": ["_tag", "fiberId"], + "required": [ + "_tag", + "fiberId" + ], "additionalProperties": false } ] @@ -2677,13 +3203,15 @@ } } }, - "description": "Durable, ordered, gap-free read of public session events after an exclusive aggregate sequence. Emits a synced marker once replay reaches the captured watermark, then completes; with follow=true it continues with live events instead. The only event API that promises reliability: attach after a snapshot watermark to compose fetch and stream without a race window.", + "description": "Experimental durable session event log. Reads events after an exclusive aggregate sequence and continues with live events when follow=true.", "summary": "Read the session log" } }, "/api/session/{sessionID}/interrupt": { "post": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.interrupt", "parameters": [ { @@ -2749,7 +3277,9 @@ }, "/api/session/{sessionID}/background": { "post": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.background", "parameters": [ { @@ -2815,7 +3345,9 @@ }, "/api/session/{sessionID}/message/{messageID}": { "get": { - "tags": ["sessions"], + "tags": [ + "session" + ], "operationId": "v2.session.message", "parameters": [ { @@ -2855,10 +3387,12 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/Session.Message" + "$ref": "#/components/schemas/Session.Message.Info" } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -2911,8 +3445,10 @@ }, "/api/session/{sessionID}/message": { "get": { - "tags": ["messages"], - "operationId": "v2.session.messages", + "tags": [ + "session" + ], + "operationId": "v2.message.list", "parameters": [ { "name": "sessionID", @@ -2950,7 +3486,10 @@ "anyOf": [ { "type": "string", - "enum": ["asc", "desc"] + "enum": [ + "asc", + "desc" + ] }, { "type": "null" @@ -3050,7 +3589,9 @@ }, "/api/model": { "get": { - "tags": ["models"], + "tags": [ + "model" + ], "operationId": "v2.model.list", "parameters": [ { @@ -3109,11 +3650,14 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/ModelV2.Info" + "$ref": "#/components/schemas/Model.Info" } } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -3156,7 +3700,9 @@ }, "/api/model/default": { "get": { - "tags": ["models"], + "tags": [ + "model" + ], "operationId": "v2.model.default", "parameters": [ { @@ -3215,7 +3761,7 @@ "data": { "anyOf": [ { - "$ref": "#/components/schemas/ModelV2.Info" + "$ref": "#/components/schemas/Model.Info" }, { "type": "null" @@ -3223,7 +3769,10 @@ ] } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -3266,7 +3815,9 @@ }, "/api/generate": { "post": { - "tags": ["generate"], + "tags": [ + "generate" + ], "operationId": "v2.generate.text", "parameters": [ { @@ -3382,7 +3933,9 @@ ] } }, - "required": ["prompt"], + "required": [ + "prompt" + ], "additionalProperties": false } } @@ -3393,7 +3946,9 @@ }, "/api/provider": { "get": { - "tags": ["providers"], + "tags": [ + "provider" + ], "operationId": "v2.provider.list", "parameters": [ { @@ -3456,7 +4011,10 @@ } } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -3499,7 +4057,9 @@ }, "/api/provider/{providerID}": { "get": { - "tags": ["providers"], + "tags": [ + "provider" + ], "operationId": "v2.provider.get", "parameters": [ { @@ -3567,7 +4127,10 @@ "$ref": "#/components/schemas/ProviderV2.Info" } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -3620,7 +4183,9 @@ }, "/api/integration": { "get": { - "tags": ["integrations"], + "tags": [ + "integration" + ], "operationId": "v2.integration.list", "parameters": [ { @@ -3683,7 +4248,10 @@ } } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -3716,7 +4284,9 @@ }, "/api/integration/{integrationID}": { "get": { - "tags": ["integrations"], + "tags": [ + "integration" + ], "operationId": "v2.integration.get", "parameters": [ { @@ -3791,7 +4361,10 @@ ] } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -3824,7 +4397,9 @@ }, "/api/integration/{integrationID}/connect/key": { "post": { - "tags": ["integrations"], + "tags": [ + "integration" + ], "operationId": "v2.integration.connect.key", "parameters": [ { @@ -3931,7 +4506,9 @@ ] } }, - "required": ["key"], + "required": [ + "key" + ], "additionalProperties": false } } @@ -3942,7 +4519,9 @@ }, "/api/integration/{integrationID}/connect/oauth": { "post": { - "tags": ["integrations"], + "tags": [ + "integration" + ], "operationId": "v2.integration.connect.oauth", "parameters": [ { @@ -4010,7 +4589,10 @@ "$ref": "#/components/schemas/Integration.Attempt" } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -4072,7 +4654,10 @@ ] } }, - "required": ["methodID", "inputs"], + "required": [ + "methodID", + "inputs" + ], "additionalProperties": false } } @@ -4083,7 +4668,9 @@ }, "/api/integration/attempt/{attemptID}": { "get": { - "tags": ["integrations"], + "tags": [ + "integration" + ], "operationId": "v2.integration.attempt.status", "parameters": [ { @@ -4151,7 +4738,10 @@ "$ref": "#/components/schemas/Integration.AttemptStatus" } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -4182,7 +4772,9 @@ "summary": "Get OAuth attempt status" }, "delete": { - "tags": ["integrations"], + "tags": [ + "integration" + ], "operationId": "v2.integration.attempt.cancel", "parameters": [ { @@ -4266,7 +4858,9 @@ }, "/api/integration/attempt/{attemptID}/complete": { "post": { - "tags": ["integrations"], + "tags": [ + "integration" + ], "operationId": "v2.integration.attempt.complete", "parameters": [ { @@ -4380,7 +4974,9 @@ }, "/api/mcp": { "get": { - "tags": ["mcp"], + "tags": [ + "mcp" + ], "operationId": "v2.mcp.list", "parameters": [ { @@ -4443,7 +5039,10 @@ } } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -4474,9 +5073,109 @@ "summary": "List MCP servers" } }, + "/api/mcp/resource": { + "get": { + "tags": [ + "mcp" + ], + "operationId": "v2.mcp.resource.catalog", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Mcp.ResourceCatalog" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve resources and resource templates from connected MCP servers.", + "summary": "List MCP resources" + } + }, "/api/credential/{credentialID}": { "patch": { - "tags": ["server.credential"], + "tags": [ + "credential" + ], "operationId": "v2.credential.update", "parameters": [ { @@ -4566,7 +5265,9 @@ "type": "string" } }, - "required": ["label"], + "required": [ + "label" + ], "additionalProperties": false } } @@ -4575,7 +5276,9 @@ } }, "delete": { - "tags": ["server.credential"], + "tags": [ + "credential" + ], "operationId": "v2.credential.remove", "parameters": [ { @@ -4657,9 +5360,58 @@ "summary": "Remove credential" } }, + "/api/project": { + "get": { + "tags": [ + "project" + ], + "operationId": "v2.project.list", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Project" + } + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "List known projects.", + "summary": "List projects" + } + }, "/api/project/current": { "get": { - "tags": ["projects"], + "tags": [ + "project" + ], "operationId": "v2.project.current", "parameters": [ { @@ -4742,7 +5494,9 @@ }, "/api/project/{projectID}/directories": { "get": { - "tags": ["projects"], + "tags": [ + "project" + ], "operationId": "v2.project.directories", "parameters": [ { @@ -4833,7 +5587,9 @@ }, "/api/form/request": { "get": { - "tags": ["forms"], + "tags": [ + "form" + ], "operationId": "v2.form.request.list", "parameters": [ { @@ -4903,7 +5659,10 @@ } } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -4936,7 +5695,9 @@ }, "/api/session/{sessionID}/form": { "get": { - "tags": ["forms"], + "tags": [ + "form" + ], "operationId": "v2.session.form.list", "parameters": [ { @@ -4971,7 +5732,9 @@ } } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -5019,7 +5782,9 @@ "summary": "List session forms" }, "post": { - "tags": ["forms"], + "tags": [ + "form" + ], "operationId": "v2.session.form.create", "parameters": [ { @@ -5051,7 +5816,9 @@ ] } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -5128,7 +5895,9 @@ }, "/api/session/{sessionID}/form/{formID}": { "get": { - "tags": ["forms"], + "tags": [ + "form" + ], "operationId": "v2.session.form.get", "parameters": [ { @@ -5173,7 +5942,9 @@ ] } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -5226,7 +5997,9 @@ }, "/api/session/{sessionID}/form/{formID}/state": { "get": { - "tags": ["forms"], + "tags": [ + "form" + ], "operationId": "v2.session.form.state", "parameters": [ { @@ -5264,7 +6037,9 @@ "$ref": "#/components/schemas/Form.State" } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -5317,7 +6092,9 @@ }, "/api/session/{sessionID}/form/{formID}/reply": { "post": { - "tags": ["forms"], + "tags": [ + "form" + ], "operationId": "v2.session.form.reply", "parameters": [ { @@ -5421,7 +6198,9 @@ }, "/api/session/{sessionID}/form/{formID}/cancel": { "post": { - "tags": ["forms"], + "tags": [ + "form" + ], "operationId": "v2.session.form.cancel", "parameters": [ { @@ -5508,7 +6287,9 @@ }, "/api/permission/request": { "get": { - "tags": ["permissions"], + "tags": [ + "permission" + ], "operationId": "v2.permission.request.list", "parameters": [ { @@ -5571,7 +6352,10 @@ } } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -5604,7 +6388,9 @@ }, "/api/permission/saved": { "get": { - "tags": ["permissions"], + "tags": [ + "permission" + ], "operationId": "v2.permission.saved.list", "parameters": [ { @@ -5639,7 +6425,9 @@ } } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -5672,7 +6460,9 @@ }, "/api/permission/saved/{id}": { "delete": { - "tags": ["permissions"], + "tags": [ + "permission" + ], "operationId": "v2.permission.saved.remove", "parameters": [ { @@ -5716,7 +6506,9 @@ }, "/api/session/{sessionID}/permission": { "post": { - "tags": ["permissions"], + "tags": [ + "permission" + ], "operationId": "v2.session.permission.create", "parameters": [ { @@ -5757,11 +6549,16 @@ "$ref": "#/components/schemas/PermissionV2.Effect" } }, - "required": ["id", "effect"], + "required": [ + "id", + "effect" + ], "additionalProperties": false } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -5860,7 +6657,10 @@ ] } }, - "required": ["action", "resources"], + "required": [ + "action", + "resources" + ], "additionalProperties": false } } @@ -5869,7 +6669,9 @@ } }, "get": { - "tags": ["permissions"], + "tags": [ + "permission" + ], "operationId": "v2.session.permission.list", "parameters": [ { @@ -5902,7 +6704,9 @@ } } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -5952,7 +6756,9 @@ }, "/api/session/{sessionID}/permission/{requestID}": { "get": { - "tags": ["permissions"], + "tags": [ + "permission" + ], "operationId": "v2.session.permission.get", "parameters": [ { @@ -5995,7 +6801,9 @@ "$ref": "#/components/schemas/PermissionV2.Request" } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -6048,7 +6856,9 @@ }, "/api/session/{sessionID}/permission/{requestID}/reply": { "post": { - "tags": ["permissions"], + "tags": [ + "permission" + ], "operationId": "v2.session.permission.reply", "parameters": [ { @@ -6146,7 +6956,9 @@ ] } }, - "required": ["reply"], + "required": [ + "reply" + ], "additionalProperties": false } } @@ -6157,7 +6969,9 @@ }, "/api/fs/read/*": { "get": { - "tags": ["filesystem"], + "tags": [ + "filesystem" + ], "operationId": "v2.fs.read", "parameters": [ { @@ -6241,7 +7055,9 @@ }, "/api/fs/list": { "get": { - "tags": ["filesystem"], + "tags": [ + "filesystem" + ], "operationId": "v2.fs.list", "parameters": [ { @@ -6319,7 +7135,10 @@ } } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -6352,7 +7171,9 @@ }, "/api/fs/find": { "get": { - "tags": ["filesystem"], + "tags": [ + "filesystem" + ], "operationId": "v2.fs.find", "parameters": [ { @@ -6408,7 +7229,10 @@ "in": "query", "schema": { "type": "string", - "enum": ["file", "directory"] + "enum": [ + "file", + "directory" + ] }, "required": false }, @@ -6447,7 +7271,10 @@ } } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -6480,7 +7307,9 @@ }, "/api/command": { "get": { - "tags": ["commands"], + "tags": [ + "command" + ], "operationId": "v2.command.list", "parameters": [ { @@ -6539,11 +7368,14 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/CommandV2.Info" + "$ref": "#/components/schemas/Command.Info" } } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -6576,7 +7408,9 @@ }, "/api/skill": { "get": { - "tags": ["skills"], + "tags": [ + "skill" + ], "operationId": "v2.skill.list", "parameters": [ { @@ -6635,11 +7469,14 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/SkillV2.Info" + "$ref": "#/components/schemas/Skill.Info" } } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -6672,7 +7509,9 @@ }, "/api/event": { "get": { - "tags": ["events"], + "tags": [ + "event" + ], "operationId": "v2.event.subscribe", "parameters": [], "security": [], @@ -6701,7 +7540,11 @@ "$ref": "#/components/schemas/V2EventStream" } }, - "required": ["id", "event", "data"], + "required": [ + "id", + "event", + "data" + ], "additionalProperties": false }, "x-effect-stream": { @@ -6715,13 +7558,18 @@ "properties": { "_tag": { "type": "string", - "enum": ["Fail"] + "enum": [ + "Fail" + ] }, "error": { "not": {} } }, - "required": ["_tag", "error"], + "required": [ + "_tag", + "error" + ], "additionalProperties": false }, { @@ -6729,11 +7577,16 @@ "properties": { "_tag": { "type": "string", - "enum": ["Die"] + "enum": [ + "Die" + ] }, "defect": {} }, - "required": ["_tag", "defect"], + "required": [ + "_tag", + "defect" + ], "additionalProperties": false }, { @@ -6741,7 +7594,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["Interrupt"] + "enum": [ + "Interrupt" + ] }, "fiberId": { "anyOf": [ @@ -6754,7 +7609,10 @@ ] } }, - "required": ["_tag", "fiberId"], + "required": [ + "_tag", + "fiberId" + ], "additionalProperties": false } ] @@ -6789,136 +7647,15 @@ } } }, - "description": "Subscribe to native event payloads for the server. Volatile by contract: a slow consumer overflows and fails the stream, and events during disconnection are missed. Consumers that need reliability should combine the changes feed with durable session log reads.", + "description": "Subscribe to native event payloads for the server. Volatile by contract: a slow consumer overflows and fails the stream, and events during disconnection are missed.", "summary": "Subscribe to events" } }, - "/api/event/changes": { - "get": { - "tags": ["events"], - "operationId": "v2.event.changes", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "text/event-stream": { - "schema": { - "type": "object", - "properties": { - "id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "event": { - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/EventLog.ChangeStream" - } - }, - "required": ["id", "event", "data"], - "additionalProperties": false - }, - "x-effect-stream": { - "encoding": "sse", - "causeSchema": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["Fail"] - }, - "error": { - "not": {} - } - }, - "required": ["_tag", "error"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["Die"] - }, - "defect": {} - }, - "required": ["_tag", "defect"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": ["Interrupt"] - }, - "fiberId": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ] - } - }, - "required": ["_tag", "fiberId"], - "additionalProperties": false - } - ] - } - }, - "errorSchema": { - "not": {} - }, - "failureEvent": "effect/httpapi/stream/failure" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Payload-free hint channel: after an event commits, a subscriber eventually receives a hint for that aggregate with seq at or beyond the event, or a sweep-required marker. Hints coalesce to the latest seq per aggregate under backpressure and the stream never fails from overflow. No consumer may derive correctness from receiving a hint; correctness always comes from durable log reads plus the consumer's own checkpoint. A sweep-required marker is emitted first on every (re)subscribe and whenever hint retention is exceeded: treat every aggregate as potentially dirty and recover via bounded sweep plus log reads.", - "summary": "Subscribe to change hints" - } - }, "/api/pty": { "get": { - "tags": ["pty"], + "tags": [ + "pty" + ], "operationId": "v2.pty.list", "parameters": [ { @@ -6981,7 +7718,10 @@ } } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -7012,7 +7752,9 @@ "summary": "List PTY sessions" }, "post": { - "tags": ["pty"], + "tags": [ + "pty" + ], "operationId": "v2.pty.create", "parameters": [ { @@ -7072,7 +7814,10 @@ "$ref": "#/components/schemas/Pty" } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -7139,7 +7884,9 @@ }, "/api/pty/{ptyID}": { "get": { - "tags": ["pty"], + "tags": [ + "pty" + ], "operationId": "v2.pty.get", "parameters": [ { @@ -7212,7 +7959,10 @@ "$ref": "#/components/schemas/Pty" } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -7253,7 +8003,9 @@ "summary": "Get PTY session" }, "put": { - "tags": ["pty"], + "tags": [ + "pty" + ], "operationId": "v2.pty.update", "parameters": [ { @@ -7326,7 +8078,10 @@ "$ref": "#/components/schemas/Pty" } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -7394,7 +8149,10 @@ ] } }, - "required": ["rows", "cols"], + "required": [ + "rows", + "cols" + ], "additionalProperties": false } }, @@ -7406,7 +8164,9 @@ } }, "delete": { - "tags": ["pty"], + "tags": [ + "pty" + ], "operationId": "v2.pty.remove", "parameters": [ { @@ -7505,8 +8265,10 @@ }, "/api/pty/{ptyID}/connect-token": { "post": { - "tags": ["pty"], - "operationId": "v2.pty.connectToken", + "tags": [ + "pty" + ], + "operationId": "v2.pty.connect.token", "parameters": [ { "name": "ptyID", @@ -7578,7 +8340,10 @@ "$ref": "#/components/schemas/PtyTicket.ConnectToken" } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -7631,9 +8396,10 @@ }, "/api/pty/{ptyID}/connect": { "get": { - "tags": ["pty"], + "tags": [ + "pty" + ], "operationId": "v2.pty.connect", - "x-websocket": true, "parameters": [ { "name": "ptyID", @@ -7731,12 +8497,15 @@ } }, "description": "Establish a WebSocket connection streaming PTY output and accepting terminal input.", - "summary": "Connect to PTY session" + "summary": "Connect to PTY session", + "x-websocket": true } }, "/api/shell": { "get": { - "tags": ["shell"], + "tags": [ + "shell" + ], "operationId": "v2.shell.list", "parameters": [ { @@ -7799,7 +8568,10 @@ } } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -7830,7 +8602,9 @@ "summary": "List running shell commands" }, "post": { - "tags": ["shell"], + "tags": [ + "shell" + ], "operationId": "v2.shell.create", "parameters": [ { @@ -7890,7 +8664,10 @@ "$ref": "#/components/schemas/Shell1" } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -7943,7 +8720,10 @@ "type": "object" } }, - "required": ["command"], + "required": [ + "command", + "timeout" + ], "additionalProperties": false } } @@ -7954,7 +8734,9 @@ }, "/api/shell/{id}": { "get": { - "tags": ["shell"], + "tags": [ + "shell" + ], "operationId": "v2.shell.get", "parameters": [ { @@ -8027,7 +8809,10 @@ "$ref": "#/components/schemas/Shell1" } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -8068,7 +8853,9 @@ "summary": "Get shell command" }, "delete": { - "tags": ["shell"], + "tags": [ + "shell" + ], "operationId": "v2.shell.remove", "parameters": [ { @@ -8165,9 +8952,156 @@ "summary": "Remove shell command" } }, + "/api/shell/{id}/timeout": { + "patch": { + "tags": [ + "shell" + ], + "operationId": "v2.shell.timeout", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Shell1" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "ShellNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShellNotFoundError" + } + } + } + } + }, + "description": "Replace a running shell command's timeout from now, or clear it with zero.", + "summary": "Update shell timeout", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "timeout": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "timeout" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, "/api/shell/{id}/output": { "get": { - "tags": ["shell"], + "tags": [ + "shell" + ], "operationId": "v2.shell.output", "parameters": [ { @@ -8288,11 +9222,19 @@ "type": "boolean" } }, - "required": ["output", "cursor", "size", "truncated"], + "required": [ + "output", + "cursor", + "size", + "truncated" + ], "additionalProperties": false } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -8335,7 +9277,9 @@ }, "/api/question/request": { "get": { - "tags": ["session questions"], + "tags": [ + "question" + ], "operationId": "v2.question.request.list", "parameters": [ { @@ -8398,7 +9342,10 @@ } } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -8431,7 +9378,9 @@ }, "/api/session/{sessionID}/question": { "get": { - "tags": ["session questions"], + "tags": [ + "question" + ], "operationId": "v2.session.question.list", "parameters": [ { @@ -8464,7 +9413,9 @@ } } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false } } @@ -8514,7 +9465,9 @@ }, "/api/session/{sessionID}/question/{requestID}/reply": { "post": { - "tags": ["session questions"], + "tags": [ + "question" + ], "operationId": "v2.session.question.reply", "parameters": [ { @@ -8606,7 +9559,9 @@ }, "/api/session/{sessionID}/question/{requestID}/reject": { "post": { - "tags": ["session questions"], + "tags": [ + "question" + ], "operationId": "v2.session.question.reject", "parameters": [ { @@ -8688,7 +9643,9 @@ }, "/api/reference": { "get": { - "tags": ["reference"], + "tags": [ + "reference" + ], "operationId": "v2.reference.list", "parameters": [ { @@ -8751,7 +9708,10 @@ } } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -8784,7 +9744,9 @@ }, "/experimental/project/{projectID}/copy": { "post": { - "tags": ["projectCopy"], + "tags": [ + "projectCopy" + ], "operationId": "v2.projectCopy.create", "parameters": [ { @@ -8892,7 +9854,10 @@ "type": "string" } }, - "required": ["strategy", "directory"], + "required": [ + "strategy", + "directory" + ], "additionalProperties": false } } @@ -8901,7 +9866,9 @@ } }, "delete": { - "tags": ["projectCopy"], + "tags": [ + "projectCopy" + ], "operationId": "v2.projectCopy.remove", "parameters": [ { @@ -8999,7 +9966,10 @@ "type": "boolean" } }, - "required": ["directory", "force"], + "required": [ + "directory", + "force" + ], "additionalProperties": false } } @@ -9010,7 +9980,9 @@ }, "/experimental/project/{projectID}/copy/refresh": { "post": { - "tags": ["projectCopy"], + "tags": [ + "projectCopy" + ], "operationId": "v2.projectCopy.refresh", "parameters": [ { @@ -9099,7 +10071,9 @@ }, "/api/vcs/status": { "get": { - "tags": ["vcs"], + "tags": [ + "vcs" + ], "operationId": "v2.vcs.status", "parameters": [ { @@ -9162,7 +10136,10 @@ } } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -9195,7 +10172,9 @@ }, "/api/vcs/diff": { "get": { - "tags": ["vcs"], + "tags": [ + "vcs" + ], "operationId": "v2.vcs.diff", "parameters": [ { @@ -9277,11 +10256,14 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" + "$ref": "#/components/schemas/FileDiff.Info" } } }, - "required": ["location", "data"], + "required": [ + "location", + "data" + ], "additionalProperties": false } } @@ -9311,6 +10293,129 @@ "description": "Diff the working copy against HEAD (mode git) or the default-branch merge base (mode branch) for the requested location.", "summary": "VCS diff" } + }, + "/api/debug/location": { + "get": { + "tags": [ + "debug" + ], + "operationId": "v2.debug.location.list", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Location.Ref" + } + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "List locations currently loaded by the server.", + "summary": "List loaded locations" + }, + "delete": { + "tags": [ + "debug" + ], + "operationId": "v2.debug.location.evict", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": 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" + } + } + } + } + }, + "description": "Dispose the requested location's cached services so its next use boots them fresh.", + "summary": "Evict a loaded location" + } } }, "components": { @@ -9320,13 +10425,18 @@ "properties": { "_tag": { "type": "string", - "enum": ["UnauthorizedError"] + "enum": [ + "UnauthorizedError" + ] }, "message": { "type": "string" } }, - "required": ["_tag", "message"], + "required": [ + "_tag", + "message" + ], "additionalProperties": false }, "InvalidRequestError": { @@ -9334,7 +10444,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["InvalidRequestError"] + "enum": [ + "InvalidRequestError" + ] }, "message": { "type": "string" @@ -9360,7 +10472,10 @@ ] } }, - "required": ["_tag", "message"], + "required": [ + "_tag", + "message" + ], "additionalProperties": false }, "Location.Info": { @@ -9387,11 +10502,17 @@ "type": "string" } }, - "required": ["id", "directory"], + "required": [ + "id", + "directory" + ], "additionalProperties": false } }, - "required": ["directory", "project"], + "required": [ + "directory", + "project" + ], "additionalProperties": false }, "Model.Ref": { @@ -9407,7 +10528,10 @@ "type": "string" } }, - "required": ["id", "providerID"], + "required": [ + "id", + "providerID" + ], "additionalProperties": false }, "Provider.Settings": { @@ -9429,7 +10553,11 @@ "type": "object" } }, - "required": ["settings", "headers", "body"], + "required": [ + "settings", + "headers", + "body" + ], "additionalProperties": false }, "Agent.Color": { @@ -9444,13 +10572,25 @@ }, { "type": "string", - "enum": ["primary", "secondary", "accent", "success", "warning", "error", "info"] + "enum": [ + "primary", + "secondary", + "accent", + "success", + "warning", + "error", + "info" + ] } ] }, "PermissionV2.Effect": { "type": "string", - "enum": ["allow", "deny", "ask"] + "enum": [ + "allow", + "deny", + "ask" + ] }, "PermissionV2.Rule": { "type": "object", @@ -9465,7 +10605,11 @@ "$ref": "#/components/schemas/PermissionV2.Effect" } }, - "required": ["action", "resource", "effect"], + "required": [ + "action", + "resource", + "effect" + ], "additionalProperties": false }, "PermissionV2.Ruleset": { @@ -9474,12 +10618,15 @@ "$ref": "#/components/schemas/PermissionV2.Rule" } }, - "AgentV2.Info": { + "Agent.Info": { "type": "object", "properties": { "id": { "type": "string" }, + "name": { + "type": "string" + }, "model": { "$ref": "#/components/schemas/Model.Ref" }, @@ -9494,7 +10641,11 @@ }, "mode": { "type": "string", - "enum": ["subagent", "primary", "all"] + "enum": [ + "subagent", + "primary", + "all" + ] }, "hidden": { "type": "boolean" @@ -9514,7 +10665,14 @@ "$ref": "#/components/schemas/PermissionV2.Ruleset" } }, - "required": ["id", "request", "mode", "hidden", "permissions"], + "required": [ + "id", + "name", + "request", + "mode", + "hidden", + "permissions" + ], "additionalProperties": false }, "Plugin.Info": { @@ -9524,7 +10682,49 @@ "type": "string" } }, - "required": ["id"], + "required": [ + "id" + ], + "additionalProperties": false + }, + "Money.USD": { + "type": "number" + }, + "TokenUsage.Info": { + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "reasoning": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": [ + "read", + "write" + ], + "additionalProperties": false + } + }, + "required": [ + "input", + "output", + "reasoning", + "cache" + ], "additionalProperties": false }, "Location.Ref": { @@ -9542,18 +10742,19 @@ ] } }, - "required": ["directory"], + "required": [ + "directory" + ], "additionalProperties": false }, - "File.Diff": { + "FileDiff.Info": { "type": "object", "properties": { - "path": { + "file": { "type": "string" }, - "status": { - "type": "string", - "enum": ["added", "modified", "deleted"] + "patch": { + "type": "string" }, "additions": { "type": "integer", @@ -9571,14 +10772,25 @@ } ] }, - "patch": { - "type": "string" + "status": { + "type": "string", + "enum": [ + "added", + "deleted", + "modified" + ] } }, - "required": ["path", "status", "additions", "deletions", "patch"], + "required": [ + "file", + "patch", + "additions", + "deletions", + "status" + ], "additionalProperties": false }, - "Revert.State": { + "Session.Revert": { "type": "object", "properties": { "messageID": { @@ -9595,20 +10807,19 @@ "snapshot": { "type": "string" }, - "diff": { - "type": "string" - }, "files": { "type": "array", "items": { - "$ref": "#/components/schemas/File.Diff" + "$ref": "#/components/schemas/FileDiff.Info" } } }, - "required": ["messageID"], + "required": [ + "messageID" + ], "additionalProperties": false }, - "SessionV2.Info": { + "Session.Info": { "type": "object", "properties": { "id": { @@ -9627,6 +10838,31 @@ } ] }, + "fork": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "sessionID" + ], + "additionalProperties": false + }, "projectID": { "type": "string" }, @@ -9637,36 +10873,10 @@ "$ref": "#/components/schemas/Model.Ref" }, "cost": { - "type": "number" + "$ref": "#/components/schemas/Money.USD" }, "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false + "$ref": "#/components/schemas/TokenUsage.Info" }, "time": { "type": "object", @@ -9681,7 +10891,10 @@ "type": "number" } }, - "required": ["created", "updated"], + "required": [ + "created", + "updated" + ], "additionalProperties": false }, "title": { @@ -9694,38 +10907,29 @@ "type": "string" }, "revert": { - "$ref": "#/components/schemas/Revert.State" + "$ref": "#/components/schemas/Session.Revert" } }, - "required": ["id", "projectID", "cost", "tokens", "time", "title", "location"], + "required": [ + "id", + "projectID", + "cost", + "tokens", + "time", + "title", + "location" + ], "additionalProperties": false }, - "SessionWatermarks": { - "type": "object", - "patternProperties": { - "^ses": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - } - }, - "description": "Durable log seq each session's snapshot was computed at. Attach a live log read after the watermark to compose fetch and stream gap-free; apply a snapshot only where its watermark is at or beyond already-applied events. Sessions without durable events are absent." - }, "SessionsResponse": { "type": "object", "properties": { "data": { "type": "array", "items": { - "$ref": "#/components/schemas/SessionV2.Info" + "$ref": "#/components/schemas/Session.Info" } }, - "watermarks": { - "$ref": "#/components/schemas/SessionWatermarks" - }, "cursor": { "type": "object", "properties": { @@ -9753,7 +10957,10 @@ "additionalProperties": false } }, - "required": ["data", "watermarks", "cursor"], + "required": [ + "data", + "cursor" + ], "additionalProperties": false }, "InvalidCursorError": { @@ -9761,13 +10968,18 @@ "properties": { "_tag": { "type": "string", - "enum": ["InvalidCursorError"] + "enum": [ + "InvalidCursorError" + ] }, "message": { "type": "string" } }, - "required": ["_tag", "message"], + "required": [ + "_tag", + "message" + ], "additionalProperties": false }, "InvalidRequestError1": { @@ -9775,7 +10987,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["InvalidRequestError"] + "enum": [ + "InvalidRequestError" + ] }, "message": { "type": "string" @@ -9801,7 +11015,10 @@ ] } }, - "required": ["_tag", "message"], + "required": [ + "_tag", + "message" + ], "additionalProperties": false }, "SessionActive": { @@ -9809,10 +11026,14 @@ "properties": { "type": { "type": "string", - "enum": ["running"] + "enum": [ + "running" + ] } }, - "required": ["type"], + "required": [ + "type" + ], "additionalProperties": false }, "SessionNotFoundError": { @@ -9820,7 +11041,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["SessionNotFoundError"] + "enum": [ + "SessionNotFoundError" + ] }, "sessionID": { "type": "string" @@ -9829,7 +11052,11 @@ "type": "string" } }, - "required": ["_tag", "sessionID", "message"], + "required": [ + "_tag", + "sessionID", + "message" + ], "additionalProperties": false }, "MessageNotFoundError": { @@ -9837,7 +11064,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["MessageNotFoundError"] + "enum": [ + "MessageNotFoundError" + ] }, "sessionID": { "type": "string" @@ -9849,10 +11078,15 @@ "type": "string" } }, - "required": ["_tag", "sessionID", "messageID", "message"], + "required": [ + "_tag", + "sessionID", + "messageID", + "message" + ], "additionalProperties": false }, - "Prompt.Source": { + "Prompt.Mention": { "type": "object", "properties": { "start": { @@ -9865,7 +11099,11 @@ "type": "string" } }, - "required": ["start", "end", "text"], + "required": [ + "start", + "end", + "text" + ], "additionalProperties": false }, "PromptInput.FileAttachment": { @@ -9880,11 +11118,13 @@ "description": { "type": "string" }, - "source": { - "$ref": "#/components/schemas/Prompt.Source" + "mention": { + "$ref": "#/components/schemas/Prompt.Mention" } }, - "required": ["uri"], + "required": [ + "uri" + ], "additionalProperties": false }, "Prompt.AgentAttachment": { @@ -9893,11 +11133,13 @@ "name": { "type": "string" }, - "source": { - "$ref": "#/components/schemas/Prompt.Source" + "mention": { + "$ref": "#/components/schemas/Prompt.Mention" } }, - "required": ["name"], + "required": [ + "name" + ], "additionalProperties": false }, "PromptInput": { @@ -9919,29 +11161,84 @@ } } }, - "required": ["text"], + "required": [ + "text" + ], "additionalProperties": false }, + "Prompt.Base64": { + "type": "string", + "allOf": [ + { + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$" + } + ] + }, + "Prompt.FileSource": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "inline" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "uri" + ] + }, + "uri": { + "type": "string" + } + }, + "required": [ + "type", + "uri" + ], + "additionalProperties": false + } + ] + }, "Prompt.FileAttachment": { "type": "object", "properties": { - "uri": { - "type": "string" + "data": { + "$ref": "#/components/schemas/Prompt.Base64" }, "mime": { "type": "string" }, + "source": { + "$ref": "#/components/schemas/Prompt.FileSource" + }, "name": { "type": "string" }, "description": { "type": "string" }, - "source": { - "$ref": "#/components/schemas/Prompt.Source" + "mention": { + "$ref": "#/components/schemas/Prompt.Mention" } }, - "required": ["uri", "mime"], + "required": [ + "data", + "mime", + "source" + ], "additionalProperties": false }, "Prompt": { @@ -9963,7 +11260,9 @@ } } }, - "required": ["text"], + "required": [ + "text" + ], "additionalProperties": false }, "SessionInput.Admitted": { @@ -9998,7 +11297,10 @@ }, "delivery": { "type": "string", - "enum": ["steer", "queue"] + "enum": [ + "steer", + "queue" + ] }, "timeCreated": { "type": "number" @@ -10012,7 +11314,14 @@ ] } }, - "required": ["admittedSeq", "id", "sessionID", "prompt", "delivery", "timeCreated"], + "required": [ + "admittedSeq", + "id", + "sessionID", + "prompt", + "delivery", + "timeCreated" + ], "additionalProperties": false }, "ConflictError": { @@ -10020,7 +11329,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["ConflictError"] + "enum": [ + "ConflictError" + ] }, "message": { "type": "string" @@ -10036,7 +11347,10 @@ ] } }, - "required": ["_tag", "message"], + "required": [ + "_tag", + "message" + ], "additionalProperties": false }, "CommandNotFoundError": { @@ -10044,7 +11358,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["CommandNotFoundError"] + "enum": [ + "CommandNotFoundError" + ] }, "command": { "type": "string" @@ -10053,7 +11369,11 @@ "type": "string" } }, - "required": ["_tag", "command", "message"], + "required": [ + "_tag", + "command", + "message" + ], "additionalProperties": false }, "CommandEvaluationError": { @@ -10061,7 +11381,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["CommandEvaluationError"] + "enum": [ + "CommandEvaluationError" + ] }, "command": { "type": "string" @@ -10070,7 +11392,11 @@ "type": "string" } }, - "required": ["_tag", "command", "message"], + "required": [ + "_tag", + "command", + "message" + ], "additionalProperties": false }, "SkillNotFoundError": { @@ -10078,7 +11404,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["SkillNotFoundError"] + "enum": [ + "SkillNotFoundError" + ] }, "skill": { "type": "string" @@ -10087,24 +11415,65 @@ "type": "string" } }, - "required": ["_tag", "skill", "message"], + "required": [ + "_tag", + "skill", + "message" + ], "additionalProperties": false }, - "SessionBusyError": { + "SessionInput.Compaction": { "type": "object", "properties": { - "_tag": { + "type": { "type": "string", - "enum": ["SessionBusyError"] + "enum": [ + "compaction" + ] + }, + "admittedSeq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] }, "sessionID": { - "type": "string" + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, - "message": { - "type": "string" + "timeCreated": { + "type": "number" + }, + "handledSeq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] } }, - "required": ["_tag", "sessionID", "message"], + "required": [ + "type", + "admittedSeq", + "id", + "sessionID", + "timeCreated" + ], "additionalProperties": false }, "ServiceUnavailableError": { @@ -10112,7 +11481,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["ServiceUnavailableError"] + "enum": [ + "ServiceUnavailableError" + ] }, "message": { "type": "string" @@ -10128,7 +11499,33 @@ ] } }, - "required": ["_tag", "message"], + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "SessionBusyError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "SessionBusyError" + ] + }, + "sessionID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "sessionID", + "message" + ], "additionalProperties": false }, "UnknownError": { @@ -10136,7 +11533,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["UnknownError"] + "enum": [ + "UnknownError" + ] }, "message": { "type": "string" @@ -10152,10 +11551,13 @@ ] } }, - "required": ["_tag", "message"], + "required": [ + "_tag", + "message" + ], "additionalProperties": false }, - "Session.Message.AgentSwitched": { + "Session.Message.AgentSelected": { "type": "object", "properties": { "id": { @@ -10176,21 +11578,30 @@ "type": "number" } }, - "required": ["created"], + "required": [ + "created" + ], "additionalProperties": false }, "type": { "type": "string", - "enum": ["agent-switched"] + "enum": [ + "agent-switched" + ] }, "agent": { "type": "string" } }, - "required": ["id", "time", "type", "agent"], + "required": [ + "id", + "time", + "type", + "agent" + ], "additionalProperties": false }, - "Session.Message.ModelSwitched": { + "Session.Message.ModelSelected": { "type": "object", "properties": { "id": { @@ -10211,18 +11622,30 @@ "type": "number" } }, - "required": ["created"], + "required": [ + "created" + ], "additionalProperties": false }, "type": { "type": "string", - "enum": ["model-switched"] + "enum": [ + "model-switched" + ] }, "model": { "$ref": "#/components/schemas/Model.Ref" + }, + "previous": { + "$ref": "#/components/schemas/Model.Ref" } }, - "required": ["id", "time", "type", "model"], + "required": [ + "id", + "time", + "type", + "model" + ], "additionalProperties": false }, "Session.Message.User": { @@ -10246,7 +11669,9 @@ "type": "number" } }, - "required": ["created"], + "required": [ + "created" + ], "additionalProperties": false }, "text": { @@ -10266,10 +11691,17 @@ }, "type": { "type": "string", - "enum": ["user"] + "enum": [ + "user" + ] } }, - "required": ["id", "time", "text", "type"], + "required": [ + "id", + "time", + "text", + "type" + ], "additionalProperties": false }, "Session.Message.Synthetic": { @@ -10293,17 +11725,11 @@ "type": "number" } }, - "required": ["created"], + "required": [ + "created" + ], "additionalProperties": false }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, "text": { "type": "string" }, @@ -10312,10 +11738,17 @@ }, "type": { "type": "string", - "enum": ["synthetic"] + "enum": [ + "synthetic" + ] } }, - "required": ["id", "time", "sessionID", "text", "type"], + "required": [ + "id", + "time", + "text", + "type" + ], "additionalProperties": false }, "Session.Message.System": { @@ -10339,18 +11772,27 @@ "type": "number" } }, - "required": ["created"], + "required": [ + "created" + ], "additionalProperties": false }, "type": { "type": "string", - "enum": ["system"] + "enum": [ + "system" + ] }, "text": { "type": "string" } }, - "required": ["id", "time", "type", "text"], + "required": [ + "id", + "time", + "type", + "text" + ], "additionalProperties": false }, "Session.Message.Skill": { @@ -10374,12 +11816,19 @@ "type": "number" } }, - "required": ["created"], + "required": [ + "created" + ], "additionalProperties": false }, "type": { "type": "string", - "enum": ["skill"] + "enum": [ + "skill" + ] + }, + "skill": { + "type": "string" }, "name": { "type": "string" @@ -10388,7 +11837,14 @@ "type": "string" } }, - "required": ["id", "time", "type", "name", "text"], + "required": [ + "id", + "time", + "type", + "skill", + "name", + "text" + ], "additionalProperties": false }, "Session.Message.Shell": { @@ -10415,24 +11871,117 @@ "type": "number" } }, - "required": ["created"], + "required": [ + "created" + ], "additionalProperties": false }, "type": { "type": "string", - "enum": ["shell"] + "enum": [ + "shell" + ] }, - "callID": { - "type": "string" + "shellID": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] }, "command": { "type": "string" }, + "status": { + "type": "string", + "enum": [ + "running", + "exited", + "timeout", + "killed" + ] + }, + "exit": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, "output": { - "type": "string" + "type": "object", + "properties": { + "output": { + "type": "string" + }, + "cursor": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "size": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "truncated": { + "type": "boolean" + } + }, + "required": [ + "output", + "cursor", + "size", + "truncated" + ], + "additionalProperties": false } }, - "required": ["id", "time", "type", "callID", "command", "output"], + "required": [ + "id", + "time", + "type", + "shellID", + "command", + "status" + ], "additionalProperties": false }, "Session.Message.Assistant.Text": { @@ -10440,39 +11989,37 @@ "properties": { "type": { "type": "string", - "enum": ["text"] - }, - "id": { - "type": "string" + "enum": [ + "text" + ] }, "text": { "type": "string" } }, - "required": ["type", "id", "text"], + "required": [ + "type", + "text" + ], "additionalProperties": false }, - "LLM.ProviderMetadata": { - "type": "object", - "additionalProperties": { - "type": "object" - } + "Session.Message.ProviderState": { + "type": "object" }, "Session.Message.Assistant.Reasoning": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["reasoning"] - }, - "id": { - "type": "string" + "enum": [ + "reasoning" + ] }, "text": { "type": "string" }, - "providerMetadata": { - "$ref": "#/components/schemas/LLM.ProviderMetadata" + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState" }, "time": { "type": "object", @@ -10484,25 +12031,35 @@ "type": "number" } }, - "required": ["created"], + "required": [ + "created" + ], "additionalProperties": false } }, - "required": ["type", "id", "text"], + "required": [ + "type", + "text" + ], "additionalProperties": false }, - "Session.Message.ToolState.Pending": { + "Session.Message.ToolState.Streaming": { "type": "object", "properties": { "status": { "type": "string", - "enum": ["pending"] + "enum": [ + "streaming" + ] }, "input": { "type": "string" } }, - "required": ["status", "input"], + "required": [ + "status", + "input" + ], "additionalProperties": false }, "Tool.TextContent": { @@ -10510,13 +12067,18 @@ "properties": { "type": { "type": "string", - "enum": ["text"] + "enum": [ + "text" + ] }, "text": { "type": "string" } }, - "required": ["type", "text"], + "required": [ + "type", + "text" + ], "additionalProperties": false }, "Tool.FileContent": { @@ -10524,7 +12086,9 @@ "properties": { "type": { "type": "string", - "enum": ["file"] + "enum": [ + "file" + ] }, "uri": { "type": "string" @@ -10536,7 +12100,11 @@ "type": "string" } }, - "required": ["type", "uri", "mime"], + "required": [ + "type", + "uri", + "mime" + ], "additionalProperties": false }, "LLM.ToolContent": { @@ -10554,7 +12122,9 @@ "properties": { "status": { "type": "string", - "enum": ["running"] + "enum": [ + "running" + ] }, "input": { "type": "object" @@ -10569,7 +12139,12 @@ } } }, - "required": ["status", "input", "structured", "content"], + "required": [ + "status", + "input", + "structured", + "content" + ], "additionalProperties": false }, "Session.Message.ToolState.Completed": { @@ -10577,49 +12152,46 @@ "properties": { "status": { "type": "string", - "enum": ["completed"] + "enum": [ + "completed" + ] }, "input": { "type": "object" }, - "attachments": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Prompt.FileAttachment" - } - }, "content": { "type": "array", "items": { "$ref": "#/components/schemas/LLM.ToolContent" } }, - "outputPaths": { - "type": "array", - "items": { - "type": "string" - } - }, "structured": { "type": "object" }, "result": {} }, - "required": ["status", "input", "content", "structured"], + "required": [ + "status", + "input", + "content", + "structured" + ], "additionalProperties": false }, - "Session.Error.Unknown": { + "Session.StructuredError": { "type": "object", "properties": { "type": { - "type": "string", - "enum": ["unknown"] + "type": "string" }, "message": { "type": "string" } }, - "required": ["type", "message"], + "required": [ + "type", + "message" + ], "additionalProperties": false }, "Session.Message.ToolState.Error": { @@ -10627,7 +12199,9 @@ "properties": { "status": { "type": "string", - "enum": ["error"] + "enum": [ + "error" + ] }, "input": { "type": "object" @@ -10642,11 +12216,17 @@ "type": "object" }, "error": { - "$ref": "#/components/schemas/Session.Error.Unknown" + "$ref": "#/components/schemas/Session.StructuredError" }, "result": {} }, - "required": ["status", "input", "content", "structured", "error"], + "required": [ + "status", + "input", + "content", + "structured", + "error" + ], "additionalProperties": false }, "Session.Message.Assistant.Tool": { @@ -10654,7 +12234,9 @@ "properties": { "type": { "type": "string", - "enum": ["tool"] + "enum": [ + "tool" + ] }, "id": { "type": "string" @@ -10662,26 +12244,19 @@ "name": { "type": "string" }, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLM.ProviderMetadata" - }, - "resultMetadata": { - "$ref": "#/components/schemas/LLM.ProviderMetadata" - } - }, - "required": ["executed"], - "additionalProperties": false + "executed": { + "type": "boolean" + }, + "providerState": { + "$ref": "#/components/schemas/Session.Message.ProviderState" + }, + "providerResultState": { + "$ref": "#/components/schemas/Session.Message.ProviderState" }, "state": { "anyOf": [ { - "$ref": "#/components/schemas/Session.Message.ToolState.Pending" + "$ref": "#/components/schemas/Session.Message.ToolState.Streaming" }, { "$ref": "#/components/schemas/Session.Message.ToolState.Running" @@ -10705,16 +12280,46 @@ }, "completed": { "type": "number" - }, - "pruned": { - "type": "number" } }, - "required": ["created"], + "required": [ + "created" + ], "additionalProperties": false } }, - "required": ["type", "id", "name", "state", "time"], + "required": [ + "type", + "id", + "name", + "state", + "time" + ], + "additionalProperties": false + }, + "Session.Message.Assistant.Retry": { + "type": "object", + "properties": { + "attempt": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "at": { + "type": "number" + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + } + }, + "required": [ + "attempt", + "at", + "error" + ], "additionalProperties": false }, "Session.Message.Assistant": { @@ -10741,12 +12346,16 @@ "type": "number" } }, - "required": ["created"], + "required": [ + "created" + ], "additionalProperties": false }, "type": { "type": "string", - "enum": ["assistant"] + "enum": [ + "assistant" + ] }, "agent": { "type": "string" @@ -10789,63 +12398,47 @@ "additionalProperties": false }, "finish": { - "type": "string" + "type": "string", + "enum": [ + "stop", + "length", + "tool-calls", + "content-filter", + "error", + "unknown" + ] }, "cost": { - "type": "number" + "$ref": "#/components/schemas/Money.USD" }, "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false + "$ref": "#/components/schemas/TokenUsage.Info" }, "error": { - "$ref": "#/components/schemas/Session.Error.Unknown" + "$ref": "#/components/schemas/Session.StructuredError" + }, + "retry": { + "$ref": "#/components/schemas/Session.Message.Assistant.Retry" } }, - "required": ["id", "time", "type", "agent", "model", "content"], + "required": [ + "id", + "time", + "type", + "agent", + "model", + "content" + ], "additionalProperties": false }, - "Session.Message.Compaction": { + "Session.Message.Compaction.Running": { "type": "object", "properties": { "type": { "type": "string", - "enum": ["compaction"] - }, - "reason": { - "type": "string", - "enum": ["auto", "manual"] - }, - "summary": { - "type": "string" - }, - "recent": { - "type": "string" + "enum": [ + "compaction" + ] }, "id": { "type": "string", @@ -10865,20 +12458,184 @@ "type": "number" } }, - "required": ["created"], + "required": [ + "created" + ], "additionalProperties": false + }, + "status": { + "type": "string", + "enum": [ + "running" + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "summary": { + "type": "string" + }, + "recent": { + "type": "string" } }, - "required": ["type", "reason", "summary", "recent", "id", "time"], + "required": [ + "type", + "id", + "time", + "status", + "reason", + "summary", + "recent" + ], "additionalProperties": false }, - "Session.Message": { + "Session.Message.Compaction.Completed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "compaction" + ] + }, + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "status": { + "type": "string", + "enum": [ + "completed" + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "summary": { + "type": "string" + }, + "recent": { + "type": "string" + } + }, + "required": [ + "type", + "id", + "time", + "status", + "reason", + "summary", + "recent" + ], + "additionalProperties": false + }, + "Session.Message.Compaction.Failed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "compaction" + ] + }, + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "status": { + "type": "string", + "enum": [ + "failed" + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + } + }, + "required": [ + "type", + "id", + "time", + "status", + "reason", + "error" + ], + "additionalProperties": false + }, + "Session.Message.Compaction": { "anyOf": [ { - "$ref": "#/components/schemas/Session.Message.AgentSwitched" + "$ref": "#/components/schemas/Session.Message.Compaction.Running" }, { - "$ref": "#/components/schemas/Session.Message.ModelSwitched" + "$ref": "#/components/schemas/Session.Message.Compaction.Completed" + }, + { + "$ref": "#/components/schemas/Session.Message.Compaction.Failed" + } + ] + }, + "Session.Message.Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.AgentSelected" + }, + { + "$ref": "#/components/schemas/Session.Message.ModelSelected" }, { "$ref": "#/components/schemas/Session.Message.User" @@ -10903,27 +12660,30 @@ } ] }, - "SessionContextEntry.Key": { + "InstructionEntry.Key": { "type": "string", "allOf": [ { "pattern": "^[a-z0-9][a-z0-9._-]*$", - "description": "Context entry key (lowercase alphanumerics plus . _ -)" + "description": "Instruction entry key (lowercase alphanumerics plus . _ -)" } ] }, - "SessionContextEntry.Info": { + "InstructionEntry.Info": { "type": "object", "properties": { "key": { - "$ref": "#/components/schemas/SessionContextEntry.Key" + "$ref": "#/components/schemas/InstructionEntry.Key" }, "value": {} }, - "required": ["key", "value"], + "required": [ + "key", + "value" + ], "additionalProperties": false }, - "session.next.agent.switched": { + "session.agent.selected": { "type": "object", "properties": { "id": { @@ -10934,12 +12694,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.agent.switched"] + "enum": [ + "session.agent.selected" + ] }, "durable": { "type": "object", @@ -10956,15 +12721,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -10973,9 +12740,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -10984,26 +12748,27 @@ } ] }, - "messageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, "agent": { "type": "string" } }, - "required": ["timestamp", "sessionID", "messageID", "agent"], + "required": [ + "sessionID", + "agent" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.model.switched": { + "session.model.selected": { "type": "object", "properties": { "id": { @@ -11014,12 +12779,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.model.switched"] + "enum": [ + "session.model.selected" + ] }, "durable": { "type": "object", @@ -11036,15 +12806,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -11053,9 +12825,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -11064,26 +12833,27 @@ } ] }, - "messageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, "model": { "$ref": "#/components/schemas/Model.Ref" } }, - "required": ["timestamp", "sessionID", "messageID", "model"], + "required": [ + "sessionID", + "model" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.moved": { + "session.moved": { "type": "object", "properties": { "id": { @@ -11094,12 +12864,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.moved"] + "enum": [ + "session.moved" + ] }, "durable": { "type": "object", @@ -11116,15 +12891,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -11133,9 +12910,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -11147,18 +12921,27 @@ "location": { "$ref": "#/components/schemas/Location.Ref" }, - "subdirectory": { + "subpath": { "type": "string" } }, - "required": ["timestamp", "sessionID", "location"], + "required": [ + "sessionID", + "location" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.renamed": { + "session.renamed": { "type": "object", "properties": { "id": { @@ -11169,12 +12952,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.renamed"] + "enum": [ + "session.renamed" + ] }, "durable": { "type": "object", @@ -11191,15 +12979,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -11208,9 +12998,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -11223,14 +13010,23 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "title"], + "required": [ + "sessionID", + "title" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.forked": { + "session.deleted": { "type": "object", "properties": { "id": { @@ -11241,12 +13037,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.forked"] + "enum": [ + "session.deleted" + ] }, "durable": { "type": "object", @@ -11263,15 +13064,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 2 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -11280,9 +13083,87 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + } + }, + "required": [ + "sessionID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.forked": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.forked" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { "sessionID": { "type": "string", "allOf": [ @@ -11299,7 +13180,7 @@ } ] }, - "messageID": { + "from": { "type": "string", "allOf": [ { @@ -11308,14 +13189,23 @@ ] } }, - "required": ["timestamp", "sessionID", "parentID"], + "required": [ + "sessionID", + "parentID" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.prompted": { + "session.prompt.promoted": { "type": "object", "properties": { "id": { @@ -11326,12 +13216,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.prompted"] + "enum": [ + "session.prompt.promoted" + ] }, "durable": { "type": "object", @@ -11348,15 +13243,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -11365,9 +13262,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -11376,7 +13270,97 @@ } ] }, - "messageID": { + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "sessionID", + "inputID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.prompt.admitted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.prompt.admitted" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "inputID": { "type": "string", "allOf": [ { @@ -11389,17 +13373,31 @@ }, "delivery": { "type": "string", - "enum": ["steer", "queue"] + "enum": [ + "steer", + "queue" + ] } }, - "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], + "required": [ + "sessionID", + "inputID", + "prompt", + "delivery" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.prompt.admitted": { + "session.execution.started": { "type": "object", "properties": { "id": { @@ -11410,12 +13408,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.prompt.admitted"] + "enum": [ + "session.execution.started" + ] }, "durable": { "type": "object", @@ -11432,15 +13435,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -11449,9 +13454,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -11459,31 +13461,24 @@ "pattern": "^ses" } ] - }, - "messageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "prompt": { - "$ref": "#/components/schemas/Prompt" - }, - "delivery": { - "type": "string", - "enum": ["steer", "queue"] } }, - "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], + "required": [ + "sessionID" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.context.updated": { + "session.execution.succeeded": { "type": "object", "properties": { "id": { @@ -11494,12 +13489,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.context.updated"] + "enum": [ + "session.execution.succeeded" + ] }, "durable": { "type": "object", @@ -11516,15 +13516,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -11533,9 +13535,87 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + } + }, + "required": [ + "sessionID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.execution.failed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.execution.failed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { "sessionID": { "type": "string", "allOf": [ @@ -11544,11 +13624,178 @@ } ] }, - "messageID": { + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + } + }, + "required": [ + "sessionID", + "error" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.execution.interrupted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.execution.interrupted" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { "type": "string", "allOf": [ { - "pattern": "^msg_" + "pattern": "^ses" + } + ] + }, + "reason": { + "type": "string", + "enum": [ + "user", + "shutdown", + "superseded" + ] + } + }, + "required": [ + "sessionID", + "reason" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.instructions.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.instructions.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" } ] }, @@ -11556,14 +13803,23 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "messageID", "text"], + "required": [ + "sessionID", + "text" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.synthetic": { + "session.synthetic": { "type": "object", "properties": { "id": { @@ -11574,12 +13830,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.synthetic"] + "enum": [ + "session.synthetic" + ] }, "durable": { "type": "object", @@ -11596,15 +13857,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -11613,9 +13876,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -11624,14 +13884,6 @@ } ] }, - "messageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, "text": { "type": "string" }, @@ -11642,14 +13894,23 @@ "type": "object" } }, - "required": ["timestamp", "sessionID", "messageID", "text"], + "required": [ + "sessionID", + "text" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.skill.activated": { + "session.skill.activated": { "type": "object", "properties": { "id": { @@ -11660,12 +13921,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.skill.activated"] + "enum": [ + "session.skill.activated" + ] }, "durable": { "type": "object", @@ -11682,15 +13948,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -11699,9 +13967,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -11710,13 +13975,8 @@ } ] }, - "messageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] + "id": { + "type": "string" }, "name": { "type": "string" @@ -11725,14 +13985,165 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "messageID", "name", "text"], + "required": [ + "sessionID", + "id", + "name", + "text" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.shell.started": { + "Shell": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "running", + "exited", + "timeout", + "killed" + ] + }, + "command": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "shell": { + "type": "string" + }, + "file": { + "type": "string" + }, + "pid": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "exit": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "started": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + "completed": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + } + }, + "required": [ + "started" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "status", + "command", + "cwd", + "shell", + "file", + "metadata", + "time" + ], + "additionalProperties": false + }, + "session.shell.started": { "type": "object", "properties": { "id": { @@ -11743,12 +14154,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.shell.started"] + "enum": [ + "session.shell.started" + ] }, "durable": { "type": "object", @@ -11765,15 +14181,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -11782,9 +14200,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -11793,29 +14208,27 @@ } ] }, - "messageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "callID": { - "type": "string" - }, - "command": { - "type": "string" + "shell": { + "$ref": "#/components/schemas/Shell" } }, - "required": ["timestamp", "sessionID", "messageID", "callID", "command"], + "required": [ + "sessionID", + "shell" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.shell.ended": { + "session.shell.ended": { "type": "object", "properties": { "id": { @@ -11826,12 +14239,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.shell.ended"] + "enum": [ + "session.shell.ended" + ] }, "durable": { "type": "object", @@ -11848,15 +14266,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -11865,9 +14285,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -11876,21 +14293,62 @@ } ] }, - "callID": { - "type": "string" + "shell": { + "$ref": "#/components/schemas/Shell" }, "output": { - "type": "string" + "type": "object", + "properties": { + "output": { + "type": "string" + }, + "cursor": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "size": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "truncated": { + "type": "boolean" + } + }, + "required": [ + "output", + "cursor", + "size", + "truncated" + ], + "additionalProperties": false } }, - "required": ["timestamp", "sessionID", "callID", "output"], + "required": [ + "sessionID", + "shell", + "output" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.step.started": { + "session.step.started": { "type": "object", "properties": { "id": { @@ -11901,12 +14359,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.step.started"] + "enum": [ + "session.step.started" + ] }, "durable": { "type": "object", @@ -11923,15 +14386,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -11940,9 +14405,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -11969,14 +14431,25 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "agent", "model"], + "required": [ + "sessionID", + "assistantMessageID", + "agent", + "model" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.step.ended": { + "session.step.ended": { "type": "object", "properties": { "id": { @@ -11987,12 +14460,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.step.ended"] + "enum": [ + "session.step.ended" + ] }, "durable": { "type": "object", @@ -12009,15 +14487,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -12026,9 +14506,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -12046,39 +14523,21 @@ ] }, "finish": { - "type": "string" + "type": "string", + "enum": [ + "stop", + "length", + "tool-calls", + "content-filter", + "error", + "unknown" + ] }, "cost": { - "type": "number" + "$ref": "#/components/schemas/Money.USD" }, "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": ["read", "write"], - "additionalProperties": false - } - }, - "required": ["input", "output", "reasoning", "cache"], - "additionalProperties": false + "$ref": "#/components/schemas/TokenUsage.Info" }, "snapshot": { "type": "string" @@ -12090,14 +14549,26 @@ } } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "finish", "cost", "tokens"], + "required": [ + "sessionID", + "assistantMessageID", + "finish", + "cost", + "tokens" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.step.failed": { + "session.step.failed": { "type": "object", "properties": { "id": { @@ -12108,12 +14579,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.step.failed"] + "enum": [ + "session.step.failed" + ] }, "durable": { "type": "object", @@ -12130,15 +14606,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -12147,9 +14625,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -12167,17 +14642,33 @@ ] }, "error": { - "$ref": "#/components/schemas/Session.Error.Unknown" + "$ref": "#/components/schemas/Session.StructuredError" + }, + "cost": { + "$ref": "#/components/schemas/Money.USD" + }, + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "error"], + "required": [ + "sessionID", + "assistantMessageID", + "error" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.text.started": { + "session.text.started": { "type": "object", "properties": { "id": { @@ -12188,12 +14679,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.text.started"] + "enum": [ + "session.text.started" + ] }, "durable": { "type": "object", @@ -12210,15 +14706,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -12227,9 +14725,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -12246,18 +14741,33 @@ } ] }, - "textID": { - "type": "string" + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID"], + "required": [ + "sessionID", + "assistantMessageID", + "ordinal" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.text.ended": { + "session.text.ended": { "type": "object", "properties": { "id": { @@ -12268,12 +14778,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.text.ended"] + "enum": [ + "session.text.ended" + ] }, "durable": { "type": "object", @@ -12290,15 +14805,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -12307,9 +14824,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -12326,21 +14840,40 @@ } ] }, - "textID": { - "type": "string" + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "text": { "type": "string" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "text"], + "required": [ + "sessionID", + "assistantMessageID", + "ordinal", + "text" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.tool.input.started": { + "Session.Message.ProviderState3": { + "type": "object" + }, + "session.reasoning.started": { "type": "object", "properties": { "id": { @@ -12351,12 +14884,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.input.started"] + "enum": [ + "session.reasoning.started" + ] }, "durable": { "type": "object", @@ -12373,15 +14911,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -12390,9 +14930,217 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState3" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "ordinal" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "Session.Message.ProviderState4": { + "type": "object" + }, + "session.reasoning.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.reasoning.ended" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "text": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState4" + } + }, + "required": [ + "sessionID", + "assistantMessageID", + "ordinal", + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.tool.input.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.tool.input.started" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { "sessionID": { "type": "string", "allOf": [ @@ -12416,14 +15164,25 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "name"], + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "name" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.tool.input.ended": { + "session.tool.input.ended": { "type": "object", "properties": { "id": { @@ -12434,12 +15193,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.input.ended"] + "enum": [ + "session.tool.input.ended" + ] }, "durable": { "type": "object", @@ -12456,15 +15220,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -12473,9 +15239,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -12499,20 +15262,28 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "text"], + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "text" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "LLM.ProviderMetadata3": { - "type": "object", - "additionalProperties": { - "type": "object" - } + "Session.Message.ProviderState5": { + "type": "object" }, - "session.next.tool.called": { + "session.tool.called": { "type": "object", "properties": { "id": { @@ -12523,12 +15294,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.called"] + "enum": [ + "session.tool.called" + ] }, "durable": { "type": "object", @@ -12545,15 +15321,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -12562,9 +15340,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -12584,34 +15359,36 @@ "callID": { "type": "string" }, - "tool": { - "type": "string" - }, "input": { "type": "object" }, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLM.ProviderMetadata3" - } - }, - "required": ["executed"], - "additionalProperties": false + "executed": { + "type": "boolean" + }, + "state": { + "$ref": "#/components/schemas/Session.Message.ProviderState5" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "tool", "input", "provider"], + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "input", + "executed" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.tool.progress": { + "session.tool.progress": { "type": "object", "properties": { "id": { @@ -12622,12 +15399,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.progress"] + "enum": [ + "session.tool.progress" + ] }, "durable": { "type": "object", @@ -12644,15 +15426,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -12661,9 +15445,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -12693,20 +15474,29 @@ } } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content"], + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "structured", + "content" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "LLM.ProviderMetadata4": { - "type": "object", - "additionalProperties": { - "type": "object" - } + "Session.Message.ProviderState6": { + "type": "object" }, - "session.next.tool.success": { + "session.tool.success": { "type": "object", "properties": { "id": { @@ -12717,12 +15507,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.success"] + "enum": [ + "session.tool.success" + ] }, "durable": { "type": "object", @@ -12739,15 +15534,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -12756,9 +15553,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -12787,41 +15581,38 @@ "$ref": "#/components/schemas/LLM.ToolContent" } }, - "outputPaths": { - "type": "array", - "items": { - "type": "string" - } - }, "result": {}, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLM.ProviderMetadata4" - } - }, - "required": ["executed"], - "additionalProperties": false + "executed": { + "type": "boolean" + }, + "resultState": { + "$ref": "#/components/schemas/Session.Message.ProviderState6" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content", "provider"], + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "structured", + "content", + "executed" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "LLM.ProviderMetadata5": { - "type": "object", - "additionalProperties": { - "type": "object" - } + "Session.Message.ProviderState7": { + "type": "object" }, - "session.next.tool.failed": { + "session.tool.failed": { "type": "object", "properties": { "id": { @@ -12832,12 +15623,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.failed"] + "enum": [ + "session.tool.failed" + ] }, "durable": { "type": "object", @@ -12854,15 +15650,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -12871,9 +15669,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -12894,37 +15689,36 @@ "type": "string" }, "error": { - "$ref": "#/components/schemas/Session.Error.Unknown" + "$ref": "#/components/schemas/Session.StructuredError" }, "result": {}, - "provider": { - "type": "object", - "properties": { - "executed": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/components/schemas/LLM.ProviderMetadata5" - } - }, - "required": ["executed"], - "additionalProperties": false + "executed": { + "type": "boolean" + }, + "resultState": { + "$ref": "#/components/schemas/Session.Message.ProviderState7" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "error", "provider"], + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "error", + "executed" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "LLM.ProviderMetadata6": { - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "session.next.reasoning.started": { + "session.retry.scheduled": { "type": "object", "properties": { "id": { @@ -12935,218 +15729,17 @@ } ] }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.reasoning.started"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "reasoningID": { - "type": "string" - }, - "providerMetadata": { - "$ref": "#/components/schemas/LLM.ProviderMetadata6" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "LLM.ProviderMetadata7": { - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "session.next.reasoning.ended": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["session.next.reasoning.ended"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "timestamp": { - "type": "number" - }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "reasoningID": { - "type": "string" - }, - "text": { - "type": "string" - }, - "providerMetadata": { - "$ref": "#/components/schemas/LLM.ProviderMetadata7" - } - }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "session.next.retry_error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "statusCode": { + "created": { "type": "number" }, - "isRetryable": { - "type": "boolean" - }, - "responseHeaders": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "responseBody": { - "type": "string" - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "required": ["message", "isRetryable"], - "additionalProperties": false - }, - "session.next.retried": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.retried"] + "enum": [ + "session.retry.scheduled" + ] }, "durable": { "type": "object", @@ -13163,15 +15756,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -13180,9 +15775,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -13191,21 +15783,54 @@ } ] }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, "attempt": { - "type": "number" + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "at": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "error": { - "$ref": "#/components/schemas/session.next.retry_error" + "$ref": "#/components/schemas/Session.StructuredError" } }, - "required": ["timestamp", "sessionID", "attempt", "error"], + "required": [ + "sessionID", + "assistantMessageID", + "attempt", + "at", + "error" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.compaction.started": { + "session.compaction.admitted": { "type": "object", "properties": { "id": { @@ -13216,12 +15841,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.compaction.started"] + "enum": [ + "session.compaction.admitted" + ] }, "durable": { "type": "object", @@ -13238,15 +15868,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -13255,9 +15887,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -13266,27 +15895,32 @@ } ] }, - "messageID": { + "inputID": { "type": "string", "allOf": [ { "pattern": "^msg_" } ] - }, - "reason": { - "type": "string", - "enum": ["auto", "manual"] } }, - "required": ["timestamp", "sessionID", "messageID", "reason"], + "required": [ + "sessionID", + "inputID" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.compaction.ended": { + "session.compaction.started": { "type": "object", "properties": { "id": { @@ -13297,12 +15931,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.compaction.ended"] + "enum": [ + "session.compaction.started" + ] }, "durable": { "type": "object", @@ -13319,15 +15958,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -13336,9 +15977,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -13347,17 +15985,113 @@ } ] }, - "messageID": { + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "recent": { + "type": "string" + }, + "inputID": { "type": "string", "allOf": [ { "pattern": "^msg_" } ] + } + }, + "required": [ + "sessionID", + "reason", + "recent" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.compaction.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.compaction.ended" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, "reason": { "type": "string", - "enum": ["auto", "manual"] + "enum": [ + "auto", + "manual" + ] }, "text": { "type": "string" @@ -13366,14 +16100,25 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "messageID", "reason", "text", "recent"], + "required": [ + "sessionID", + "reason", + "text", + "recent" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.revert.staged": { + "session.compaction.failed": { "type": "object", "properties": { "id": { @@ -13384,12 +16129,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.revert.staged"] + "enum": [ + "session.compaction.failed" + ] }, "durable": { "type": "object", @@ -13406,15 +16156,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -13423,9 +16175,107 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + }, + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "sessionID", + "reason", + "error" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], + "additionalProperties": false + }, + "session.revert.staged": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.revert.staged" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "number", + "enum": [ + 1 + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { "sessionID": { "type": "string", "allOf": [ @@ -13435,17 +16285,26 @@ ] }, "revert": { - "$ref": "#/components/schemas/Revert.State" + "$ref": "#/components/schemas/Session.Revert" } }, - "required": ["timestamp", "sessionID", "revert"], + "required": [ + "sessionID", + "revert" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.revert.cleared": { + "session.revert.cleared": { "type": "object", "properties": { "id": { @@ -13456,12 +16315,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.revert.cleared"] + "enum": [ + "session.revert.cleared" + ] }, "durable": { "type": "object", @@ -13478,15 +16342,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -13495,9 +16361,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -13507,14 +16370,22 @@ ] } }, - "required": ["timestamp", "sessionID"], + "required": [ + "sessionID" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.revert.committed": { + "session.revert.committed": { "type": "object", "properties": { "id": { @@ -13525,12 +16396,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.revert.committed"] + "enum": [ + "session.revert.committed" + ] }, "durable": { "type": "object", @@ -13547,15 +16423,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -13564,9 +16442,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -13575,7 +16450,7 @@ } ] }, - "messageID": { + "to": { "type": "string", "allOf": [ { @@ -13584,107 +16459,137 @@ ] } }, - "required": ["timestamp", "sessionID", "messageID"], + "required": [ + "sessionID", + "to" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "SessionDurableEvent": { + "Session.Event.Durable": { "oneOf": [ { - "$ref": "#/components/schemas/session.next.agent.switched" + "$ref": "#/components/schemas/session.agent.selected" }, { - "$ref": "#/components/schemas/session.next.model.switched" + "$ref": "#/components/schemas/session.model.selected" }, { - "$ref": "#/components/schemas/session.next.moved" + "$ref": "#/components/schemas/session.moved" }, { - "$ref": "#/components/schemas/session.next.renamed" + "$ref": "#/components/schemas/session.renamed" }, { - "$ref": "#/components/schemas/session.next.forked" + "$ref": "#/components/schemas/session.deleted" }, { - "$ref": "#/components/schemas/session.next.prompted" + "$ref": "#/components/schemas/session.forked" }, { - "$ref": "#/components/schemas/session.next.prompt.admitted" + "$ref": "#/components/schemas/session.prompt.promoted" }, { - "$ref": "#/components/schemas/session.next.context.updated" + "$ref": "#/components/schemas/session.prompt.admitted" }, { - "$ref": "#/components/schemas/session.next.synthetic" + "$ref": "#/components/schemas/session.execution.started" }, { - "$ref": "#/components/schemas/session.next.skill.activated" + "$ref": "#/components/schemas/session.execution.succeeded" }, { - "$ref": "#/components/schemas/session.next.shell.started" + "$ref": "#/components/schemas/session.execution.failed" }, { - "$ref": "#/components/schemas/session.next.shell.ended" + "$ref": "#/components/schemas/session.execution.interrupted" }, { - "$ref": "#/components/schemas/session.next.step.started" + "$ref": "#/components/schemas/session.instructions.updated" }, { - "$ref": "#/components/schemas/session.next.step.ended" + "$ref": "#/components/schemas/session.synthetic" }, { - "$ref": "#/components/schemas/session.next.step.failed" + "$ref": "#/components/schemas/session.skill.activated" }, { - "$ref": "#/components/schemas/session.next.text.started" + "$ref": "#/components/schemas/session.shell.started" }, { - "$ref": "#/components/schemas/session.next.text.ended" + "$ref": "#/components/schemas/session.shell.ended" }, { - "$ref": "#/components/schemas/session.next.tool.input.started" + "$ref": "#/components/schemas/session.step.started" }, { - "$ref": "#/components/schemas/session.next.tool.input.ended" + "$ref": "#/components/schemas/session.step.ended" }, { - "$ref": "#/components/schemas/session.next.tool.called" + "$ref": "#/components/schemas/session.step.failed" }, { - "$ref": "#/components/schemas/session.next.tool.progress" + "$ref": "#/components/schemas/session.text.started" }, { - "$ref": "#/components/schemas/session.next.tool.success" + "$ref": "#/components/schemas/session.text.ended" }, { - "$ref": "#/components/schemas/session.next.tool.failed" + "$ref": "#/components/schemas/session.reasoning.started" }, { - "$ref": "#/components/schemas/session.next.reasoning.started" + "$ref": "#/components/schemas/session.reasoning.ended" }, { - "$ref": "#/components/schemas/session.next.reasoning.ended" + "$ref": "#/components/schemas/session.tool.input.started" }, { - "$ref": "#/components/schemas/session.next.retried" + "$ref": "#/components/schemas/session.tool.input.ended" }, { - "$ref": "#/components/schemas/session.next.compaction.started" + "$ref": "#/components/schemas/session.tool.called" }, { - "$ref": "#/components/schemas/session.next.compaction.ended" + "$ref": "#/components/schemas/session.tool.progress" }, { - "$ref": "#/components/schemas/session.next.revert.staged" + "$ref": "#/components/schemas/session.tool.success" }, { - "$ref": "#/components/schemas/session.next.revert.cleared" + "$ref": "#/components/schemas/session.tool.failed" }, { - "$ref": "#/components/schemas/session.next.revert.committed" + "$ref": "#/components/schemas/session.retry.scheduled" + }, + { + "$ref": "#/components/schemas/session.compaction.admitted" + }, + { + "$ref": "#/components/schemas/session.compaction.started" + }, + { + "$ref": "#/components/schemas/session.compaction.ended" + }, + { + "$ref": "#/components/schemas/session.compaction.failed" + }, + { + "$ref": "#/components/schemas/session.revert.staged" + }, + { + "$ref": "#/components/schemas/session.revert.cleared" + }, + { + "$ref": "#/components/schemas/session.revert.committed" } ] }, @@ -13693,7 +16598,9 @@ "properties": { "type": { "type": "string", - "enum": ["log.synced"] + "enum": [ + "log.synced" + ] }, "aggregateID": { "type": "string" @@ -13707,14 +16614,17 @@ ] } }, - "required": ["type", "aggregateID"], + "required": [ + "type", + "aggregateID" + ], "additionalProperties": false, "description": "Marker emitted once when a log read reaches its captured watermark. The reader holds every event committed at or below seq." }, "SessionLogItem": { "anyOf": [ { - "$ref": "#/components/schemas/SessionDurableEvent" + "$ref": "#/components/schemas/Session.Event.Durable" }, { "$ref": "#/components/schemas/EventLog.Synced" @@ -13734,17 +16644,9 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Session.Message" + "$ref": "#/components/schemas/Session.Message.Info" } }, - "watermark": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, "cursor": { "type": "object", "properties": { @@ -13772,56 +16674,12 @@ "additionalProperties": false } }, - "required": ["data", "cursor"], + "required": [ + "data", + "cursor" + ], "additionalProperties": false }, - "Model.Api": { - "anyOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["aisdk"] - }, - "package": { - "type": "string" - }, - "url": { - "type": "string" - }, - "settings": { - "type": "object" - } - }, - "required": ["id", "type", "package"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["native"] - }, - "url": { - "type": "string" - }, - "settings": { - "type": "object" - } - }, - "required": ["id", "type", "settings"], - "additionalProperties": false - } - ] - }, "Model.Capabilities": { "type": "object", "properties": { @@ -13841,9 +16699,40 @@ } } }, - "required": ["tools", "input", "output"], + "required": [ + "tools", + "input", + "output" + ], "additionalProperties": false }, + "Model.Variant": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "settings": { + "type": "object" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "Money.USDPerMillionTokens": { + "type": "number" + }, "Model.Cost": { "type": "object", "properties": { @@ -13852,44 +16741,59 @@ "properties": { "type": { "type": "string", - "enum": ["context"] + "enum": [ + "context" + ] }, "size": { "type": "integer" } }, - "required": ["type", "size"], + "required": [ + "type", + "size" + ], "additionalProperties": false }, "input": { - "type": "number" + "$ref": "#/components/schemas/Money.USDPerMillionTokens" }, "output": { - "type": "number" + "$ref": "#/components/schemas/Money.USDPerMillionTokens" }, "cache": { "type": "object", "properties": { "read": { - "type": "number" + "$ref": "#/components/schemas/Money.USDPerMillionTokens" }, "write": { - "type": "number" + "$ref": "#/components/schemas/Money.USDPerMillionTokens" } }, - "required": ["read", "write"], + "required": [ + "read", + "write" + ], "additionalProperties": false } }, - "required": ["input", "output", "cache"], + "required": [ + "input", + "output", + "cache" + ], "additionalProperties": false }, - "ModelV2.Info": { + "Model.Info": { "type": "object", "properties": { "id": { "type": "string" }, + "modelID": { + "type": "string" + }, "providerID": { "type": "string" }, @@ -13899,57 +16803,28 @@ "name": { "type": "string" }, - "api": { - "$ref": "#/components/schemas/Model.Api" + "package": { + "type": "string" + }, + "settings": { + "type": "object" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" }, "capabilities": { "$ref": "#/components/schemas/Model.Capabilities" }, - "request": { - "type": "object", - "properties": { - "settings": { - "$ref": "#/components/schemas/Provider.Settings" - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "body": { - "type": "object" - }, - "variant": { - "type": "string" - } - }, - "required": ["settings", "headers", "body"], - "additionalProperties": false - }, "variants": { "type": "array", "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "settings": { - "$ref": "#/components/schemas/Provider.Settings" - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "body": { - "type": "object" - } - }, - "required": ["id", "settings", "headers", "body"], - "additionalProperties": false + "$ref": "#/components/schemas/Model.Variant" } }, "time": { @@ -13959,7 +16834,9 @@ "type": "number" } }, - "required": ["released"], + "required": [ + "released" + ], "additionalProperties": false }, "cost": { @@ -13970,7 +16847,12 @@ }, "status": { "type": "string", - "enum": ["alpha", "beta", "deprecated", "active"] + "enum": [ + "alpha", + "beta", + "deprecated", + "active" + ] }, "enabled": { "type": "boolean" @@ -13988,17 +16870,19 @@ "type": "integer" } }, - "required": ["context", "output"], + "required": [ + "context", + "output" + ], "additionalProperties": false } }, "required": [ "id", + "modelID", "providerID", "name", - "api", "capabilities", - "request", "variants", "time", "cost", @@ -14018,60 +16902,17 @@ "type": "string" } }, - "required": ["text"], + "required": [ + "text" + ], "additionalProperties": false } }, - "required": ["data"], + "required": [ + "data" + ], "additionalProperties": false }, - "Provider.AISDK": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["aisdk"] - }, - "package": { - "type": "string" - }, - "url": { - "type": "string" - }, - "settings": { - "type": "object" - } - }, - "required": ["type", "package"], - "additionalProperties": false - }, - "Provider.Native": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["native"] - }, - "url": { - "type": "string" - }, - "settings": { - "type": "object" - } - }, - "required": ["type", "settings"], - "additionalProperties": false - }, - "Provider.Api": { - "anyOf": [ - { - "$ref": "#/components/schemas/Provider.AISDK" - }, - { - "$ref": "#/components/schemas/Provider.Native" - } - ] - }, "ProviderV2.Info": { "type": "object", "properties": { @@ -14087,14 +16928,27 @@ "disabled": { "type": "boolean" }, - "api": { - "$ref": "#/components/schemas/Provider.Api" + "package": { + "type": "string" }, - "request": { - "$ref": "#/components/schemas/Provider.Request" + "settings": { + "type": "object" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" } }, - "required": ["id", "name", "api", "request"], + "required": [ + "id", + "name", + "package" + ], "additionalProperties": false }, "ProviderNotFoundError": { @@ -14102,7 +16956,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["ProviderNotFoundError"] + "enum": [ + "ProviderNotFoundError" + ] }, "providerID": { "type": "string" @@ -14111,7 +16967,11 @@ "type": "string" } }, - "required": ["_tag", "providerID", "message"], + "required": [ + "_tag", + "providerID", + "message" + ], "additionalProperties": false }, "Integration.When": { @@ -14122,13 +16982,20 @@ }, "op": { "type": "string", - "enum": ["eq", "neq"] + "enum": [ + "eq", + "neq" + ] }, "value": { "type": "string" } }, - "required": ["key", "op", "value"], + "required": [ + "key", + "op", + "value" + ], "additionalProperties": false }, "Integration.TextPrompt": { @@ -14136,7 +17003,9 @@ "properties": { "type": { "type": "string", - "enum": ["text"] + "enum": [ + "text" + ] }, "key": { "type": "string" @@ -14151,7 +17020,11 @@ "$ref": "#/components/schemas/Integration.When" } }, - "required": ["type", "key", "message"], + "required": [ + "type", + "key", + "message" + ], "additionalProperties": false }, "Integration.SelectPrompt": { @@ -14159,7 +17032,9 @@ "properties": { "type": { "type": "string", - "enum": ["select"] + "enum": [ + "select" + ] }, "key": { "type": "string" @@ -14182,7 +17057,10 @@ "type": "string" } }, - "required": ["label", "value"], + "required": [ + "label", + "value" + ], "additionalProperties": false } }, @@ -14190,7 +17068,12 @@ "$ref": "#/components/schemas/Integration.When" } }, - "required": ["type", "key", "message", "options"], + "required": [ + "type", + "key", + "message", + "options" + ], "additionalProperties": false }, "Integration.OAuthMethod": { @@ -14201,7 +17084,9 @@ }, "type": { "type": "string", - "enum": ["oauth"] + "enum": [ + "oauth" + ] }, "label": { "type": "string" @@ -14220,7 +17105,11 @@ } } }, - "required": ["id", "type", "label"], + "required": [ + "id", + "type", + "label" + ], "additionalProperties": false }, "Integration.KeyMethod": { @@ -14228,13 +17117,17 @@ "properties": { "type": { "type": "string", - "enum": ["key"] + "enum": [ + "key" + ] }, "label": { "type": "string" } }, - "required": ["type"], + "required": [ + "type" + ], "additionalProperties": false }, "Integration.EnvMethod": { @@ -14242,7 +17135,9 @@ "properties": { "type": { "type": "string", - "enum": ["env"] + "enum": [ + "env" + ] }, "names": { "type": "array", @@ -14251,7 +17146,10 @@ } } }, - "required": ["type", "names"], + "required": [ + "type", + "names" + ], "additionalProperties": false }, "Integration.Method": { @@ -14272,7 +17170,9 @@ "properties": { "type": { "type": "string", - "enum": ["credential"] + "enum": [ + "credential" + ] }, "id": { "type": "string" @@ -14281,7 +17181,11 @@ "type": "string" } }, - "required": ["type", "id", "label"], + "required": [ + "type", + "id", + "label" + ], "additionalProperties": false }, "Connection.EnvInfo": { @@ -14289,13 +17193,18 @@ "properties": { "type": { "type": "string", - "enum": ["env"] + "enum": [ + "env" + ] }, "name": { "type": "string" } }, - "required": ["type", "name"], + "required": [ + "type", + "name" + ], "additionalProperties": false }, "Connection.Info": { @@ -14330,7 +17239,12 @@ } } }, - "required": ["id", "name", "methods", "connections"], + "required": [ + "id", + "name", + "methods", + "connections" + ], "additionalProperties": false }, "Integration.Attempt": { @@ -14347,7 +17261,10 @@ }, "mode": { "type": "string", - "enum": ["auto", "code"] + "enum": [ + "auto", + "code" + ] }, "time": { "type": "object", @@ -14361,21 +17278,31 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] }, @@ -14388,30 +17315,49 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] } }, - "required": ["created", "expires"], + "required": [ + "created", + "expires" + ], "additionalProperties": false } }, - "required": ["attemptID", "url", "instructions", "mode", "time"], + "required": [ + "attemptID", + "url", + "instructions", + "mode", + "time" + ], "additionalProperties": false }, "Integration.AttemptStatus": { @@ -14421,7 +17367,9 @@ "properties": { "status": { "type": "string", - "enum": ["pending"] + "enum": [ + "pending" + ] }, "time": { "type": "object", @@ -14435,21 +17383,31 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] }, @@ -14462,30 +17420,46 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] } }, - "required": ["created", "expires"], + "required": [ + "created", + "expires" + ], "additionalProperties": false } }, - "required": ["status", "time"], + "required": [ + "status", + "time" + ], "additionalProperties": false }, { @@ -14493,7 +17467,9 @@ "properties": { "status": { "type": "string", - "enum": ["complete"] + "enum": [ + "complete" + ] }, "time": { "type": "object", @@ -14507,21 +17483,31 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] }, @@ -14534,30 +17520,46 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] } }, - "required": ["created", "expires"], + "required": [ + "created", + "expires" + ], "additionalProperties": false } }, - "required": ["status", "time"], + "required": [ + "status", + "time" + ], "additionalProperties": false }, { @@ -14565,7 +17567,9 @@ "properties": { "status": { "type": "string", - "enum": ["failed"] + "enum": [ + "failed" + ] }, "message": { "type": "string" @@ -14582,21 +17586,31 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] }, @@ -14609,30 +17623,47 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] } }, - "required": ["created", "expires"], + "required": [ + "created", + "expires" + ], "additionalProperties": false } }, - "required": ["status", "message", "time"], + "required": [ + "status", + "message", + "time" + ], "additionalProperties": false }, { @@ -14640,7 +17671,9 @@ "properties": { "status": { "type": "string", - "enum": ["expired"] + "enum": [ + "expired" + ] }, "time": { "type": "object", @@ -14654,21 +17687,31 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] }, @@ -14681,30 +17724,46 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] } }, - "required": ["created", "expires"], + "required": [ + "created", + "expires" + ], "additionalProperties": false } }, - "required": ["status", "time"], + "required": [ + "status", + "time" + ], "additionalProperties": false } ] @@ -14714,21 +17773,29 @@ "properties": { "status": { "type": "string", - "enum": ["connected"] + "enum": [ + "connected" + ] } }, - "required": ["status"], + "required": [ + "status" + ], "additionalProperties": false }, - "Mcp.Status.Disconnected": { + "Mcp.Status.Pending": { "type": "object", "properties": { "status": { "type": "string", - "enum": ["disconnected"] + "enum": [ + "pending" + ] } }, - "required": ["status"], + "required": [ + "status" + ], "additionalProperties": false }, "Mcp.Status.Disabled": { @@ -14736,10 +17803,14 @@ "properties": { "status": { "type": "string", - "enum": ["disabled"] + "enum": [ + "disabled" + ] } }, - "required": ["status"], + "required": [ + "status" + ], "additionalProperties": false }, "Mcp.Status.Failed": { @@ -14747,13 +17818,18 @@ "properties": { "status": { "type": "string", - "enum": ["failed"] + "enum": [ + "failed" + ] }, "error": { "type": "string" } }, - "required": ["status", "error"], + "required": [ + "status", + "error" + ], "additionalProperties": false }, "Mcp.Status.NeedsAuth": { @@ -14761,10 +17837,14 @@ "properties": { "status": { "type": "string", - "enum": ["needs_auth"] + "enum": [ + "needs_auth" + ] } }, - "required": ["status"], + "required": [ + "status" + ], "additionalProperties": false }, "Mcp.Status.NeedsClientRegistration": { @@ -14772,13 +17852,18 @@ "properties": { "status": { "type": "string", - "enum": ["needs_client_registration"] + "enum": [ + "needs_client_registration" + ] }, "error": { "type": "string" } }, - "required": ["status", "error"], + "required": [ + "status", + "error" + ], "additionalProperties": false }, "Mcp.Server": { @@ -14793,7 +17878,7 @@ "$ref": "#/components/schemas/Mcp.Status.Connected" }, { - "$ref": "#/components/schemas/Mcp.Status.Disconnected" + "$ref": "#/components/schemas/Mcp.Status.Pending" }, { "$ref": "#/components/schemas/Mcp.Status.Disabled" @@ -14813,7 +17898,189 @@ "type": "string" } }, - "required": ["name", "status"], + "required": [ + "name", + "status" + ], + "additionalProperties": false + }, + "Mcp.Resource": { + "type": "object", + "properties": { + "server": { + "type": "string" + }, + "name": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mimeType": { + "type": "string" + } + }, + "required": [ + "server", + "name", + "uri" + ], + "additionalProperties": false + }, + "Mcp.ResourceTemplate": { + "type": "object", + "properties": { + "server": { + "type": "string" + }, + "name": { + "type": "string" + }, + "uriTemplate": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mimeType": { + "type": "string" + } + }, + "required": [ + "server", + "name", + "uriTemplate" + ], + "additionalProperties": false + }, + "Mcp.ResourceCatalog": { + "type": "object", + "properties": { + "resources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Mcp.Resource" + } + }, + "templates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Mcp.ResourceTemplate" + } + } + }, + "required": [ + "resources", + "templates" + ], + "additionalProperties": false + }, + "Project.Vcs": { + "type": "string", + "enum": [ + "git", + "hg" + ] + }, + "Project.Icon": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "override": { + "type": "string" + }, + "color": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Project.Commands": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "Startup script to run when creating a new workspace (worktree)" + } + }, + "additionalProperties": false + }, + "Project.Time": { + "type": "object", + "properties": { + "created": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "updated": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "initialized": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "created", + "updated" + ], + "additionalProperties": false + }, + "Project": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "worktree": { + "type": "string" + }, + "vcs": { + "$ref": "#/components/schemas/Project.Vcs" + }, + "name": { + "type": "string" + }, + "icon": { + "$ref": "#/components/schemas/Project.Icon" + }, + "commands": { + "$ref": "#/components/schemas/Project.Commands" + }, + "time": { + "$ref": "#/components/schemas/Project.Time" + }, + "sandboxes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "worktree", + "time", + "sandboxes" + ], "additionalProperties": false }, "Project.Current": { @@ -14826,7 +18093,10 @@ "type": "string" } }, - "required": ["id", "directory"], + "required": [ + "id", + "directory" + ], "additionalProperties": false }, "Project.Directory": { @@ -14839,7 +18109,9 @@ "type": "string" } }, - "required": ["directory"], + "required": [ + "directory" + ], "additionalProperties": false }, "Project.Directories": { @@ -14859,7 +18131,10 @@ }, "op": { "type": "string", - "enum": ["eq", "neq"] + "enum": [ + "eq", + "neq" + ] }, "value": { "anyOf": [ @@ -14875,21 +18150,31 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] }, @@ -14899,7 +18184,11 @@ ] } }, - "required": ["key", "op", "value"], + "required": [ + "key", + "op", + "value" + ], "additionalProperties": false }, "Form.Option": { @@ -14915,7 +18204,10 @@ "type": "string" } }, - "required": ["value", "label"], + "required": [ + "value", + "label" + ], "additionalProperties": false }, "Form.StringField": { @@ -14941,11 +18233,18 @@ }, "type": { "type": "string", - "enum": ["string"] + "enum": [ + "string" + ] }, "format": { "type": "string", - "enum": ["email", "uri", "date", "date-time"] + "enum": [ + "email", + "uri", + "date", + "date-time" + ] }, "minLength": { "type": "integer", @@ -14982,7 +18281,10 @@ "type": "boolean" } }, - "required": ["key", "type"], + "required": [ + "key", + "type" + ], "additionalProperties": false }, "Form.NumberField": { @@ -15008,7 +18310,9 @@ }, "type": { "type": "string", - "enum": ["number"] + "enum": [ + "number" + ] }, "minimum": { "anyOf": [ @@ -15019,21 +18323,31 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] }, @@ -15046,21 +18360,31 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] }, @@ -15073,26 +18397,39 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] } }, - "required": ["key", "type"], + "required": [ + "key", + "type" + ], "additionalProperties": false }, "Form.IntegerField": { @@ -15118,7 +18455,9 @@ }, "type": { "type": "string", - "enum": ["integer"] + "enum": [ + "integer" + ] }, "minimum": { "anyOf": [ @@ -15129,21 +18468,31 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] }, @@ -15156,21 +18505,31 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] }, @@ -15183,26 +18542,39 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] } }, - "required": ["key", "type"], + "required": [ + "key", + "type" + ], "additionalProperties": false }, "Form.BooleanField": { @@ -15228,13 +18600,18 @@ }, "type": { "type": "string", - "enum": ["boolean"] + "enum": [ + "boolean" + ] }, "default": { "type": "boolean" } }, - "required": ["key", "type"], + "required": [ + "key", + "type" + ], "additionalProperties": false }, "Form.MultiselectField": { @@ -15260,7 +18637,9 @@ }, "type": { "type": "string", - "enum": ["multiselect"] + "enum": [ + "multiselect" + ] }, "options": { "type": "array", @@ -15294,7 +18673,11 @@ } } }, - "required": ["key", "type", "options"], + "required": [ + "key", + "type", + "options" + ], "additionalProperties": false }, "Form.FormInfo": { @@ -15319,7 +18702,9 @@ }, "mode": { "type": "string", - "enum": ["form"] + "enum": [ + "form" + ] }, "fields": { "type": "array", @@ -15344,7 +18729,13 @@ } } }, - "required": ["id", "sessionID", "mode", "fields"], + "required": [ + "id", + "sessionID", + "title", + "mode", + "fields" + ], "additionalProperties": false }, "Form.UrlInfo": { @@ -15369,13 +18760,21 @@ }, "mode": { "type": "string", - "enum": ["url"] + "enum": [ + "url" + ] }, "url": { "type": "string" } }, - "required": ["id", "sessionID", "mode", "url"], + "required": [ + "id", + "sessionID", + "title", + "mode", + "url" + ], "additionalProperties": false }, "Form.CreatePayload": { @@ -15404,7 +18803,10 @@ }, "mode": { "type": "string", - "enum": ["form", "url"] + "enum": [ + "form", + "url" + ] }, "fields": { "anyOf": [ @@ -15446,7 +18848,10 @@ ] } }, - "required": ["mode"], + "required": [ + "title", + "mode" + ], "additionalProperties": false }, "FormNotFoundError": { @@ -15454,7 +18859,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["FormNotFoundError"] + "enum": [ + "FormNotFoundError" + ] }, "id": { "type": "string" @@ -15463,7 +18870,11 @@ "type": "string" } }, - "required": ["_tag", "id", "message"], + "required": [ + "_tag", + "id", + "message" + ], "additionalProperties": false }, "Form.Value": { @@ -15480,21 +18891,31 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] }, @@ -15522,10 +18943,14 @@ "properties": { "status": { "type": "string", - "enum": ["pending"] + "enum": [ + "pending" + ] } }, - "required": ["status"], + "required": [ + "status" + ], "additionalProperties": false }, { @@ -15533,13 +18958,18 @@ "properties": { "status": { "type": "string", - "enum": ["answered"] + "enum": [ + "answered" + ] }, "answer": { "$ref": "#/components/schemas/Form.Answer" } }, - "required": ["status", "answer"], + "required": [ + "status", + "answer" + ], "additionalProperties": false }, { @@ -15547,10 +18977,14 @@ "properties": { "status": { "type": "string", - "enum": ["cancelled"] + "enum": [ + "cancelled" + ] } }, - "required": ["status"], + "required": [ + "status" + ], "additionalProperties": false } ] @@ -15562,7 +18996,9 @@ "$ref": "#/components/schemas/Form.Answer" } }, - "required": ["answer"], + "required": [ + "answer" + ], "additionalProperties": false }, "FormAlreadySettledError": { @@ -15570,7 +19006,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["FormAlreadySettledError"] + "enum": [ + "FormAlreadySettledError" + ] }, "id": { "type": "string" @@ -15579,7 +19017,11 @@ "type": "string" } }, - "required": ["_tag", "id", "message"], + "required": [ + "_tag", + "id", + "message" + ], "additionalProperties": false }, "FormInvalidAnswerError": { @@ -15587,7 +19029,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["FormInvalidAnswerError"] + "enum": [ + "FormInvalidAnswerError" + ] }, "id": { "type": "string" @@ -15596,7 +19040,11 @@ "type": "string" } }, - "required": ["_tag", "id", "message"], + "required": [ + "_tag", + "id", + "message" + ], "additionalProperties": false }, "PermissionV2.Source": { @@ -15606,7 +19054,9 @@ "properties": { "type": { "type": "string", - "enum": ["tool"] + "enum": [ + "tool" + ] }, "messageID": { "type": "string" @@ -15615,7 +19065,11 @@ "type": "string" } }, - "required": ["type", "messageID", "callID"], + "required": [ + "type", + "messageID", + "callID" + ], "additionalProperties": false } ] @@ -15661,7 +19115,12 @@ "$ref": "#/components/schemas/PermissionV2.Source" } }, - "required": ["id", "sessionID", "action", "resources"], + "required": [ + "id", + "sessionID", + "action", + "resources" + ], "additionalProperties": false }, "PermissionSaved.Info": { @@ -15680,7 +19139,12 @@ "type": "string" } }, - "required": ["id", "projectID", "action", "resource"], + "required": [ + "id", + "projectID", + "action", + "resource" + ], "additionalProperties": false }, "PermissionNotFoundError": { @@ -15688,7 +19152,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["PermissionNotFoundError"] + "enum": [ + "PermissionNotFoundError" + ] }, "requestID": { "type": "string" @@ -15697,12 +19163,20 @@ "type": "string" } }, - "required": ["_tag", "requestID", "message"], + "required": [ + "_tag", + "requestID", + "message" + ], "additionalProperties": false }, "PermissionV2.Reply": { "type": "string", - "enum": ["once", "always", "reject"] + "enum": [ + "once", + "always", + "reject" + ] }, "FileSystem.Entry": { "type": "object", @@ -15712,13 +19186,19 @@ }, "type": { "type": "string", - "enum": ["file", "directory"] + "enum": [ + "file", + "directory" + ] } }, - "required": ["path", "type"], + "required": [ + "path", + "type" + ], "additionalProperties": false }, - "CommandV2.Info": { + "Command.Info": { "type": "object", "properties": { "name": { @@ -15740,12 +19220,18 @@ "type": "boolean" } }, - "required": ["name", "template"], + "required": [ + "name", + "template" + ], "additionalProperties": false }, - "SkillV2.Info": { + "Skill.Info": { "type": "object", "properties": { + "id": { + "type": "string" + }, "name": { "type": "string" }, @@ -15765,7 +19251,12 @@ "type": "string" } }, - "required": ["name", "location", "content"], + "required": [ + "id", + "name", + "location", + "content" + ], "additionalProperties": false }, "models-dev.refreshed": { @@ -15779,38 +19270,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["models-dev.refreshed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "models-dev.refreshed" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -15826,7 +19296,12 @@ ] } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "integration.updated": { @@ -15840,38 +19315,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["integration.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "integration.updated" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -15887,7 +19341,12 @@ ] } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "integration.connection.updated": { @@ -15901,38 +19360,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["integration.connection.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "integration.connection.updated" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -15944,11 +19382,18 @@ "type": "string" } }, - "required": ["integrationID"], + "required": [ + "integrationID" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "catalog.updated": { @@ -15962,38 +19407,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["catalog.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "catalog.updated" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -16009,7 +19433,12 @@ ] } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "agent.updated": { @@ -16023,38 +19452,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["agent.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "agent.updated" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -16070,10 +19478,15 @@ ] } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, - "SnapshotFileDiff": { + "FileDiff.LegacyInfo": { "type": "object", "properties": { "file": { @@ -16090,15 +19503,26 @@ }, "status": { "type": "string", - "enum": ["added", "deleted", "modified"] + "enum": [ + "added", + "deleted", + "modified" + ] } }, - "required": ["additions", "deletions"], + "required": [ + "additions", + "deletions" + ], "additionalProperties": false }, "PermissionAction": { "type": "string", - "enum": ["allow", "deny", "ask"] + "enum": [ + "allow", + "deny", + "ask" + ] }, "PermissionRule": { "type": "object", @@ -16113,7 +19537,11 @@ "$ref": "#/components/schemas/PermissionAction" } }, - "required": ["permission", "pattern", "action"], + "required": [ + "permission", + "pattern", + "action" + ], "additionalProperties": false }, "PermissionRuleset": { @@ -16122,7 +19550,7 @@ "$ref": "#/components/schemas/PermissionRule" } }, - "Session": { + "SessionV1.Info": { "type": "object", "properties": { "id": { @@ -16176,11 +19604,15 @@ "diffs": { "type": "array", "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" + "$ref": "#/components/schemas/FileDiff.LegacyInfo" } } }, - "required": ["additions", "deletions", "files"], + "required": [ + "additions", + "deletions", + "files" + ], "additionalProperties": false }, "cost": { @@ -16208,11 +19640,19 @@ "type": "number" } }, - "required": ["read", "write"], + "required": [ + "read", + "write" + ], "additionalProperties": false } }, - "required": ["input", "output", "reasoning", "cache"], + "required": [ + "input", + "output", + "reasoning", + "cache" + ], "additionalProperties": false }, "share": { @@ -16222,7 +19662,9 @@ "type": "string" } }, - "required": ["url"], + "required": [ + "url" + ], "additionalProperties": false }, "title": { @@ -16244,7 +19686,10 @@ "type": "string" } }, - "required": ["id", "providerID"], + "required": [ + "id", + "providerID" + ], "additionalProperties": false }, "version": { @@ -16284,7 +19729,10 @@ "type": "number" } }, - "required": ["created", "updated"], + "required": [ + "created", + "updated" + ], "additionalProperties": false }, "permission": { @@ -16316,11 +19764,21 @@ "type": "string" } }, - "required": ["messageID"], + "required": [ + "messageID" + ], "additionalProperties": false } }, - "required": ["id", "slug", "projectID", "directory", "title", "version", "time"], + "required": [ + "id", + "slug", + "projectID", + "directory", + "title", + "version", + "time" + ], "additionalProperties": false }, "session.created": { @@ -16334,12 +19792,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.created"] + "enum": [ + "session.created" + ] }, "durable": { "type": "object", @@ -16356,15 +19819,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -16382,14 +19847,23 @@ ] }, "info": { - "$ref": "#/components/schemas/Session" + "$ref": "#/components/schemas/SessionV1.Info" } }, - "required": ["sessionID", "info"], + "required": [ + "sessionID", + "info" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, "session.updated": { @@ -16403,12 +19877,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.updated"] + "enum": [ + "session.updated" + ] }, "durable": { "type": "object", @@ -16425,15 +19904,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -16451,17 +19932,26 @@ ] }, "info": { - "$ref": "#/components/schemas/Session" + "$ref": "#/components/schemas/SessionV1.Info" } }, - "required": ["sessionID", "info"], + "required": [ + "sessionID", + "info" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.deleted": { + "session.deleted1": { "type": "object", "properties": { "id": { @@ -16472,12 +19962,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.deleted"] + "enum": [ + "session.deleted" + ] }, "durable": { "type": "object", @@ -16494,15 +19989,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -16520,14 +20017,23 @@ ] }, "info": { - "$ref": "#/components/schemas/Session" + "$ref": "#/components/schemas/SessionV1.Info" } }, - "required": ["sessionID", "info"], + "required": [ + "sessionID", + "info" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, "JSONSchema": { @@ -16540,10 +20046,14 @@ "properties": { "type": { "type": "string", - "enum": ["text"] + "enum": [ + "text" + ] } }, - "required": ["type"], + "required": [ + "type" + ], "additionalProperties": false }, { @@ -16551,7 +20061,9 @@ "properties": { "type": { "type": "string", - "enum": ["json_schema"] + "enum": [ + "json_schema" + ] }, "schema": { "$ref": "#/components/schemas/JSONSchema" @@ -16579,7 +20091,10 @@ ] } }, - "required": ["type", "schema"], + "required": [ + "type", + "schema" + ], "additionalProperties": false } ] @@ -16605,7 +20120,9 @@ }, "role": { "type": "string", - "enum": ["user"] + "enum": [ + "user" + ] }, "time": { "type": "object", @@ -16619,7 +20136,9 @@ ] } }, - "required": ["created"], + "required": [ + "created" + ], "additionalProperties": false }, "format": { @@ -16660,11 +20179,13 @@ "diffs": { "type": "array", "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" + "$ref": "#/components/schemas/FileDiff.LegacyInfo" } } }, - "required": ["diffs"], + "required": [ + "diffs" + ], "additionalProperties": false }, { @@ -16695,7 +20216,10 @@ ] } }, - "required": ["providerID", "modelID"], + "required": [ + "providerID", + "modelID" + ], "additionalProperties": false }, "system": { @@ -16722,7 +20246,14 @@ ] } }, - "required": ["id", "sessionID", "role", "time", "agent", "model"], + "required": [ + "id", + "sessionID", + "role", + "time", + "agent", + "model" + ], "additionalProperties": false }, "ProviderAuthError": { @@ -16730,7 +20261,9 @@ "properties": { "name": { "type": "string", - "enum": ["ProviderAuthError"] + "enum": [ + "ProviderAuthError" + ] }, "data": { "type": "object", @@ -16742,11 +20275,17 @@ "type": "string" } }, - "required": ["providerID", "message"], + "required": [ + "providerID", + "message" + ], "additionalProperties": false } }, - "required": ["name", "data"], + "required": [ + "name", + "data" + ], "additionalProperties": false }, "UnknownError1": { @@ -16754,7 +20293,9 @@ "properties": { "name": { "type": "string", - "enum": ["UnknownError"] + "enum": [ + "UnknownError" + ] }, "data": { "type": "object", @@ -16773,11 +20314,16 @@ ] } }, - "required": ["message"], + "required": [ + "message" + ], "additionalProperties": false } }, - "required": ["name", "data"], + "required": [ + "name", + "data" + ], "additionalProperties": false }, "MessageOutputLengthError": { @@ -16785,7 +20331,9 @@ "properties": { "name": { "type": "string", - "enum": ["MessageOutputLengthError"] + "enum": [ + "MessageOutputLengthError" + ] }, "data": { "anyOf": [ @@ -16798,7 +20346,10 @@ ] } }, - "required": ["name", "data"], + "required": [ + "name", + "data" + ], "additionalProperties": false }, "MessageAbortedError": { @@ -16806,7 +20357,9 @@ "properties": { "name": { "type": "string", - "enum": ["MessageAbortedError"] + "enum": [ + "MessageAbortedError" + ] }, "data": { "type": "object", @@ -16815,11 +20368,16 @@ "type": "string" } }, - "required": ["message"], + "required": [ + "message" + ], "additionalProperties": false } }, - "required": ["name", "data"], + "required": [ + "name", + "data" + ], "additionalProperties": false }, "StructuredOutputError": { @@ -16827,7 +20385,9 @@ "properties": { "name": { "type": "string", - "enum": ["StructuredOutputError"] + "enum": [ + "StructuredOutputError" + ] }, "data": { "type": "object", @@ -16844,11 +20404,17 @@ ] } }, - "required": ["message", "retries"], + "required": [ + "message", + "retries" + ], "additionalProperties": false } }, - "required": ["name", "data"], + "required": [ + "name", + "data" + ], "additionalProperties": false }, "ContextOverflowError": { @@ -16856,7 +20422,9 @@ "properties": { "name": { "type": "string", - "enum": ["ContextOverflowError"] + "enum": [ + "ContextOverflowError" + ] }, "data": { "type": "object", @@ -16875,11 +20443,16 @@ ] } }, - "required": ["message"], + "required": [ + "message" + ], "additionalProperties": false } }, - "required": ["name", "data"], + "required": [ + "name", + "data" + ], "additionalProperties": false }, "ContentFilterError": { @@ -16887,7 +20460,9 @@ "properties": { "name": { "type": "string", - "enum": ["ContentFilterError"] + "enum": [ + "ContentFilterError" + ] }, "data": { "type": "object", @@ -16896,11 +20471,16 @@ "type": "string" } }, - "required": ["message"], + "required": [ + "message" + ], "additionalProperties": false } }, - "required": ["name", "data"], + "required": [ + "name", + "data" + ], "additionalProperties": false }, "APIError": { @@ -16908,7 +20488,9 @@ "properties": { "name": { "type": "string", - "enum": ["APIError"] + "enum": [ + "APIError" + ] }, "data": { "type": "object", @@ -16971,11 +20553,17 @@ ] } }, - "required": ["message", "isRetryable"], + "required": [ + "message", + "isRetryable" + ], "additionalProperties": false } }, - "required": ["name", "data"], + "required": [ + "name", + "data" + ], "additionalProperties": false }, "AssistantMessage": { @@ -16999,7 +20587,9 @@ }, "role": { "type": "string", - "enum": ["assistant"] + "enum": [ + "assistant" + ] }, "time": { "type": "object", @@ -17028,7 +20618,9 @@ ] } }, - "required": ["created"], + "required": [ + "created" + ], "additionalProperties": false }, "error": { @@ -17096,7 +20688,10 @@ "type": "string" } }, - "required": ["cwd", "root"], + "required": [ + "cwd", + "root" + ], "additionalProperties": false }, "summary": { @@ -17144,11 +20739,19 @@ "type": "number" } }, - "required": ["read", "write"], + "required": [ + "read", + "write" + ], "additionalProperties": false } }, - "required": ["input", "output", "reasoning", "cache"], + "required": [ + "input", + "output", + "reasoning", + "cache" + ], "additionalProperties": false }, "structured": { @@ -17217,12 +20820,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["message.updated"] + "enum": [ + "message.updated" + ] }, "durable": { "type": "object", @@ -17239,15 +20847,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -17268,11 +20878,20 @@ "$ref": "#/components/schemas/Message" } }, - "required": ["sessionID", "info"], + "required": [ + "sessionID", + "info" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, "message.removed": { @@ -17286,12 +20905,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["message.removed"] + "enum": [ + "message.removed" + ] }, "durable": { "type": "object", @@ -17308,15 +20932,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -17342,11 +20968,20 @@ ] } }, - "required": ["sessionID", "messageID"], + "required": [ + "sessionID", + "messageID" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, "TextPart": { @@ -17378,7 +21013,9 @@ }, "type": { "type": "string", - "enum": ["text"] + "enum": [ + "text" + ] }, "text": { "type": "string" @@ -17432,7 +21069,9 @@ ] } }, - "required": ["start"], + "required": [ + "start" + ], "additionalProperties": false }, { @@ -17451,7 +21090,13 @@ ] } }, - "required": ["id", "sessionID", "messageID", "type", "text"], + "required": [ + "id", + "sessionID", + "messageID", + "type", + "text" + ], "additionalProperties": false }, "SubtaskPart": { @@ -17483,7 +21128,9 @@ }, "type": { "type": "string", - "enum": ["subtask"] + "enum": [ + "subtask" + ] }, "prompt": { "type": "string" @@ -17506,7 +21153,10 @@ "type": "string" } }, - "required": ["providerID", "modelID"], + "required": [ + "providerID", + "modelID" + ], "additionalProperties": false }, { @@ -17525,7 +21175,15 @@ ] } }, - "required": ["id", "sessionID", "messageID", "type", "prompt", "description", "agent"], + "required": [ + "id", + "sessionID", + "messageID", + "type", + "prompt", + "description", + "agent" + ], "additionalProperties": false }, "ReasoningPart": { @@ -17557,7 +21215,9 @@ }, "type": { "type": "string", - "enum": ["reasoning"] + "enum": [ + "reasoning" + ] }, "text": { "type": "string" @@ -17599,11 +21259,20 @@ ] } }, - "required": ["start"], + "required": [ + "start" + ], "additionalProperties": false } }, - "required": ["id", "sessionID", "messageID", "type", "text", "time"], + "required": [ + "id", + "sessionID", + "messageID", + "type", + "text", + "time" + ], "additionalProperties": false }, "FilePartSourceText": { @@ -17619,7 +21288,11 @@ "type": "number" } }, - "required": ["value", "start", "end"], + "required": [ + "value", + "start", + "end" + ], "additionalProperties": false }, "FileSource": { @@ -17630,13 +21303,19 @@ }, "type": { "type": "string", - "enum": ["file"] + "enum": [ + "file" + ] }, "path": { "type": "string" } }, - "required": ["text", "type", "path"], + "required": [ + "text", + "type", + "path" + ], "additionalProperties": false }, "Range": { @@ -17662,7 +21341,10 @@ ] } }, - "required": ["line", "character"], + "required": [ + "line", + "character" + ], "additionalProperties": false }, "end": { @@ -17685,11 +21367,17 @@ ] } }, - "required": ["line", "character"], + "required": [ + "line", + "character" + ], "additionalProperties": false } }, - "required": ["start", "end"], + "required": [ + "start", + "end" + ], "additionalProperties": false }, "SymbolSource": { @@ -17700,7 +21388,9 @@ }, "type": { "type": "string", - "enum": ["symbol"] + "enum": [ + "symbol" + ] }, "path": { "type": "string" @@ -17720,7 +21410,14 @@ ] } }, - "required": ["text", "type", "path", "range", "name", "kind"], + "required": [ + "text", + "type", + "path", + "range", + "name", + "kind" + ], "additionalProperties": false }, "ResourceSource": { @@ -17731,7 +21428,9 @@ }, "type": { "type": "string", - "enum": ["resource"] + "enum": [ + "resource" + ] }, "clientName": { "type": "string" @@ -17740,7 +21439,12 @@ "type": "string" } }, - "required": ["text", "type", "clientName", "uri"], + "required": [ + "text", + "type", + "clientName", + "uri" + ], "additionalProperties": false }, "FilePartSource": { @@ -17785,7 +21489,9 @@ }, "type": { "type": "string", - "enum": ["file"] + "enum": [ + "file" + ] }, "mime": { "type": "string" @@ -17814,7 +21520,14 @@ ] } }, - "required": ["id", "sessionID", "messageID", "type", "mime", "url"], + "required": [ + "id", + "sessionID", + "messageID", + "type", + "mime", + "url" + ], "additionalProperties": false }, "ToolStatePending": { @@ -17822,7 +21535,9 @@ "properties": { "status": { "type": "string", - "enum": ["pending"] + "enum": [ + "pending" + ] }, "input": { "type": "object" @@ -17831,7 +21546,11 @@ "type": "string" } }, - "required": ["status", "input", "raw"], + "required": [ + "status", + "input", + "raw" + ], "additionalProperties": false }, "ToolStateRunning": { @@ -17839,7 +21558,9 @@ "properties": { "status": { "type": "string", - "enum": ["running"] + "enum": [ + "running" + ] }, "input": { "type": "object" @@ -17876,11 +21597,17 @@ ] } }, - "required": ["start"], + "required": [ + "start" + ], "additionalProperties": false } }, - "required": ["status", "input", "time"], + "required": [ + "status", + "input", + "time" + ], "additionalProperties": false }, "ToolStateCompleted": { @@ -17888,7 +21615,9 @@ "properties": { "status": { "type": "string", - "enum": ["completed"] + "enum": [ + "completed" + ] }, "input": { "type": "object" @@ -17937,7 +21666,10 @@ ] } }, - "required": ["start", "end"], + "required": [ + "start", + "end" + ], "additionalProperties": false }, "attachments": { @@ -17954,7 +21686,14 @@ ] } }, - "required": ["status", "input", "output", "title", "metadata", "time"], + "required": [ + "status", + "input", + "output", + "title", + "metadata", + "time" + ], "additionalProperties": false }, "ToolStateError": { @@ -17962,7 +21701,9 @@ "properties": { "status": { "type": "string", - "enum": ["error"] + "enum": [ + "error" + ] }, "input": { "type": "object" @@ -18000,11 +21741,19 @@ ] } }, - "required": ["start", "end"], + "required": [ + "start", + "end" + ], "additionalProperties": false } }, - "required": ["status", "input", "error", "time"], + "required": [ + "status", + "input", + "error", + "time" + ], "additionalProperties": false }, "ToolState": { @@ -18052,7 +21801,9 @@ }, "type": { "type": "string", - "enum": ["tool"] + "enum": [ + "tool" + ] }, "callID": { "type": "string" @@ -18074,7 +21825,15 @@ ] } }, - "required": ["id", "sessionID", "messageID", "type", "callID", "tool", "state"], + "required": [ + "id", + "sessionID", + "messageID", + "type", + "callID", + "tool", + "state" + ], "additionalProperties": false }, "StepStartPart": { @@ -18106,7 +21865,9 @@ }, "type": { "type": "string", - "enum": ["step-start"] + "enum": [ + "step-start" + ] }, "snapshot": { "anyOf": [ @@ -18119,7 +21880,12 @@ ] } }, - "required": ["id", "sessionID", "messageID", "type"], + "required": [ + "id", + "sessionID", + "messageID", + "type" + ], "additionalProperties": false }, "StepFinishPart": { @@ -18151,7 +21917,9 @@ }, "type": { "type": "string", - "enum": ["step-finish"] + "enum": [ + "step-finish" + ] }, "reason": { "type": "string" @@ -18201,15 +21969,31 @@ "type": "number" } }, - "required": ["read", "write"], + "required": [ + "read", + "write" + ], "additionalProperties": false } }, - "required": ["input", "output", "reasoning", "cache"], + "required": [ + "input", + "output", + "reasoning", + "cache" + ], "additionalProperties": false } }, - "required": ["id", "sessionID", "messageID", "type", "reason", "cost", "tokens"], + "required": [ + "id", + "sessionID", + "messageID", + "type", + "reason", + "cost", + "tokens" + ], "additionalProperties": false }, "SnapshotPart": { @@ -18241,13 +22025,21 @@ }, "type": { "type": "string", - "enum": ["snapshot"] + "enum": [ + "snapshot" + ] }, "snapshot": { "type": "string" } }, - "required": ["id", "sessionID", "messageID", "type", "snapshot"], + "required": [ + "id", + "sessionID", + "messageID", + "type", + "snapshot" + ], "additionalProperties": false }, "PatchPart": { @@ -18279,7 +22071,9 @@ }, "type": { "type": "string", - "enum": ["patch"] + "enum": [ + "patch" + ] }, "hash": { "type": "string" @@ -18291,7 +22085,14 @@ } } }, - "required": ["id", "sessionID", "messageID", "type", "hash", "files"], + "required": [ + "id", + "sessionID", + "messageID", + "type", + "hash", + "files" + ], "additionalProperties": false }, "AgentPart": { @@ -18323,7 +22124,9 @@ }, "type": { "type": "string", - "enum": ["agent"] + "enum": [ + "agent" + ] }, "name": { "type": "string" @@ -18353,7 +22156,11 @@ ] } }, - "required": ["value", "start", "end"], + "required": [ + "value", + "start", + "end" + ], "additionalProperties": false }, { @@ -18362,7 +22169,13 @@ ] } }, - "required": ["id", "sessionID", "messageID", "type", "name"], + "required": [ + "id", + "sessionID", + "messageID", + "type", + "name" + ], "additionalProperties": false }, "RetryPart": { @@ -18394,7 +22207,9 @@ }, "type": { "type": "string", - "enum": ["retry"] + "enum": [ + "retry" + ] }, "attempt": { "type": "integer", @@ -18419,11 +22234,21 @@ ] } }, - "required": ["created"], + "required": [ + "created" + ], "additionalProperties": false } }, - "required": ["id", "sessionID", "messageID", "type", "attempt", "error", "time"], + "required": [ + "id", + "sessionID", + "messageID", + "type", + "attempt", + "error", + "time" + ], "additionalProperties": false }, "CompactionPart": { @@ -18455,7 +22280,9 @@ }, "type": { "type": "string", - "enum": ["compaction"] + "enum": [ + "compaction" + ] }, "auto": { "type": "boolean" @@ -18486,7 +22313,13 @@ ] } }, - "required": ["id", "sessionID", "messageID", "type", "auto"], + "required": [ + "id", + "sessionID", + "messageID", + "type", + "auto" + ], "additionalProperties": false }, "Part": { @@ -18540,12 +22373,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["message.part.updated"] + "enum": [ + "message.part.updated" + ] }, "durable": { "type": "object", @@ -18562,15 +22400,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -18594,11 +22434,21 @@ "type": "number" } }, - "required": ["sessionID", "part", "time"], + "required": [ + "sessionID", + "part", + "time" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, "message.part.removed": { @@ -18612,12 +22462,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["message.part.removed"] + "enum": [ + "message.part.removed" + ] }, "durable": { "type": "object", @@ -18634,15 +22489,17 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, - "required": ["aggregateID", "seq", "version"], + "required": [ + "aggregateID", + "seq", + "version" + ], "additionalProperties": false }, "location": { @@ -18676,14 +22533,24 @@ ] } }, - "required": ["sessionID", "messageID", "partID"], + "required": [ + "sessionID", + "messageID", + "partID" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "durable", + "data" + ], "additionalProperties": false }, - "session.next.execution.settled": { + "session.usage.updated": { "type": "object", "properties": { "id": { @@ -18694,38 +22561,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.execution.settled"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "session.usage.updated" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -18733,9 +22579,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -18744,22 +22587,30 @@ } ] }, - "outcome": { - "type": "string", - "enum": ["success", "failure", "interrupted"] + "cost": { + "$ref": "#/components/schemas/Money.USD" }, - "error": { - "$ref": "#/components/schemas/Session.Error.Unknown" + "tokens": { + "$ref": "#/components/schemas/TokenUsage.Info" } }, - "required": ["timestamp", "sessionID", "outcome"], + "required": [ + "sessionID", + "cost", + "tokens" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, - "session.next.text.delta": { + "session.text.delta": { "type": "object", "properties": { "id": { @@ -18770,38 +22621,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.text.delta"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "session.text.delta" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -18809,9 +22639,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -18828,21 +22655,36 @@ } ] }, - "textID": { - "type": "string" + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "delta": { "type": "string" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "delta"], + "required": [ + "sessionID", + "assistantMessageID", + "ordinal", + "delta" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, - "session.next.reasoning.delta": { + "session.reasoning.delta": { "type": "object", "properties": { "id": { @@ -18853,38 +22695,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.reasoning.delta"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "session.reasoning.delta" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -18892,9 +22713,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -18911,21 +22729,36 @@ } ] }, - "reasoningID": { - "type": "string" + "ordinal": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] }, "delta": { "type": "string" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "delta"], + "required": [ + "sessionID", + "assistantMessageID", + "ordinal", + "delta" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, - "session.next.tool.input.delta": { + "session.tool.input.delta": { "type": "object", "properties": { "id": { @@ -18936,38 +22769,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.tool.input.delta"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "session.tool.input.delta" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -18975,9 +22787,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -19001,14 +22810,24 @@ "type": "string" } }, - "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "delta"], + "required": [ + "sessionID", + "assistantMessageID", + "callID", + "delta" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, - "session.next.compaction.delta": { + "session.compaction.delta": { "type": "object", "properties": { "id": { @@ -19019,38 +22838,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.next.compaction.delta"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "session.compaction.delta" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -19058,9 +22856,6 @@ "data": { "type": "object", "properties": { - "timestamp": { - "type": "number" - }, "sessionID": { "type": "string", "allOf": [ @@ -19069,26 +22864,26 @@ } ] }, - "messageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, "text": { "type": "string" } }, - "required": ["timestamp", "sessionID", "messageID", "text"], + "required": [ + "sessionID", + "text" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, - "file.edited": { + "filesystem.changed": { "type": "object", "properties": { "id": { @@ -19099,38 +22894,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["file.edited"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "filesystem.changed" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -19140,13 +22914,29 @@ "properties": { "file": { "type": "string" + }, + "event": { + "type": "string", + "enum": [ + "add", + "change", + "unlink" + ] } }, - "required": ["file"], + "required": [ + "file", + "event" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "reference.updated": { @@ -19160,38 +22950,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["reference.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "reference.updated" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -19207,7 +22976,12 @@ ] } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "permission.v2.asked": { @@ -19221,38 +22995,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["permission.v2.asked"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "permission.v2.asked" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -19298,11 +23051,21 @@ "$ref": "#/components/schemas/PermissionV2.Source" } }, - "required": ["id", "sessionID", "action", "resources"], + "required": [ + "id", + "sessionID", + "action", + "resources" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "permission.v2.replied": { @@ -19316,38 +23079,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["permission.v2.replied"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "permission.v2.replied" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -19375,11 +23117,20 @@ "$ref": "#/components/schemas/PermissionV2.Reply" } }, - "required": ["sessionID", "requestID", "reply"], + "required": [ + "sessionID", + "requestID", + "reply" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "plugin.added": { @@ -19393,38 +23144,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["plugin.added"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "plugin.added" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -19436,11 +23166,63 @@ "type": "string" } }, - "required": ["id"], + "required": [ + "id" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "plugin.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "plugin.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "project.directories.updated": { @@ -19454,38 +23236,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["project.directories.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "project.directories.updated" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -19497,11 +23258,18 @@ "type": "string" } }, - "required": ["projectID"], + "required": [ + "projectID" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "command.updated": { @@ -19515,38 +23283,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["command.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "command.updated" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -19562,7 +23309,57 @@ ] } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "config.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "config.updated" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "skill.updated": { @@ -19576,38 +23373,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["skill.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "skill.updated" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -19623,72 +23399,12 @@ ] } }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "file.watcher.updated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["file.watcher.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "event": { - "type": "string", - "enum": ["add", "change", "unlink"] - } - }, - "required": ["file", "event"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "Pty": { @@ -19719,7 +23435,10 @@ }, "status": { "type": "string", - "enum": ["running", "exited"] + "enum": [ + "running", + "exited" + ] }, "pid": { "type": "integer", @@ -19738,7 +23457,15 @@ ] } }, - "required": ["id", "title", "command", "args", "cwd", "status", "pid"], + "required": [ + "id", + "title", + "command", + "args", + "cwd", + "status", + "pid" + ], "additionalProperties": false }, "pty.created": { @@ -19752,38 +23479,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["pty.created"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "pty.created" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -19795,11 +23501,18 @@ "$ref": "#/components/schemas/Pty" } }, - "required": ["info"], + "required": [ + "info" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "pty.updated": { @@ -19813,38 +23526,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["pty.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "pty.updated" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -19856,11 +23548,18 @@ "$ref": "#/components/schemas/Pty" } }, - "required": ["info"], + "required": [ + "info" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "pty.exited": { @@ -19874,38 +23573,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["pty.exited"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "pty.exited" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -19930,11 +23608,19 @@ ] } }, - "required": ["id", "exitCode"], + "required": [ + "id", + "exitCode" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "pty.deleted": { @@ -19948,38 +23634,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["pty.deleted"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "pty.deleted" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -19996,117 +23661,18 @@ ] } }, - "required": ["id"], + "required": [ + "id" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], - "additionalProperties": false - }, - "Shell": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^sh_" - } - ] - }, - "status": { - "type": "string", - "enum": ["running", "exited", "timeout", "killed"] - }, - "command": { - "type": "string" - }, - "cwd": { - "type": "string" - }, - "shell": { - "type": "string" - }, - "file": { - "type": "string" - }, - "pid": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "exit": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - } - ] - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "started": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - } - ] - }, - "completed": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": ["NaN"] - }, - { - "type": "string", - "enum": ["Infinity"] - }, - { - "type": "string", - "enum": ["-Infinity"] - } - ] - } - }, - "required": ["started"], - "additionalProperties": false - } - }, - "required": ["id", "status", "command", "cwd", "shell", "file", "metadata", "time"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "shell.created": { @@ -20120,38 +23686,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["shell.created"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "shell.created" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -20163,11 +23708,18 @@ "$ref": "#/components/schemas/Shell" } }, - "required": ["info"], + "required": [ + "info" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "shell.exited": { @@ -20181,38 +23733,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["shell.exited"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "shell.exited" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -20235,28 +23766,47 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, "status": { "type": "string", - "enum": ["running", "exited", "timeout", "killed"] + "enum": [ + "running", + "exited", + "timeout", + "killed" + ] } }, - "required": ["id", "status"], + "required": [ + "id", + "status" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "shell.deleted": { @@ -20270,38 +23820,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["shell.deleted"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "shell.deleted" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -20318,11 +23847,18 @@ ] } }, - "required": ["id"], + "required": [ + "id" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "QuestionV2.Option": { @@ -20337,7 +23873,10 @@ "description": "Explanation of choice" } }, - "required": ["label", "description"], + "required": [ + "label", + "description" + ], "additionalProperties": false }, "QuestionV2.Info": { @@ -20365,7 +23904,11 @@ "type": "boolean" } }, - "required": ["question", "header", "options"], + "required": [ + "question", + "header", + "options" + ], "additionalProperties": false }, "QuestionV2.Tool": { @@ -20378,7 +23921,10 @@ "type": "string" } }, - "required": ["messageID", "callID"], + "required": [ + "messageID", + "callID" + ], "additionalProperties": false }, "question.v2.asked": { @@ -20392,38 +23938,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["question.v2.asked"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "question.v2.asked" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -20458,11 +23983,20 @@ "$ref": "#/components/schemas/QuestionV2.Tool" } }, - "required": ["id", "sessionID", "questions"], + "required": [ + "id", + "sessionID", + "questions" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "QuestionV2.Answer": { @@ -20482,38 +24016,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["question.v2.replied"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "question.v2.replied" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -20544,11 +24057,20 @@ } } }, - "required": ["sessionID", "requestID", "answers"], + "required": [ + "sessionID", + "requestID", + "answers" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "question.v2.rejected": { @@ -20562,38 +24084,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["question.v2.rejected"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "question.v2.rejected" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -20618,11 +24119,19 @@ ] } }, - "required": ["sessionID", "requestID"], + "required": [ + "sessionID", + "requestID" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "Form.Metadata1": { @@ -20636,7 +24145,10 @@ }, "op": { "type": "string", - "enum": ["eq", "neq"] + "enum": [ + "eq", + "neq" + ] }, "value": { "anyOf": [ @@ -20650,15 +24162,21 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, @@ -20668,7 +24186,11 @@ ] } }, - "required": ["key", "op", "value"], + "required": [ + "key", + "op", + "value" + ], "additionalProperties": false }, "Form.StringField1": { @@ -20694,11 +24216,18 @@ }, "type": { "type": "string", - "enum": ["string"] + "enum": [ + "string" + ] }, "format": { "type": "string", - "enum": ["email", "uri", "date", "date-time"] + "enum": [ + "email", + "uri", + "date", + "date-time" + ] }, "minLength": { "type": "integer", @@ -20735,7 +24264,10 @@ "type": "boolean" } }, - "required": ["key", "type"], + "required": [ + "key", + "type" + ], "additionalProperties": false }, "Form.NumberField1": { @@ -20761,7 +24293,9 @@ }, "type": { "type": "string", - "enum": ["number"] + "enum": [ + "number" + ] }, "minimum": { "anyOf": [ @@ -20770,15 +24304,21 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, @@ -20789,15 +24329,21 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, @@ -20808,20 +24354,29 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] } }, - "required": ["key", "type"], + "required": [ + "key", + "type" + ], "additionalProperties": false }, "Form.IntegerField1": { @@ -20847,7 +24402,9 @@ }, "type": { "type": "string", - "enum": ["integer"] + "enum": [ + "integer" + ] }, "minimum": { "anyOf": [ @@ -20856,15 +24413,21 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, @@ -20875,15 +24438,21 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, @@ -20894,20 +24463,29 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] } }, - "required": ["key", "type"], + "required": [ + "key", + "type" + ], "additionalProperties": false }, "Form.BooleanField1": { @@ -20933,13 +24511,18 @@ }, "type": { "type": "string", - "enum": ["boolean"] + "enum": [ + "boolean" + ] }, "default": { "type": "boolean" } }, - "required": ["key", "type"], + "required": [ + "key", + "type" + ], "additionalProperties": false }, "Form.MultiselectField1": { @@ -20965,7 +24548,9 @@ }, "type": { "type": "string", - "enum": ["multiselect"] + "enum": [ + "multiselect" + ] }, "options": { "type": "array", @@ -20999,7 +24584,11 @@ } } }, - "required": ["key", "type", "options"], + "required": [ + "key", + "type", + "options" + ], "additionalProperties": false }, "Form.FormInfo1": { @@ -21024,7 +24613,9 @@ }, "mode": { "type": "string", - "enum": ["form"] + "enum": [ + "form" + ] }, "fields": { "type": "array", @@ -21049,7 +24640,13 @@ } } }, - "required": ["id", "sessionID", "mode", "fields"], + "required": [ + "id", + "sessionID", + "title", + "mode", + "fields" + ], "additionalProperties": false }, "Form.UrlInfo1": { @@ -21074,13 +24671,21 @@ }, "mode": { "type": "string", - "enum": ["url"] + "enum": [ + "url" + ] }, "url": { "type": "string" } }, - "required": ["id", "sessionID", "mode", "url"], + "required": [ + "id", + "sessionID", + "title", + "mode", + "url" + ], "additionalProperties": false }, "form.created": { @@ -21094,38 +24699,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["form.created"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "form.created" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -21144,11 +24728,18 @@ ] } }, - "required": ["form"], + "required": [ + "form" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "Form.Value1": { @@ -21163,15 +24754,21 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, @@ -21203,38 +24800,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["form.replied"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "form.replied" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -21257,11 +24833,20 @@ "$ref": "#/components/schemas/Form.Answer1" } }, - "required": ["id", "sessionID", "answer"], + "required": [ + "id", + "sessionID", + "answer" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "form.cancelled": { @@ -21275,38 +24860,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["form.cancelled"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "form.cancelled" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -21326,102 +24890,19 @@ "type": "string" } }, - "required": ["id", "sessionID"], + "required": [ + "id", + "sessionID" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], - "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 - }, - "todo.updated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": ["todo.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "todos": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Todo" - } - } - }, - "required": ["sessionID", "todos"], - "additionalProperties": false - } - }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "SessionStatus": { @@ -21431,10 +24912,14 @@ "properties": { "type": { "type": "string", - "enum": ["idle"] + "enum": [ + "idle" + ] } }, - "required": ["type"], + "required": [ + "type" + ], "additionalProperties": false }, { @@ -21442,7 +24927,9 @@ "properties": { "type": { "type": "string", - "enum": ["retry"] + "enum": [ + "retry" + ] }, "attempt": { "type": "integer", @@ -21477,7 +24964,13 @@ "type": "string" } }, - "required": ["reason", "provider", "title", "message", "label"], + "required": [ + "reason", + "provider", + "title", + "message", + "label" + ], "additionalProperties": false }, "next": { @@ -21489,7 +24982,12 @@ ] } }, - "required": ["type", "attempt", "message", "next"], + "required": [ + "type", + "attempt", + "message", + "next" + ], "additionalProperties": false }, { @@ -21497,10 +24995,14 @@ "properties": { "type": { "type": "string", - "enum": ["busy"] + "enum": [ + "busy" + ] } }, - "required": ["type"], + "required": [ + "type" + ], "additionalProperties": false } ] @@ -21516,38 +25018,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.status"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "session.status" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -21567,11 +25048,19 @@ "$ref": "#/components/schemas/SessionStatus" } }, - "required": ["sessionID", "status"], + "required": [ + "sessionID", + "status" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "session.idle": { @@ -21585,38 +25074,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.idle"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "session.idle" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -21633,11 +25101,18 @@ ] } }, - "required": ["sessionID"], + "required": [ + "sessionID" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "tui.prompt.append": { @@ -21651,38 +25126,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["tui.prompt.append"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "tui.prompt.append" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -21694,11 +25148,18 @@ "type": "string" } }, - "required": ["text"], + "required": [ + "text" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "tui.command.execute": { @@ -21712,38 +25173,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["tui.command.execute"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "tui.command.execute" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -21781,11 +25221,18 @@ ] } }, - "required": ["command"], + "required": [ + "command" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "tui.toast.show": { @@ -21799,38 +25246,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["tui.toast.show"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "tui.toast.show" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -21846,7 +25272,12 @@ }, "variant": { "type": "string", - "enum": ["info", "success", "warning", "error"] + "enum": [ + "info", + "success", + "warning", + "error" + ] }, "duration": { "anyOf": [ @@ -21864,11 +25295,19 @@ ] } }, - "required": ["message", "variant"], + "required": [ + "message", + "variant" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "tui.session.select": { @@ -21882,38 +25321,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["tui.session.select"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "tui.session.select" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -21931,11 +25349,18 @@ ] } }, - "required": ["sessionID"], + "required": [ + "sessionID" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "installation.updated": { @@ -21949,38 +25374,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["installation.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "installation.updated" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -21992,11 +25396,18 @@ "type": "string" } }, - "required": ["version"], + "required": [ + "version" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "installation.update-available": { @@ -22010,38 +25421,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["installation.update-available"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "installation.update-available" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -22053,11 +25443,18 @@ "type": "string" } }, - "required": ["version"], + "required": [ + "version" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "vcs.branch.updated": { @@ -22071,38 +25468,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["vcs.branch.updated"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "vcs.branch.updated" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -22117,7 +25493,12 @@ "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "mcp.status.changed": { @@ -22131,38 +25512,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["mcp.status.changed"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "mcp.status.changed" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -22174,11 +25534,65 @@ "type": "string" } }, - "required": ["server"], + "required": [ + "server" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], + "additionalProperties": false + }, + "mcp.resources.changed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "created": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "mcp.resources.changed" + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "server": { + "type": "string" + } + }, + "required": [ + "server" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "permission.asked": { @@ -22192,38 +25606,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["permission.asked"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "permission.asked" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -22277,7 +25670,10 @@ "type": "string" } }, - "required": ["messageID", "callID"], + "required": [ + "messageID", + "callID" + ], "additionalProperties": false }, { @@ -22286,11 +25682,23 @@ ] } }, - "required": ["id", "sessionID", "permission", "patterns", "metadata", "always"], + "required": [ + "id", + "sessionID", + "permission", + "patterns", + "metadata", + "always" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "permission.replied": { @@ -22304,38 +25712,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["permission.replied"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "permission.replied" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -22361,14 +25748,27 @@ }, "reply": { "type": "string", - "enum": ["once", "always", "reject"] + "enum": [ + "once", + "always", + "reject" + ] } }, - "required": ["sessionID", "requestID", "reply"], + "required": [ + "sessionID", + "requestID", + "reply" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "QuestionOption": { @@ -22383,7 +25783,10 @@ "description": "Explanation of choice" } }, - "required": ["label", "description"], + "required": [ + "label", + "description" + ], "additionalProperties": false }, "QuestionInfo": { @@ -22427,7 +25830,11 @@ "description": "Allow typing a custom answer (default: true)" } }, - "required": ["question", "header", "options"], + "required": [ + "question", + "header", + "options" + ], "additionalProperties": false }, "QuestionTool": { @@ -22445,7 +25852,10 @@ "type": "string" } }, - "required": ["messageID", "callID"], + "required": [ + "messageID", + "callID" + ], "additionalProperties": false }, "question.asked": { @@ -22459,38 +25869,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["question.asked"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "question.asked" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -22532,11 +25921,20 @@ ] } }, - "required": ["id", "sessionID", "questions"], + "required": [ + "id", + "sessionID", + "questions" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "QuestionAnswer": { @@ -22556,38 +25954,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["question.replied"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "question.replied" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -22618,11 +25995,20 @@ } } }, - "required": ["sessionID", "requestID", "answers"], + "required": [ + "sessionID", + "requestID", + "answers" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "question.rejected": { @@ -22636,38 +26022,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["question.rejected"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "question.rejected" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -22692,11 +26057,19 @@ ] } }, - "required": ["sessionID", "requestID"], + "required": [ + "sessionID", + "requestID" + ], "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "session.error": { @@ -22710,38 +26083,17 @@ } ] }, + "created": { + "type": "number" + }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": ["session.error"] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false + "enum": [ + "session.error" + ] }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -22803,7 +26155,12 @@ "additionalProperties": false } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "created", + "type", + "data" + ], "additionalProperties": false }, "V2Event.server.connected": { @@ -22827,39 +26184,6 @@ } ] }, - "durable": { - "anyOf": [ - { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } - ] - } - }, - "required": ["aggregateID", "seq", "version"], - "additionalProperties": false - }, - { - "type": "null" - } - ] - }, "location": { "anyOf": [ { @@ -22872,7 +26196,9 @@ }, "type": { "type": "string", - "enum": ["server.connected"] + "enum": [ + "server.connected" + ] }, "data": { "anyOf": [ @@ -22885,7 +26211,11 @@ ] } }, - "required": ["id", "type", "data"], + "required": [ + "id", + "type", + "data" + ], "additionalProperties": false }, "V2Event": { @@ -22912,7 +26242,7 @@ "$ref": "#/components/schemas/session.updated" }, { - "$ref": "#/components/schemas/session.deleted" + "$ref": "#/components/schemas/session.deleted1" }, { "$ref": "#/components/schemas/message.updated" @@ -22927,115 +26257,136 @@ "$ref": "#/components/schemas/message.part.removed" }, { - "$ref": "#/components/schemas/session.next.agent.switched" + "$ref": "#/components/schemas/session.agent.selected" }, { - "$ref": "#/components/schemas/session.next.model.switched" + "$ref": "#/components/schemas/session.model.selected" }, { - "$ref": "#/components/schemas/session.next.moved" + "$ref": "#/components/schemas/session.moved" }, { - "$ref": "#/components/schemas/session.next.renamed" + "$ref": "#/components/schemas/session.renamed" }, { - "$ref": "#/components/schemas/session.next.forked" + "$ref": "#/components/schemas/session.usage.updated" }, { - "$ref": "#/components/schemas/session.next.prompted" + "$ref": "#/components/schemas/session.deleted" }, { - "$ref": "#/components/schemas/session.next.prompt.admitted" + "$ref": "#/components/schemas/session.forked" }, { - "$ref": "#/components/schemas/session.next.execution.settled" + "$ref": "#/components/schemas/session.prompt.promoted" }, { - "$ref": "#/components/schemas/session.next.context.updated" + "$ref": "#/components/schemas/session.prompt.admitted" }, { - "$ref": "#/components/schemas/session.next.synthetic" + "$ref": "#/components/schemas/session.execution.started" }, { - "$ref": "#/components/schemas/session.next.skill.activated" + "$ref": "#/components/schemas/session.execution.succeeded" }, { - "$ref": "#/components/schemas/session.next.shell.started" + "$ref": "#/components/schemas/session.execution.failed" }, { - "$ref": "#/components/schemas/session.next.shell.ended" + "$ref": "#/components/schemas/session.execution.interrupted" }, { - "$ref": "#/components/schemas/session.next.step.started" + "$ref": "#/components/schemas/session.instructions.updated" }, { - "$ref": "#/components/schemas/session.next.step.ended" + "$ref": "#/components/schemas/session.synthetic" }, { - "$ref": "#/components/schemas/session.next.step.failed" + "$ref": "#/components/schemas/session.skill.activated" }, { - "$ref": "#/components/schemas/session.next.text.started" + "$ref": "#/components/schemas/session.shell.started" }, { - "$ref": "#/components/schemas/session.next.text.delta" + "$ref": "#/components/schemas/session.shell.ended" }, { - "$ref": "#/components/schemas/session.next.text.ended" + "$ref": "#/components/schemas/session.step.started" }, { - "$ref": "#/components/schemas/session.next.reasoning.started" + "$ref": "#/components/schemas/session.step.ended" }, { - "$ref": "#/components/schemas/session.next.reasoning.delta" + "$ref": "#/components/schemas/session.step.failed" }, { - "$ref": "#/components/schemas/session.next.reasoning.ended" + "$ref": "#/components/schemas/session.text.started" }, { - "$ref": "#/components/schemas/session.next.tool.input.started" + "$ref": "#/components/schemas/session.text.delta" }, { - "$ref": "#/components/schemas/session.next.tool.input.delta" + "$ref": "#/components/schemas/session.text.ended" }, { - "$ref": "#/components/schemas/session.next.tool.input.ended" + "$ref": "#/components/schemas/session.reasoning.started" }, { - "$ref": "#/components/schemas/session.next.tool.called" + "$ref": "#/components/schemas/session.reasoning.delta" }, { - "$ref": "#/components/schemas/session.next.tool.progress" + "$ref": "#/components/schemas/session.reasoning.ended" }, { - "$ref": "#/components/schemas/session.next.tool.success" + "$ref": "#/components/schemas/session.tool.input.started" }, { - "$ref": "#/components/schemas/session.next.tool.failed" + "$ref": "#/components/schemas/session.tool.input.delta" }, { - "$ref": "#/components/schemas/session.next.retried" + "$ref": "#/components/schemas/session.tool.input.ended" }, { - "$ref": "#/components/schemas/session.next.compaction.started" + "$ref": "#/components/schemas/session.tool.called" }, { - "$ref": "#/components/schemas/session.next.compaction.delta" + "$ref": "#/components/schemas/session.tool.progress" }, { - "$ref": "#/components/schemas/session.next.compaction.ended" + "$ref": "#/components/schemas/session.tool.success" }, { - "$ref": "#/components/schemas/session.next.revert.staged" + "$ref": "#/components/schemas/session.tool.failed" }, { - "$ref": "#/components/schemas/session.next.revert.cleared" + "$ref": "#/components/schemas/session.retry.scheduled" }, { - "$ref": "#/components/schemas/session.next.revert.committed" + "$ref": "#/components/schemas/session.compaction.admitted" }, { - "$ref": "#/components/schemas/file.edited" + "$ref": "#/components/schemas/session.compaction.started" + }, + { + "$ref": "#/components/schemas/session.compaction.delta" + }, + { + "$ref": "#/components/schemas/session.compaction.ended" + }, + { + "$ref": "#/components/schemas/session.compaction.failed" + }, + { + "$ref": "#/components/schemas/session.revert.staged" + }, + { + "$ref": "#/components/schemas/session.revert.cleared" + }, + { + "$ref": "#/components/schemas/session.revert.committed" + }, + { + "$ref": "#/components/schemas/filesystem.changed" }, { "$ref": "#/components/schemas/reference.updated" @@ -23049,6 +26400,9 @@ { "$ref": "#/components/schemas/plugin.added" }, + { + "$ref": "#/components/schemas/plugin.updated" + }, { "$ref": "#/components/schemas/project.directories.updated" }, @@ -23056,10 +26410,10 @@ "$ref": "#/components/schemas/command.updated" }, { - "$ref": "#/components/schemas/skill.updated" + "$ref": "#/components/schemas/config.updated" }, { - "$ref": "#/components/schemas/file.watcher.updated" + "$ref": "#/components/schemas/skill.updated" }, { "$ref": "#/components/schemas/pty.created" @@ -23100,9 +26454,6 @@ { "$ref": "#/components/schemas/form.cancelled" }, - { - "$ref": "#/components/schemas/todo.updated" - }, { "$ref": "#/components/schemas/session.status" }, @@ -23133,6 +26484,9 @@ { "$ref": "#/components/schemas/mcp.status.changed" }, + { + "$ref": "#/components/schemas/mcp.resources.changed" + }, { "$ref": "#/components/schemas/permission.asked" }, @@ -23163,64 +26517,14 @@ }, "contentMediaType": "application/json" }, - "EventLog.Hint": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["log.hint"] - }, - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - } - }, - "required": ["type", "aggregateID", "seq"], - "additionalProperties": false, - "description": "Payload-free change hint: the aggregate's durable log advanced to at least seq. Hints coalesce under backpressure (latest per aggregate) and are never a delivery guarantee." - }, - "EventLog.SweepRequired": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["log.sweep_required"] - } - }, - "required": ["type"], - "additionalProperties": false, - "description": "Hints may have been lost; treat every aggregate as potentially dirty and recover via bounded sweep plus durable log reads. Emitted first on every (re)subscribe." - }, - "EventLog.Change": { - "anyOf": [ - { - "$ref": "#/components/schemas/EventLog.Hint" - }, - { - "$ref": "#/components/schemas/EventLog.SweepRequired" - } - ] - }, - "EventLog.ChangeStream": { - "type": "string", - "contentSchema": { - "$ref": "#/components/schemas/EventLog.Change" - }, - "contentMediaType": "application/json" - }, "PtyNotFoundError": { "type": "object", "properties": { "_tag": { "type": "string", - "enum": ["PtyNotFoundError"] + "enum": [ + "PtyNotFoundError" + ] }, "ptyID": { "type": "string" @@ -23229,7 +26533,11 @@ "type": "string" } }, - "required": ["_tag", "ptyID", "message"], + "required": [ + "_tag", + "ptyID", + "message" + ], "additionalProperties": false }, "PtyTicket.ConnectToken": { @@ -23247,7 +26555,10 @@ ] } }, - "required": ["ticket", "expires_in"], + "required": [ + "ticket", + "expires_in" + ], "additionalProperties": false }, "ForbiddenError": { @@ -23255,13 +26566,18 @@ "properties": { "_tag": { "type": "string", - "enum": ["ForbiddenError"] + "enum": [ + "ForbiddenError" + ] }, "message": { "type": "string" } }, - "required": ["_tag", "message"], + "required": [ + "_tag", + "message" + ], "additionalProperties": false }, "Shell1": { @@ -23277,7 +26593,12 @@ }, "status": { "type": "string", - "enum": ["running", "exited", "timeout", "killed"] + "enum": [ + "running", + "exited", + "timeout", + "killed" + ] }, "command": { "type": "string" @@ -23308,21 +26629,31 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] }, @@ -23341,21 +26672,31 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] }, @@ -23368,30 +26709,51 @@ }, { "type": "string", - "enum": ["NaN"] + "enum": [ + "NaN" + ] }, { "type": "string", - "enum": ["Infinity"] + "enum": [ + "Infinity" + ] }, { "type": "string", - "enum": ["-Infinity"] + "enum": [ + "-Infinity" + ] } ] }, { "type": "string", - "enum": ["Infinity", "-Infinity", "NaN"] + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] } ] } }, - "required": ["started"], + "required": [ + "started" + ], "additionalProperties": false } }, - "required": ["id", "status", "command", "cwd", "shell", "file", "metadata", "time"], + "required": [ + "id", + "status", + "command", + "cwd", + "shell", + "file", + "metadata", + "time" + ], "additionalProperties": false }, "ShellNotFoundError": { @@ -23399,7 +26761,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["ShellNotFoundError"] + "enum": [ + "ShellNotFoundError" + ] }, "id": { "type": "string" @@ -23408,7 +26772,11 @@ "type": "string" } }, - "required": ["_tag", "id", "message"], + "required": [ + "_tag", + "id", + "message" + ], "additionalProperties": false }, "QuestionV2.Request": { @@ -23441,7 +26809,11 @@ "$ref": "#/components/schemas/QuestionV2.Tool" } }, - "required": ["id", "sessionID", "questions"], + "required": [ + "id", + "sessionID", + "questions" + ], "additionalProperties": false }, "QuestionV2.Reply": { @@ -23455,7 +26827,9 @@ "description": "User answers in order of questions (each answer is an array of selected labels)" } }, - "required": ["answers"], + "required": [ + "answers" + ], "additionalProperties": false }, "QuestionNotFoundError": { @@ -23463,7 +26837,9 @@ "properties": { "_tag": { "type": "string", - "enum": ["QuestionNotFoundError"] + "enum": [ + "QuestionNotFoundError" + ] }, "requestID": { "type": "string" @@ -23472,7 +26848,11 @@ "type": "string" } }, - "required": ["_tag", "requestID", "message"], + "required": [ + "_tag", + "requestID", + "message" + ], "additionalProperties": false }, "Reference.LocalSource": { @@ -23480,7 +26860,9 @@ "properties": { "type": { "type": "string", - "enum": ["local"] + "enum": [ + "local" + ] }, "path": { "type": "string" @@ -23492,7 +26874,10 @@ "type": "boolean" } }, - "required": ["type", "path"], + "required": [ + "type", + "path" + ], "additionalProperties": false }, "Reference.GitSource": { @@ -23500,7 +26885,9 @@ "properties": { "type": { "type": "string", - "enum": ["git"] + "enum": [ + "git" + ] }, "repository": { "type": "string" @@ -23515,7 +26902,10 @@ "type": "boolean" } }, - "required": ["type", "repository"], + "required": [ + "type", + "repository" + ], "additionalProperties": false }, "Reference.Source": { @@ -23547,7 +26937,11 @@ "$ref": "#/components/schemas/Reference.Source" } }, - "required": ["name", "path", "source"], + "required": [ + "name", + "path", + "source" + ], "additionalProperties": false }, "ProjectCopy.Copy": { @@ -23557,7 +26951,9 @@ "type": "string" } }, - "required": ["directory"], + "required": [ + "directory" + ], "additionalProperties": false }, "ProjectCopyError": { @@ -23565,7 +26961,9 @@ "properties": { "name": { "type": "string", - "enum": ["ProjectCopyError"] + "enum": [ + "ProjectCopyError" + ] }, "data": { "type": "object", @@ -23584,11 +26982,16 @@ ] } }, - "required": ["message"], + "required": [ + "message" + ], "additionalProperties": false } }, - "required": ["name", "data"], + "required": [ + "name", + "data" + ], "additionalProperties": false }, "Vcs.FileStatus": { @@ -23615,15 +27018,27 @@ }, "status": { "type": "string", - "enum": ["added", "deleted", "modified"] + "enum": [ + "added", + "deleted", + "modified" + ] } }, - "required": ["file", "additions", "deletions", "status"], + "required": [ + "file", + "additions", + "deletions", + "status" + ], "additionalProperties": false }, "Vcs.Mode": { "type": "string", - "enum": ["working", "branch"] + "enum": [ + "working", + "branch" + ] } }, "securitySchemes": {} @@ -23631,28 +27046,31 @@ "security": [], "tags": [ { - "name": "server.health" + "name": "health" }, { - "name": "server.location" + "name": "server" }, { - "name": "server.agent" + "name": "location" }, { - "name": "plugins", + "name": "agent" + }, + { + "name": "plugin", "description": "Experimental plugin routes." }, { - "name": "sessions", + "name": "session", "description": "Experimental session routes." }, { - "name": "messages", + "name": "session", "description": "Experimental message routes." }, { - "name": "models", + "name": "model", "description": "Experimental model routes." }, { @@ -23660,30 +27078,30 @@ "description": "Experimental one-shot generation routes." }, { - "name": "providers", + "name": "provider", "description": "Experimental provider routes." }, { - "name": "integrations", + "name": "integration", "description": "Integration discovery and authentication routes." }, { "name": "mcp", - "description": "MCP server status routes." + "description": "MCP server and resource routes." }, { - "name": "server.credential" + "name": "credential" }, { - "name": "projects", + "name": "project", "description": "Location-scoped project routes." }, { - "name": "forms", + "name": "form", "description": "Session form routes." }, { - "name": "permissions", + "name": "permission", "description": "Experimental permission routes." }, { @@ -23691,15 +27109,15 @@ "description": "Experimental location-scoped filesystem routes." }, { - "name": "commands", + "name": "command", "description": "Experimental command routes." }, { - "name": "skills", + "name": "skill", "description": "Experimental skill routes." }, { - "name": "events", + "name": "event", "description": "Experimental event stream routes." }, { @@ -23711,7 +27129,7 @@ "description": "Experimental location-scoped shell command routes." }, { - "name": "session questions", + "name": "question", "description": "Experimental session question routes." }, { @@ -23725,6 +27143,9 @@ { "name": "vcs", "description": "Location-scoped version control routes." + }, + { + "name": "debug" } ] } diff --git a/packages/codemode/test/lexical-test262.test.ts b/packages/codemode/test/lexical-test262.test.ts new file mode 100644 index 0000000000..6f67a9acb9 --- /dev/null +++ b/packages/codemode/test/lexical-test262.test.ts @@ -0,0 +1,188 @@ +/* + * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75: + * - test/language/statements/let/global-use-before-initialization-in-prior-statement.js + * - test/language/statements/let/block-local-use-before-initialization-in-prior-statement.js + * - test/language/statements/const/global-use-before-initialization-in-prior-statement.js + * - test/language/statements/const/block-local-use-before-initialization-in-prior-statement.js + * - test/language/statements/let/block-local-use-before-initialization-in-declaration-statement.js + * - test/language/statements/const/block-local-use-before-initialization-in-declaration-statement.js + * - test/language/statements/let/block-local-closure-set-before-initialization.js + * - test/language/statements/for-of/head-let-bound-names-fordecl-tdz.js + * - test/language/statements/for-in/head-let-bound-names-fordecl-tdz.js + * - test/language/statements/let/syntax/let-iteration-variable-is-freshly-allocated-for-each-iteration-single-let-binding.js + * - test/language/statements/let/syntax/let-iteration-variable-is-freshly-allocated-for-each-iteration-multi-let-binding.js + * - test/language/statements/for-of/head-let-fresh-binding-per-iteration.js + * - test/language/statements/for-in/head-let-fresh-binding-per-iteration.js + * - test/language/statements/for/scope-head-lex-open.js + * - test/language/statements/for/scope-body-lex-open.js + * - test/language/statements/switch/scope-lex-open-case.js + * - test/language/statements/switch/scope-lex-close-case.js + * - test/language/statements/function/dflt-params-ref-prior.js + * - test/language/statements/function/dflt-params-ref-later.js + * - test/language/statements/function/dflt-params-ref-self.js + * + * Copyright (C) 2011, 2014, 2016 the V8 project authors. All rights reserved. + * Test262 portions are governed by the BSD license in LICENSE.test262. + */ +import { describe, expect, test } from "bun:test" +import { Effect } from "effect" +import { CodeMode } from "../src/index.js" + +const value = async (code: string) => { + const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} })) + if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) + return result.value +} + +describe("Test262 lexical temporal dead zones", () => { + test("program and block bindings exist before initialization", async () => { + expect( + await value(` + const errors = [] + try { programLet; let programLet } catch (error) { errors.push(error.name) } + try { programConst; const programConst = 1 } catch (error) { errors.push(error.name) } + try { { blockLet; let blockLet } } catch (error) { errors.push(error.name) } + try { { blockConst; const blockConst = 1 } } catch (error) { errors.push(error.name) } + return errors + `), + ).toEqual(["ReferenceError", "ReferenceError", "ReferenceError", "ReferenceError"]) + }) + + test("self-initialization and closure assignment observe the TDZ", async () => { + expect( + await value(` + const errors = [] + try { { let item = item + 1 } } catch (error) { errors.push(error.name) } + try { { const item = item + 1 } } catch (error) { errors.push(error.name) } + try { + { + function assign() { item = 1 } + assign() + let item + } + } catch (error) { errors.push(error.name) } + return errors + `), + ).toEqual(["ReferenceError", "ReferenceError", "ReferenceError"]) + }) + + test("for-of and for-in bound names are in the head TDZ", async () => { + expect( + await value(` + const errors = [] + try { let item = [1]; for (let item of item) {} } catch (error) { errors.push(error.name) } + try { let item = { value: 1 }; for (let item in item) {} } catch (error) { errors.push(error.name) } + return errors + `), + ).toEqual(["ReferenceError", "ReferenceError"]) + }) +}) + +describe("Test262 lexical loop environments", () => { + test("classic for creates fresh single and multiple bindings", async () => { + expect( + await value(` + const single = [] + for (let index = 0; index < 5; ++index) single.push(() => index) + + const left = [] + const right = [] + for (let first = 0, second = 10; first < 5; ++first, ++second) { + left.push(() => first) + right.push(() => second) + } + return [ + single.map((read) => read()), + left.map((read) => read()), + right.map((read) => read()), + ] + `), + ).toEqual([ + [0, 1, 2, 3, 4], + [0, 1, 2, 3, 4], + [10, 11, 12, 13, 14], + ]) + }) + + test("for-of and for-in create fresh bindings", async () => { + expect( + await value(` + const values = [] + for (let item of [1, 2, 3]) values.push(() => item) + + const keys = {} + for (let key in { first: 1, second: 2, third: 3 }) keys[key] = () => key + return [values.map((read) => read()), keys.first(), keys.second(), keys.third()] + `), + ).toEqual([[1, 2, 3], "first", "second", "third"]) + }) + + test("classic for separates declaration and per-iteration environments", async () => { + expect( + await value(` + let before + let testRead + let bodyRead + let updateRead + let run = true + for ( + let item = "outside", ignored = before = () => item; + run && (item = "inside", testRead = () => item); + updateRead = () => item + ) bodyRead = () => item, run = false + return [before(), testRead(), bodyRead(), updateRead()] + `), + ).toEqual(["outside", "inside", "inside", "inside"]) + }) +}) + +describe("Test262 switch and parameter environments", () => { + test("switch creates its lexical environment after the discriminant", async () => { + expect( + await value(` + let item = "outside" + let discriminantRead + let selectorRead + let statementRead + switch ((discriminantRead = () => item, null)) { + case (selectorRead = () => item, null): + statementRead = () => item + let item = "inside" + } + return [discriminantRead(), selectorRead(), statementRead()] + `), + ).toEqual(["outside", "inside", "inside"]) + }) + + test("all switch cases share one lexical environment that closes afterward", async () => { + expect( + await value(` + let item = "outside" + let firstRead + let secondRead + switch (null) { + case null: + let item = "inside" + firstRead = () => item + case null: + secondRead = () => item + } + return [firstRead(), secondRead(), item] + `), + ).toEqual(["inside", "inside", "outside"]) + }) + + test("parameter defaults see prior bindings but not self or later bindings", async () => { + expect( + await value(` + function prior(first, second = first, third = second) { return [first, second, third] } + function later(first = second, second) { return first } + function self(item = item) { return item } + function failure(run) { + try { return run() } catch (error) { return error.name } + } + return [prior(3), failure(later), failure(self)] + `), + ).toEqual([[3, 3, 3], "ReferenceError", "ReferenceError"]) + }) +}) diff --git a/packages/codemode/test/openapi.test.ts b/packages/codemode/test/openapi.test.ts index 12a094e033..f4da5c31e0 100644 --- a/packages/codemode/test/openapi.test.ts +++ b/packages/codemode/test/openapi.test.ts @@ -59,6 +59,53 @@ const singleOperation = (operation: Record, method = "get"): Do }, }) +const directionalSpec = (openapi: string): Document => ({ + openapi, + paths: { + "/users": { + post: { + operationId: "users.create", + requestBody: { + required: true, + content: { "application/json": { schema: { $ref: "#/components/schemas/User" } } }, + }, + responses: { + 200: { + description: "Created", + content: { "application/json": { schema: { $ref: "#/components/schemas/User" } } }, + }, + }, + }, + }, + }, + components: { + schemas: { + ReadOnlyID: { type: "string", readOnly: true }, + User: { + type: "object", + additionalProperties: false, + required: ["id", "name", "password", "profile", "generated"], + properties: { + id: { type: "string", readOnly: true }, + name: { type: "string" }, + password: { type: "string", writeOnly: true }, + profile: { + type: "object", + additionalProperties: false, + required: ["createdAt", "secret", "label"], + properties: { + createdAt: { type: "string", readOnly: true }, + secret: { type: "string", writeOnly: true }, + label: { type: "string" }, + }, + }, + generated: { $ref: "#/components/schemas/ReadOnlyID" }, + }, + }, + }, + }, +}) + describe("OpenAPI.fromSpec", () => { test("covers a representative API from generation through execution", async () => { const resolutions: Array = [] @@ -177,13 +224,13 @@ describe("OpenAPI.fromSpec", () => { const spec = await opencodeSpec() const result = OpenAPI.fromSpec({ spec, baseUrl }) - expect(result.skipped).toHaveLength(5) + expect(result.skipped).toHaveLength(4) expect(result.skipped).toContainEqual({ method: "GET", path: "/api/pty/{ptyID}/connect", reason: "WebSocket operations are not supported", }) - expect(result.skipped.filter((item) => item.reason === "SSE operations are not supported")).toHaveLength(3) + expect(result.skipped.filter((item) => item.reason === "SSE operations are not supported")).toHaveLength(2) expect(result.skipped).toContainEqual({ method: "GET", path: "/api/fs/read/*", @@ -205,17 +252,16 @@ describe("OpenAPI.fromSpec", () => { if (!Tool.isDefinition(switchAgent)) throw new Error("v2.session.switchAgent was not generated") expect(inputTypeScript(switchAgent)).toBe("{ sessionID: string; agent: string }") - const contextEntryPut = toolAt(result.tools, "v2.session.contextEntry.put") - expect(Tool.isDefinition(contextEntryPut)).toBe(true) - if (!Tool.isDefinition(contextEntryPut)) throw new Error("v2.session.contextEntry.put was not generated") - expect(inputTypeScript(contextEntryPut)).toBe("{ sessionID: string; key: string; value: unknown }") - expect(toolAt(result.tools, "v2_session_context_entry_put_2")).toBeUndefined() - expect(toolAt(result.tools, "v2.pty.connect")).toBeUndefined() + const instructionPut = toolAt(result.tools, "v2.session.instructions.entry.put") + expect(Tool.isDefinition(instructionPut)).toBe(true) + if (!Tool.isDefinition(instructionPut)) throw new Error("v2.session.instructions.entry.put was not generated") + expect(inputTypeScript(instructionPut)).toBe("{ sessionID: string; key: string; value: unknown }") + expect(toolAt(result.tools, "v2_session_instructions_entry_put_2")).toBeUndefined() + expect(Tool.isDefinition(toolAt(result.tools, "v2.pty.connect"))).toBe(false) expect(toolAt(result.tools, "v2.session.log")).toBeUndefined() expect(toolAt(result.tools, "v2.event.subscribe")).toBeUndefined() - expect(toolAt(result.tools, "v2.event.changes")).toBeUndefined() expect(toolAt(result.tools, "v2.fs.read")).toBeUndefined() - expect(toolAt(result.tools, "v2.pty.connectToken")).not.toBeUndefined() + expect(toolAt(result.tools, "v2.pty.connect.token")).not.toBeUndefined() }) test("preserves operation path sanitization and collision handling", () => { @@ -355,6 +401,550 @@ describe("OpenAPI.fromSpec", () => { expect(tool.output.$defs).toMatchObject({ Local: { type: "string" }, Global: { type: "number" } }) }) + test("projects read-only and write-only properties by schema direction", () => { + for (const version of ["3.0.3", "3.1.0"]) { + const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec: directionalSpec(version) }).tools, "users.create") + if (!Tool.isDefinition(tool) || !isRecord(tool.input) || !isRecord(tool.output)) { + throw new Error(`users.create was not generated for OpenAPI ${version}`) + } + + expect(inputTypeScript(tool)).toBe( + "{ name: string; password: string; profile: { secret: string; label: string } }", + ) + expect(outputTypeScript(tool)).toBe( + "{ id: string; name: string; profile: { createdAt: string; label: string }; generated: string }", + ) + + const requestDefinitions = isRecord(tool.input.$defs) ? tool.input.$defs : {} + const responseDefinitions = isRecord(tool.output.$defs) ? tool.output.$defs : {} + const requestUser = isRecord(requestDefinitions.User) ? requestDefinitions.User : {} + const responseUser = isRecord(responseDefinitions.User) ? responseDefinitions.User : {} + expect(Object.keys(isRecord(requestUser.properties) ? requestUser.properties : {})).toEqual([ + "name", + "password", + "profile", + ]) + expect(requestUser.required).toEqual(["name", "password", "profile"]) + expect(Object.keys(isRecord(responseUser.properties) ? responseUser.properties : {})).toEqual([ + "id", + "name", + "profile", + "generated", + ]) + expect(responseUser.required).toEqual(["id", "name", "profile", "generated"]) + } + }) + + test("projects directional annotations through local refs and allOf composition", () => { + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: singleOperation( + { + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + additionalProperties: false, + required: ["local", "composed", "name"], + properties: { + local: { $ref: "#/$defs/ReadOnlyValue" }, + composed: { allOf: [{ $ref: "#/$defs/ReadOnlyValue" }] }, + name: { type: "string" }, + }, + $defs: { + ReadOnlyValue: { type: "string", readOnly: true }, + }, + }, + }, + }, + }, + }, + "post", + ), + }).tools, + "test", + ) + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + + expect(inputTypeScript(tool)).toBe("{ name: string }") + }) + + test("honors declarations that are siblings of a $ref", () => { + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: { + openapi: "3.1.0", + paths: { + "/test": { + post: { + operationId: "test", + responses: { 200: { description: "Success" } }, + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + additionalProperties: false, + required: ["record"], + properties: { + record: { + $ref: "#/components/schemas/Base", + properties: { extra: { type: "string", readOnly: true }, note: { type: "string" } }, + required: ["extra", "note", "id"], + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Base: { + type: "object", + required: ["id", "name"], + properties: { id: { type: "string", readOnly: true }, name: { type: "string" } }, + }, + }, + }, + }, + }).tools, + "test", + ) + if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated") + const properties = isRecord(tool.input.properties) ? tool.input.properties : {} + const record = isRecord(properties.record) ? properties.record : {} + const definitions = isRecord(tool.input.$defs) ? tool.input.$defs : {} + const base = isRecord(definitions.Base) ? definitions.Base : {} + + expect(Object.keys(isRecord(record.properties) ? record.properties : {})).toEqual(["note"]) + expect(record.required).toEqual(["note"]) + expect(Object.keys(isRecord(base.properties) ? base.properties : {})).toEqual(["name"]) + expect(base.required).toEqual(["name"]) + }) + + test("honors directional declarations on intermediate reference hops", () => { + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: { + ...singleOperation( + { + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + additionalProperties: false, + required: ["secret", "name"], + properties: { + // Hidden only by the sibling declaration on the middle hop. + secret: { $ref: "#/components/schemas/Middle" }, + name: { type: "string" }, + }, + }, + }, + }, + }, + }, + "post", + ), + components: { + schemas: { + Middle: { $ref: "#/components/schemas/Plain", readOnly: true }, + Plain: { type: "string" }, + }, + }, + }, + }).tools, + "test", + ) + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + + expect(inputTypeScript(tool)).toBe("{ name: string }") + }) + + test("projects cyclic component references without hanging", () => { + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: { + openapi: "3.1.0", + paths: { + "/test": { + post: { + operationId: "test", + responses: { 200: { description: "Success" } }, + requestBody: { + required: true, + content: { "application/json": { schema: { $ref: "#/components/schemas/Node" } } }, + }, + }, + }, + }, + components: { + schemas: { + Node: { + type: "object", + required: ["id", "name", "child"], + properties: { + id: { type: "string", readOnly: true }, + name: { type: "string" }, + child: { $ref: "#/components/schemas/Node" }, + }, + }, + }, + }, + }, + }).tools, + "test", + ) + if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated") + const definitions = isRecord(tool.input.$defs) ? tool.input.$defs : {} + const node = isRecord(definitions.Node) ? definitions.Node : {} + + expect(Object.keys(isRecord(node.properties) ? node.properties : {})).toEqual(["name", "child"]) + expect(node.required).toEqual(["name", "child"]) + }) + + test("projects diamond-shaped reference graphs in linear time", () => { + // Each component references the next twice; without memoized hidden-ness this is 2^30 work. + const depth = 30 + const schemas = Object.fromEntries( + Array.from({ length: depth }, (_, index) => [ + `C${index}`, + index === depth - 1 + ? { type: "object", properties: { id: { type: "string", readOnly: true }, name: { type: "string" } } } + : { allOf: [{ $ref: `#/components/schemas/C${index + 1}` }, { $ref: `#/components/schemas/C${index + 1}` }] }, + ]), + ) + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: { + openapi: "3.1.0", + paths: { + "/test": { + post: { + operationId: "test", + responses: { 200: { description: "Success" } }, + requestBody: { + required: true, + content: { "application/json": { schema: { $ref: "#/components/schemas/C0" } } }, + }, + }, + }, + }, + components: { schemas }, + }, + }).tools, + "test", + ) + if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated") + const definitions = isRecord(tool.input.$defs) ? tool.input.$defs : {} + const leaf = isRecord(definitions[`C${depth - 1}`]) ? definitions[`C${depth - 1}`] : {} + + expect(Object.keys(isRecord(leaf.properties) ? leaf.properties : {})).toEqual(["name"]) + }) + + test("resolves hiding through reference cycles regardless of evaluation order", () => { + // `Wrap` is hidden only through the cycle member `Loop`; evaluating a property that + // enters the cycle at `Loop` first must not freeze a provisional result for `Wrap`. + const schemas = { + Wrap: { allOf: [{ $ref: "#/components/schemas/Loop" }] }, + Loop: { allOf: [{ $ref: "#/components/schemas/Wrap" }, { readOnly: true }] }, + } + const body = (properties: Record) => ({ + required: true, + content: { + "application/json": { + schema: { + type: "object", + additionalProperties: false, + required: [...Object.keys(properties), "name"], + properties: { ...properties, name: { type: "string" } }, + }, + }, + }, + }) + for (const properties of [ + { a: { $ref: "#/components/schemas/Loop" }, b: { $ref: "#/components/schemas/Wrap" } }, + { a: { $ref: "#/components/schemas/Wrap" }, b: { $ref: "#/components/schemas/Loop" } }, + ]) { + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: { ...singleOperation({ requestBody: body(properties) }, "post"), components: { schemas } }, + }).tools, + "test", + ) + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + + expect(inputTypeScript(tool)).toBe("{ name: string }") + } + }) + + test("keeps not, if, and contains subschemas unprojected", () => { + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: singleOperation( + { + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + additionalProperties: false, + required: ["record"], + properties: { + record: { + type: "object", + // Removing `secret` here would turn `not` unsatisfiable and + // flip which branch of `if` applies; both must pass through. + not: { required: ["secret"], properties: { secret: { type: "string", readOnly: true } } }, + if: { required: ["kind"], properties: { kind: { type: "string", readOnly: true } } }, + }, + }, + }, + }, + }, + }, + }, + "post", + ), + }).tools, + "test", + ) + if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated") + const properties = isRecord(tool.input.properties) ? tool.input.properties : {} + const record: Record = isRecord(properties.record) ? properties.record : {} + + expect(record.not).toEqual({ required: ["secret"], properties: { secret: { type: "string", readOnly: true } } }) + expect(record.if).toEqual({ required: ["kind"], properties: { kind: { type: "string", readOnly: true } } }) + }) + + test("does not hide properties whose direction is declared only in anyOf or oneOf alternatives", () => { + // Deliberate scope bound: alternatives may apply, so a directional declaration on + // one alternative does not hide the property; the annotation is preserved as-is. + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: singleOperation( + { + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + additionalProperties: false, + required: ["choice", "pick"], + properties: { + choice: { anyOf: [{ type: "string", readOnly: true }, { type: "number" }] }, + pick: { oneOf: [{ type: "string", readOnly: true }, { type: "number" }] }, + }, + }, + }, + }, + }, + }, + "post", + ), + }).tools, + "test", + ) + if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated") + const properties = isRecord(tool.input.properties) ? tool.input.properties : {} + const choice: Record = isRecord(properties.choice) ? properties.choice : {} + const pick: Record = isRecord(properties.pick) ? properties.pick : {} + + expect(Object.keys(properties)).toEqual(["choice", "pick"]) + expect(choice.anyOf).toEqual([{ type: "string", readOnly: true }, { type: "number" }]) + expect(pick.oneOf).toEqual([{ type: "string", readOnly: true }, { type: "number" }]) + }) + + test("does not misresolve shadowed local $defs when flattening body fields", () => { + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: singleOperation( + { + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + additionalProperties: false, + required: ["record"], + $defs: { Value: { type: "string" } }, + properties: { + record: { + type: "object", + required: ["x"], + properties: { x: { $ref: "#/$defs/Value" } }, + // Shadows the body-level Value; must not affect the body-rooted projection. + $defs: { Value: { type: "string", readOnly: true } }, + }, + }, + }, + }, + }, + }, + }, + "post", + ), + }).tools, + "test", + ) + if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated") + const properties = isRecord(tool.input.properties) ? tool.input.properties : {} + const record = isRecord(properties.record) ? properties.record : {} + + expect(Object.keys(isRecord(record.properties) ? record.properties : {})).toEqual(["x"]) + expect(record.required).toEqual(["x"]) + }) + + test("projects directional annotations inside parameter schemas", () => { + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: singleOperation({ + parameters: [ + { + name: "filter", + in: "query", + required: true, + schema: { + type: "object", + required: ["state", "id"], + properties: { state: { type: "string" }, id: { type: "string", readOnly: true } }, + }, + }, + ], + }), + }).tools, + "test", + ) + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + + expect(inputTypeScript(tool)).toBe("{ filter: { state: string } }") + }) + + test("ignores inherited directional annotations", () => { + const inherited: Record = { type: "string" } + Object.setPrototypeOf(inherited, { readOnly: true }) + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: singleOperation({ + parameters: [ + { + name: "filter", + in: "query", + required: true, + schema: { + type: "object", + // The own annotation on `id` keeps projection active for the document, + // so `value` pins that prototype-inherited annotations are not read. + properties: { value: inherited, id: { type: "string", readOnly: true } }, + required: ["value", "id"], + }, + }, + ], + }), + }).tools, + "test", + ) + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + + expect(inputTypeScript(tool)).toBe("{ filter: { value: string } }") + }) + + test("cleans required properties across allOf branches", () => { + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: singleOperation( + { + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + required: ["id", "name"], + allOf: [ + { + type: "object", + required: ["id", "name"], + properties: { id: { type: "string", readOnly: true }, name: { type: "string" } }, + }, + ], + }, + }, + }, + }, + }, + "post", + ), + }).tools, + "test", + ) + if (!Tool.isDefinition(tool) || !isRecord(tool.input)) throw new Error("test was not generated") + const properties = isRecord(tool.input.properties) ? tool.input.properties : {} + const body = isRecord(properties.body) ? properties.body : {} + const allOf = Array.isArray(body.allOf) ? body.allOf : [] + const branch = isRecord(allOf[0]) ? allOf[0] : {} + + expect(body.required).toEqual(["name"]) + expect(branch.required).toEqual(["name"]) + expect(Object.keys(isRecord(branch.properties) ? branch.properties : {})).toEqual(["name"]) + }) + + test("keeps directional schemas model-facing while preserving runtime pass-through", async () => { + const client = recordingClient(() => + json({ + id: "server-id", + name: "Ada", + password: "returned-by-server", + profile: { createdAt: "today", secret: "returned-secret", label: "primary" }, + generated: "generated-id", + }), + ) + const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec: directionalSpec("3.1.0") }).tools, "users.create") + if (!Tool.isDefinition(tool)) throw new Error("users.create was not generated") + + const result = await Effect.runPromise( + tool + .run({ + id: "ignored-top-level", + generated: "ignored-generated", + name: "Ada", + password: "request-secret", + profile: { createdAt: "sent-nested", secret: "nested-secret", label: "primary" }, + }) + .pipe(Effect.provide(client.layer)), + ) + + expect(client.requests[0]?.body).toEqual({ + name: "Ada", + password: "request-secret", + profile: { createdAt: "sent-nested", secret: "nested-secret", label: "primary" }, + }) + expect(result).toMatchObject({ password: "returned-by-server", profile: { secret: "returned-secret" } }) + }) + test("documents that the opencode fixture is unauthenticated", async () => { const spec = await opencodeSpec() const components = isRecord(spec.components) ? spec.components : {} @@ -378,7 +968,7 @@ describe("OpenAPI.fromSpec", () => { runtime .execute( ` - return await tools.$codemode.search({ query: "global health", namespace: "opencode", limit: 1 }) + return search({ query: "global health", namespace: "opencode", limit: 1 }) `, ) .pipe(Effect.provide(layer)), @@ -496,6 +1086,48 @@ describe("OpenAPI.fromSpec", () => { ) }) + test("preserves ordered exploded and deep-object query parameters", async () => { + const client = recordingClient(() => json({ ok: true })) + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: singleOperation({ + parameters: [ + { name: "tags", in: "query", style: "form", explode: true, schema: { type: "array" } }, + { name: "filter", in: "query", style: "form", explode: true, schema: { type: "object" } }, + { name: "location", in: "query", style: "deepObject", explode: true, schema: { type: "object" } }, + ], + }), + }).tools, + "test", + ) + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + + await Effect.runPromise( + tool + .run({ + tags: ["first value", "second&value"], + filter: { state: "open now", page: 2 }, + location: { directory: "/tmp/a b", workspace: "work&1" }, + }) + .pipe(Effect.provide(client.layer)), + ) + + expect(client.requests[0]?.url).toBe( + `${baseUrl}/test?tags=first+value&tags=second%26value&state=open+now&page=2&location%5Bdirectory%5D=%2Ftmp%2Fa+b&location%5Bworkspace%5D=work%261`, + ) + await expect(Effect.runPromise(tool.run({ tags: [{}] }).pipe(Effect.provide(client.layer)))).rejects.toThrow( + "Parameter 'tags' contains an unsupported nested value.", + ) + await expect( + Effect.runPromise(tool.run({ filter: { state: {} } }).pipe(Effect.provide(client.layer))), + ).rejects.toThrow("Query parameter 'filter' contains an unsupported nested value.") + await expect( + Effect.runPromise(tool.run({ location: { directory: [] } }).pipe(Effect.provide(client.layer))), + ).rejects.toThrow("Deep-object parameter 'location' contains an unsupported nested value.") + expect(client.requests).toHaveLength(1) + }) + test("skips unsupported parameter encodings and malformed security", () => { const result = OpenAPI.fromSpec({ baseUrl, diff --git a/packages/codemode/test/parity.test.ts b/packages/codemode/test/parity.test.ts index dfa8583183..a5c003a62c 100644 --- a/packages/codemode/test/parity.test.ts +++ b/packages/codemode/test/parity.test.ts @@ -8,7 +8,7 @@ import { ToolRuntime } from "../src/tool-runtime.js" // a strict interpreter would throw but idiomatic JS yields undefined / succeeds. // // Note on the result boundary: this package normalizes a bare `undefined` result to `null` when -// it crosses out of the sandbox (results are JSON data), so tests asserting an in-sandbox +// it crosses out of CodeMode (results are JSON data), so tests asserting an in-CodeMode // `undefined` read check `=== undefined` inside the program and `null` at the boundary. const run = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools: {} })) const value = async (code: string) => { @@ -42,11 +42,6 @@ describe("H2: string property access reads as undefined (not a throw)", () => { test("unknown property on a number is undefined", async () => { expect(await value(`return (5).foo ?? "n"`)).toBe("n") }) - - test("supported string methods still work", async () => { - expect(await value(`return "AB".toLowerCase()`)).toBe("ab") - expect(await value(`return "hello".length`)).toBe(5) - }) }) describe("H3: array property access reads as undefined (not a throw)", () => { @@ -63,8 +58,7 @@ describe("H3: array property access reads as undefined (not a throw)", () => { expect(await value(`return [1,2,3].toSpliced === undefined`)).toBe(true) }) - test("supported array methods and indexing still work", async () => { - expect(await value(`return [1,2,3].map(x => x + 1)`)).toEqual([2, 3, 4]) + test("array indexing still works", async () => { expect(await value(`return [1,2,3][9] === undefined`)).toBe(true) expect(await value(`return [1,2,3][9]`)).toBeNull() }) @@ -105,6 +99,137 @@ describe("H4: typeof on an undeclared identifier is 'undefined'", () => { }) }) +describe("CodeMode lexical scope integration", () => { + test("keeps self, cross, and destructuring defaults in the TDZ", async () => { + expect( + await value(` + const outer = 1 + const errors = [] + try { const first = second, second = 2 } catch (error) { errors.push(error.name) } + try { const [first = second, second = 2] = [] } catch (error) { errors.push(error.name) } + return errors + `), + ).toEqual(["ReferenceError", "ReferenceError"]) + }) + + test("keeps typeof and constant assignment inside the TDZ", async () => { + expect( + await value(` + const errors = [] + try { { errors.push(typeof item); let item } } catch (error) { errors.push(error.name) } + try { { constant = 1; const constant = 2 } } catch (error) { errors.push(error.name) } + return errors + `), + ).toEqual(["ReferenceError", "ReferenceError"]) + }) + + test("shadows builtins from the start of the program scope", async () => { + expect( + await value(` + let observed + try { observed = typeof Promise } catch (error) { observed = error.name } + const Promise = 1 + return observed + `), + ).toBe("ReferenceError") + }) + + test("keeps classic for initializers inside the header TDZ", async () => { + expect( + await value(` + let index = 1 + try { for (let index = index; index < 2; index++) {} } catch (error) { return error.name } + `), + ).toBe("ReferenceError") + }) + + test("removes loop scopes when per-iteration initialization fails", async () => { + expect( + await value(` + const value = "outer" + try { for (let [value] of [1]) {} } catch {} + return value + `), + ).toBe("outer") + }) +}) + +describe("unary void", () => { + test("evaluates its operand and returns undefined", async () => { + expect( + await value(`let count = 0; const result = void (count += 1); return [count, result === undefined]`), + ).toEqual([1, true]) + }) + + test("discards opaque values", async () => { + expect(await value(`return void tools === undefined`)).toBe(true) + }) +}) + +describe("property deletion", () => { + test("deletes plain object fields and reports missing fields as successful", async () => { + expect( + await value(` + const object = { keep: 1, remove: 2 } + return [delete object.remove, delete object.missing, object] + `), + ).toEqual([true, true, { keep: 1 }]) + }) + + test("evaluates computed object and key expressions once", async () => { + expect( + await value(` + const object = { remove: true } + let objectReads = 0 + let keyReads = 0 + function getObject() { objectReads++; return object } + function getKey() { keyReads++; return "remove" } + const removed = delete getObject()[getKey()] + return [removed, objectReads, keyReads, Object.hasOwn(object, "remove")] + `), + ).toEqual([true, 1, 1, false]) + }) + + test("deleting an array index creates a hole without changing its length", async () => { + expect( + await value( + `const values = [1, 2, 3]; const removed = delete values[1]; return [removed, values.length, 1 in values, values]`, + ), + ).toEqual([true, 3, false, [1, null, 3]]) + }) + + test("array length is not configurable", async () => { + expect(await value(`const values = [1, 2]; return [delete values.length, values.length]`)).toEqual([false, 2]) + }) + + test("does not broaden unsupported array property assignment", async () => { + expect( + await value(` + const values = [] + let rightHandSideRuns = 0 + function next() { rightHandSideRuns++; return 1 } + try { values.field = next() } catch {} + return rightHandSideRuns + `), + ).toBe(0) + }) + + test("optional deletion short-circuits without evaluating the key", async () => { + expect( + await value(`let keyReads = 0; const object = null; return [delete object?.[keyReads++], keyReads]`), + ).toEqual([true, 0]) + }) + + test("rejects deletion from opaque runtime references", async () => { + expect((await error(`return delete tools.example`)).kind).toBe("InvalidDataValue") + }) + + test("keeps blocked property names unavailable", async () => { + expect((await error(`const object = {}; return delete object.__proto__`)).kind).toBe("ExecutionFailure") + expect((await error(`const values = []; return delete values["constructor"]`)).kind).toBe("ExecutionFailure") + }) +}) + describe("H1: NaN/Infinity flow as intermediates and normalize to null at the boundary", () => { test("guards run instead of the program crashing on a transient NaN", async () => { expect(await value(`return parseInt("abc") || 0`)).toBe(0) @@ -114,7 +239,7 @@ describe("H1: NaN/Infinity flow as intermediates and normalize to null at the bo expect(await value(`const a = []; return a.length ? a.reduce((s,x)=>s+x,0)/a.length : 0`)).toBe(0) }) - test("a non-finite value becomes null when it leaves the sandbox", async () => { + test("a non-finite value becomes null when it leaves CodeMode", async () => { expect(await value(`return 5/0`)).toBeNull() expect(await value(`return 0/0`)).toBeNull() expect(await value(`return Math.max()`)).toBeNull() @@ -122,22 +247,34 @@ describe("H1: NaN/Infinity flow as intermediates and normalize to null at the bo expect(await value(`return { a: Number("x"), b: 2, c: [1/0] }`)).toEqual({ a: null, b: 2, c: [null] }) }) - test("NaN and Infinity are usable identifiers and inspectable in-sandbox", async () => { + test("NaN and Infinity are usable identifiers and inspectable in-CodeMode", async () => { expect(await value(`return Number.isNaN(NaN)`)).toBe(true) expect(await value(`return Infinity > 1e9`)).toBe(true) expect(await value(`return Number.isFinite(1/0)`)).toBe(false) expect(await value(`return [3,1,2].reduce((a,b)=>Math.max(a,b), -Infinity)`)).toBe(3) - // JSON.stringify inside the sandbox matches JS: non-finite serializes to null + // JSON.stringify inside CodeMode matches JS: non-finite serializes to null expect(await value(`return JSON.stringify({ x: Number("z") })`)).toBe('{"x":null}') }) test("copyOut normalizes non-finite numbers to null (the shared return + tool-arg boundary)", () => { // Tool-call arguments funnel through copyOut too, so this one function pins both boundaries. - expect(ToolRuntime.copyOut(NaN)).toBeNull() - expect(ToolRuntime.copyOut(Infinity)).toBeNull() - expect(ToolRuntime.copyOut(-Infinity)).toBeNull() - expect(ToolRuntime.copyOut(42)).toBe(42) - expect(ToolRuntime.copyOut({ a: NaN, b: [Infinity, 1] })).toEqual({ a: null, b: [null, 1] }) + expect(ToolRuntime.copyOut(NaN, "json")).toBeNull() + expect(ToolRuntime.copyOut(Infinity, "json")).toBeNull() + expect(ToolRuntime.copyOut(-Infinity, "nullify")).toBeNull() + expect(ToolRuntime.copyOut(42, "json")).toBe(42) + expect(ToolRuntime.copyOut({ a: NaN, b: [Infinity, 1] }, "json")).toEqual({ a: null, b: [null, 1] }) + }) +}) + +describe("copyOut undefined handling per boundary mode", () => { + test("json mode mirrors JSON.stringify for undefined", () => { + expect(ToolRuntime.copyOut({ q: undefined, keep: 1 }, "json")).toStrictEqual({ keep: 1 }) + expect(ToolRuntime.copyOut([1, undefined, 2], "json")).toStrictEqual([1, null, 2]) + expect(ToolRuntime.copyOut({ nested: { a: undefined, b: [undefined] } }, "json")).toStrictEqual({ + nested: { b: [null] }, + }) + expect(ToolRuntime.copyOut(undefined, "json")).toBeUndefined() + expect(ToolRuntime.copyOut({ a: undefined }, "nullify")).toStrictEqual({ a: null }) }) }) @@ -202,9 +339,6 @@ describe("Error values and instanceof", () => { "TypeError", true, ]) - expect(await value(`try { "a".normalize("NOPE") } catch (e) { return [e.name, e instanceof RangeError] }`)).toEqual( - ["RangeError", true], - ) expect(await value(`try { "a".match("(") } catch (e) { return [e.name, e instanceof SyntaxError] }`)).toEqual([ "SyntaxError", true, @@ -263,15 +397,18 @@ describe("Error values and instanceof", () => { }) }) -describe("array methods: splice, fill, copyWithin, keys/values/entries", () => { - test("splice removes in place and returns the removed elements", async () => { - expect(await value(`const a = [1,2,3,4]; const removed = a.splice(1, 2); return { removed, a }`)).toEqual({ - removed: [2, 3], - a: [1, 4], - }) +describe("CodeMode-specific array behavior", () => { + test("sort with a comparator mutates and returns the receiver", async () => { + expect( + await value(` + const input = [3, 1, 2] + const result = input.sort((a, b) => a - b) + return { input, same: input === result } + `), + ).toEqual({ input: [1, 2, 3], same: true }) }) - test("splice inserts new elements at the cut", async () => { + test("splice can replace and insert elements", async () => { expect(await value(`const a = ["a","d"]; a.splice(1, 0, "b", "c"); return a`)).toEqual(["a", "b", "c", "d"]) expect(await value(`const a = [1,2,3]; const removed = a.splice(1, 1, "x"); return { removed, a }`)).toEqual({ removed: [2], @@ -279,32 +416,12 @@ describe("array methods: splice, fill, copyWithin, keys/values/entries", () => { }) }) - test("splice with one argument removes to the end; negative start counts back", async () => { - expect(await value(`const a = [1,2,3]; const removed = a.splice(1); return { removed, a }`)).toEqual({ - removed: [2, 3], - a: [1], - }) - expect(await value(`const a = [1,2,3]; const removed = a.splice(-1); return { removed, a }`)).toEqual({ - removed: [3], - a: [1, 2], - }) - }) - test("splice rejects inserting a container into itself", async () => { const err = await error(`const a = [1]; a.splice(0, 0, [a]); return a`) expect(err.kind).toBe("InvalidDataValue") expect(err.message).toContain("circular") }) - test("fill overwrites a range and returns the mutated array", async () => { - expect(await value(`const a = [1,2,3,4]; return a.fill(0, 1, 3)`)).toEqual([1, 0, 0, 4]) - expect(await value(`return [1,2,3].fill("z")`)).toEqual(["z", "z", "z"]) - }) - - test("copyWithin copies a range in place", async () => { - expect(await value(`return [1,2,3,4,5].copyWithin(0, 3)`)).toEqual([4, 5, 3, 4, 5]) - }) - test("keys/values/entries return arrays usable with for...of and spread", async () => { expect(await value(`return [...["x","y","z"].keys()]`)).toEqual([0, 1, 2]) expect(await value(`return ["x","y"].values()`)).toEqual(["x", "y"]) @@ -319,25 +436,21 @@ describe("array methods: splice, fill, copyWithin, keys/values/entries", () => { }) }) -describe("string methods: localeCompare, normalize, trim aliases", () => { +describe("CodeMode-specific string behavior", () => { test("localeCompare orders strings for sorting", async () => { expect(await value(`return ["b","a","c"].sort((x, y) => x.localeCompare(y))`)).toEqual(["a", "b", "c"]) - expect(await value(`return "a".localeCompare("a")`)).toBe(0) - }) - - test("normalize applies unicode normalization forms", async () => { - expect(await value(`return "\\u0065\\u0301".normalize("NFC").length`)).toBe(1) - expect(await value(`return "\\u00e9".normalize("NFD").length`)).toBe(2) - expect(await value(`return "x".normalize() === "x"`)).toBe(true) }) test("an invalid normalize form is a clear catchable error", async () => { expect(await value(`try { "x".normalize("nope"); return "no" } catch (e) { return e.message }`)).toContain('"NFC"') }) - test("trimLeft/trimRight alias trimStart/trimEnd", async () => { - expect(await value(`return " x ".trimLeft()`)).toBe("x ") - expect(await value(`return " x ".trimRight()`)).toBe(" x") + test("does not expose obsolete string aliases", async () => { + expect(await value(`return [typeof "x".trimLeft, typeof "x".trimRight, typeof "x".substr]`)).toEqual([ + "undefined", + "undefined", + "undefined", + ]) }) }) @@ -351,12 +464,12 @@ describe("compound assignment matches its binary operator", () => { return a } - test("sandbox Date += concatenates its string form, like d = d + 1", async () => { + test("CodeMode Date += concatenates its string form, like d = d + 1", async () => { const result = await pair(`let d = new Date(1000); d += 1; return d`, `let d = new Date(1000); d = d + 1; return d`) expect(result).toBe("1970-01-01T00:00:01.000Z1") }) - test("sandbox Date numeric compound ops use its time value", async () => { + test("CodeMode Date numeric compound ops use its time value", async () => { expect( await pair(`let d = new Date(1000); d -= 400; return d`, `let d = new Date(1000); d = d - 400; return d`), ).toBe(600) @@ -413,13 +526,332 @@ describe("H5: builtin coercion functions work as array callbacks", () => { expect(await value(`return [1, 2, 3].map(String)`)).toEqual(["1", "2", "3"]) }) - test("arrow callbacks still work (no regression)", async () => { - expect(await value(`return [1, 2, 3, 4].filter(x => x % 2 === 0)`)).toEqual([2, 4]) - expect(await value(`return [1, 2, 3].reduce((a, b) => a + b, 0)`)).toBe(6) - }) - test("a non-callable callback is still rejected", async () => { const err = await error(`return [1,2,3].map(42)`) expect(err.message).toContain("callback") }) }) + +describe("for...of assignment destructuring", () => { + test("assigns entry pairs into predeclared variables", async () => { + expect( + await value(` + let key + let item + const out = [] + for ([key, item] of Object.entries({ a: 1, b: 2 })) out.push(key + item) + return { key, item, out } + `), + ).toEqual({ key: "b", item: 2, out: ["a1", "b2"] }) + }) + + test("assigns object patterns and defaults", async () => { + expect( + await value(` + let id + let label + const labels = [] + for ({ id, label = "unknown" } of [{ id: 1 }, { id: 2, label: "two" }]) labels.push(label) + return { id, label, labels } + `), + ).toEqual({ id: 2, label: "two", labels: ["unknown", "two"] }) + }) +}) + +describe("sequence expressions", () => { + test("evaluate left to right and return the final value", async () => { + expect(await value(`let x = 0; const result = (x += 1, x *= 3, x + 2); return { x, result }`)).toEqual({ + x: 3, + result: 5, + }) + }) + + test("support comma-separated for-loop updates", async () => { + expect( + await value(` + const pairs = [] + for (let left = 0, right = 3; left < right; left++, right--) pairs.push([left, right]) + return pairs + `), + ).toEqual([ + [0, 3], + [1, 2], + ]) + }) +}) + +describe("destructuring assignment", () => { + test("assigns object and array patterns to existing bindings", async () => { + expect( + await value(` + let a = 0 + let b = 0 + ;({ a } = { a: 2 }) + ;[a, b] = [3, 4] + return [a, b] + `), + ).toEqual([3, 4]) + }) + + test("supports defaults, nesting, rest, and member targets", async () => { + expect( + await value(` + let first = 0 + let fallback = 0 + let rest = {} + const target = {} + ;[first, fallback = 2, ...target.tail] = [1] + ;({ nested: { value: target.value }, kept: target.kept = 3, ...rest } = { + nested: { value: 4 }, + extra: 5, + }) + return { first, fallback, target, rest } + `), + ).toEqual({ first: 1, fallback: 2, target: { tail: [], value: 4, kept: 3 }, rest: { extra: 5 } }) + }) + + test("returns the assigned value", async () => { + expect(await value(`let a = 0; const result = ([a] = [7]); return [a, result]`)).toEqual([7, [7]]) + }) + + test("supports computed object keys and evaluates them once", async () => { + expect( + await value(` + let calls = 0 + const field = () => { calls++; return "name" } + const { [field()]: name, ...rest } = { name: "Ada", role: "engineer" } + return { calls, name, rest } + `), + ).toEqual({ calls: 1, name: "Ada", rest: { role: "engineer" } }) + }) + + test("supports object patterns over arrays", async () => { + expect( + await value(` + const { 0: first, length, slice, ...rest } = ["a", "b", "c"] + return { first, length, sliced: slice(1), rest } + `), + ).toEqual({ first: "a", length: 3, sliced: ["b", "c"], rest: { 1: "b", 2: "c" } }) + }) + + test("preserves exact computed property names on arrays", async () => { + expect( + await value(` + const { ["01"]: item, ...rest } = [10, 20] + return { missing: item === undefined, rest } + `), + ).toEqual({ missing: true, rest: { 0: 10, 1: 20 } }) + }) + + test("supports array patterns over strings, Maps, Sets, and URLSearchParams", async () => { + expect( + await value(` + const [letter, ...letters] = "A😀B" + const [[mapKey, mapValue]] = new Map([["key", 1]]) + const [setFirst, setSecond] = new Set([2, 3]) + const [[queryKey, queryValue]] = new URLSearchParams("q=test&page=2") + return { letter, letters, mapKey, mapValue, setFirst, setSecond, queryKey, queryValue } + `), + ).toEqual({ + letter: "A", + letters: ["😀", "B"], + mapKey: "key", + mapValue: 1, + setFirst: 2, + setSecond: 3, + queryKey: "q", + queryValue: "test", + }) + }) + + test("supports iterable patterns in assignment and parameters", async () => { + expect( + await value(` + let first + let rest + ;[first, ...rest] = new Set([1, 2, 3]) + const read = ([[key, value]]) => key + value + return { first, rest, entry: read(new Map([["a", 4]])) } + `), + ).toEqual({ first: 1, rest: [2, 3], entry: "a4" }) + }) + + test("rejects computed keys that are not confined property keys", async () => { + const err = await error(`const key = {}; const { [key]: value } = {}`) + expect(err.message).toContain("Property key must be a string or number") + }) +}) + +describe("coercion parity: zero-argument coercion functions", () => { + test("Number() is 0 and String() is empty, unlike their undefined-argument forms", async () => { + expect(await value(`return Number()`)).toBe(0) + expect(await value(`return String()`)).toBe("") + expect(await value(`return Boolean()`)).toBe(false) + expect(await value(`return Number.isNaN(Number(undefined))`)).toBe(true) + expect(await value(`return String(undefined)`)).toBe("undefined") + }) + + test("parseInt() and parseFloat() stay NaN with no argument", async () => { + expect(await value(`return Number.isNaN(parseInt())`)).toBe(true) + expect(await value(`return Number.isNaN(parseFloat())`)).toBe(true) + }) +}) + +describe("coercion parity: global isFinite and isNaN", () => { + test("coerce their argument like native JS, unlike the Number statics", async () => { + expect(await value(`return isFinite("42")`)).toBe(true) + expect(await value(`return Number.isFinite("42")`)).toBe(false) + expect(await value(`return isNaN("oops")`)).toBe(true) + expect(await value(`return isNaN("42")`)).toBe(false) + expect(await value(`return isFinite(Infinity)`)).toBe(false) + expect(await value(`return isNaN(null)`)).toBe(false) + }) + + test("zero-argument forms match native", async () => { + expect(await value(`return isFinite()`)).toBe(false) + expect(await value(`return isNaN()`)).toBe(true) + }) + + test("read as functions", async () => { + expect(await value(`return typeof isFinite`)).toBe("function") + expect(await value(`return typeof isNaN`)).toBe("function") + }) + + test("work as array callbacks", async () => { + expect(await value(`return [1, "2", "x", Infinity].filter(isFinite)`)).toEqual([1, "2"]) + expect(await value(`return ["1", "x"].map(isNaN)`)).toEqual([false, true]) + }) +}) + +describe("coercion parity: arrays coerce to numbers through their string form", () => { + test("arrays with objects become NaN instead of crashing on host ToPrimitive", async () => { + expect(await value(`let x = [{}]; x++; return Number.isNaN(x)`)).toBe(true) + expect(await value(`return isFinite([{}])`)).toBe(false) + expect(await value(`return "abc".slice([{}])`)).toBe("abc") + }) + + test("single-element and empty arrays match native Number()", async () => { + expect(await value(`return Number([5])`)).toBe(5) + expect(await value(`return Number([])`)).toBe(0) + expect(await value(`return Number.isNaN(Number([1, 2]))`)).toBe(true) + }) +}) + +describe("coercion parity: String method arguments coerce like native JS", () => { + test("includes and indexOf coerce numbers", async () => { + expect(await value(`return "v1.2".includes(1)`)).toBe(true) + expect(await value(`return "a2b".indexOf(2)`)).toBe(1) + expect(await value(`return "abc".includes("d")`)).toBe(false) + }) + + test("slice, repeat, and padStart coerce numeric strings", async () => { + expect(await value(`return "abc".slice("1")`)).toBe("bc") + expect(await value(`return "ab".repeat("2")`)).toBe("abab") + expect(await value(`return "7".padStart("3", 0)`)).toBe("007") + }) + + test("split coerces separators but treats undefined as absent", async () => { + expect(await value(`return "a1b".split(1)`)).toEqual(["a", "b"]) + expect(await value(`return "a,b".split(undefined)`)).toEqual(["a,b"]) + expect(await value(`return "a,b".split()`)).toEqual(["a,b"]) + expect(await value(`return "a,b".split(undefined, 0)`)).toEqual([]) + expect(await value(`return "a,b".split(undefined, 1)`)).toEqual(["a,b"]) + }) + + test("replace coerces search and replacement values", async () => { + expect(await value(`return "a1b".replace(1, 2)`)).toBe("a2b") + expect(await value(`return "a1b".replace(1, () => "x")`)).toBe("axb") + }) + + test("repeat rejections carry the native RangeError name", async () => { + expect(await value(`try { "a".repeat(-1) } catch (e) { return e.name }`)).toBe("RangeError") + }) + + test("includes, startsWith, and endsWith reject regular expressions with a TypeError", async () => { + expect(await value(`try { "abc".includes(/a/) } catch (e) { return e.name }`)).toBe("TypeError") + expect(await value(`try { "abc".startsWith(/a/) } catch (e) { return e.name }`)).toBe("TypeError") + expect(await value(`try { "abc".endsWith(/a/) } catch (e) { return e.name }`)).toBe("TypeError") + }) + + test("opaque runtime references still reject as data errors", async () => { + const err = await error(`const f = () => 1; return "abc".includes(f)`) + expect(err.message).toContain("data value") + const replacerErr = await error(`const f = () => 1; return "a".replace(f, () => "x")`) + expect(replacerErr.message).toContain("data value") + }) +}) + +describe("coercion parity: match() and search() with no argument", () => { + test("behave as an empty pattern like native JS", async () => { + expect(await value(`return "abc".search()`)).toBe(0) + expect(await value(`const m = "abc".match(); return { first: m[0], index: m.index }`)).toEqual({ + first: "", + index: 0, + }) + }) +}) + +describe("coercion parity: ++ and -- use CodeMode numeric coercion", () => { + test("numeric strings increment like native JS", async () => { + expect(await value(`let x = "5"; x++; return x`)).toBe(6) + expect(await value(`let x = "5"; return ++x`)).toBe(6) + expect(await value(`const o = { n: "2" }; o.n--; return o.n`)).toBe(1) + }) + + test("dates increment through their epoch time", async () => { + expect(await value(`let d = new Date(5); d++; return d`)).toBe(6) + }) + + test("plain data objects become NaN instead of crashing", async () => { + expect(await value(`let x = {}; x++; return Number.isNaN(x)`)).toBe(true) + expect(await value(`const o = { a: {} }; o.a++; return Number.isNaN(o.a)`)).toBe(true) + }) + + test("opaque runtime references reject with a clear error", async () => { + const err = await error(`let f = () => 1; f++`) + expect(err.message).toContain("data value") + }) +}) + +describe("coercion parity: unknown static members read as undefined", () => { + test("feature detection on missing statics works like native JS", async () => { + expect(await value(`return typeof Math.sumPrecise`)).toBe("undefined") + expect(await value(`return Object.groupBy === undefined`)).toBe(true) + expect(await value(`return RegExp.escape === undefined`)).toBe(true) + expect(await value(`return Number.range === undefined`)).toBe(true) + expect(await value(`return String.raw === undefined`)).toBe(true) + expect(await value(`return isFinite.something === undefined`)).toBe(true) + expect(await value(`return console.group === undefined`)).toBe(true) + expect(await value(`return Date.moment === undefined`)).toBe(true) + expect(await value(`return JSON.rawJSON === undefined`)).toBe(true) + expect(await value(`return URL.createObjectURL === undefined`)).toBe(true) + expect(await value(`return Map.groupBy === undefined`)).toBe(true) + expect(await value(`return Math.sumPrecise?.([1]) ?? "fallback"`)).toBe("fallback") + }) + + test("known statics still resolve and run", async () => { + expect(await value(`return typeof Math.max`)).toBe("function") + expect(await value(`return typeof console.log`)).toBe("function") + expect(await value(`return typeof Date.now`)).toBe("function") + expect(await value(`return Math.max(1, 2)`)).toBe(2) + expect(await value(`return URL.canParse("https://example.com")`)).toBe(true) + expect(await value(`return Number.isInteger(3)`)).toBe(true) + expect(await value(`return Number.MAX_SAFE_INTEGER`)).toBe(Number.MAX_SAFE_INTEGER) + }) + + test("calling an unknown static reports a native-style TypeError", async () => { + expect(await value(`try { Math.sumPrecise([1]) } catch (e) { return e.name + ": " + e.message }`)).toBe( + "TypeError: Math.sumPrecise is not a function.", + ) + expect(await value(`try { Math["sumPrecise"]([1]) } catch (e) { return e.message }`)).toBe( + "Math.sumPrecise is not a function.", + ) + }) + + test("blocked members still throw instead of reading as undefined", async () => { + const err = await error(`return Math.constructor`) + expect(err.message).toContain("not available") + const coercionErr = await error(`return Number.constructor`) + expect(coercionErr.message).toContain("Number.constructor is not available") + }) +}) diff --git a/packages/codemode/test/promise-test262.test.ts b/packages/codemode/test/promise-test262.test.ts new file mode 100644 index 0000000000..0dc57e7b75 --- /dev/null +++ b/packages/codemode/test/promise-test262.test.ts @@ -0,0 +1,1422 @@ +/* + * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75. + * Every test names its upstream source; test.failing cases are executable conformance + * targets for intended Promise behavior that CodeMode does not implement yet. + * + * Copyright 2014 Cubane Canada, Inc. All rights reserved. + * Copyright 2015 Microsoft Corporation. All rights reserved. + * Copyright 2016 Microsoft, Inc. All rights reserved. + * Copyright 2017 Caitlin Potter. All rights reserved. + * Copyright (C) 2016-2020 the V8 project authors. All rights reserved. + * Copyright (C) 2018-2020 Rick Waldron. All rights reserved. + * Copyright (C) 2019 Leo Balter. All rights reserved. + * Test262 portions are governed by the BSD license in LICENSE.test262. + */ +import { describe, expect, test } from "bun:test" +import { Effect } from "effect" +import { CodeMode } from "../src/index.js" + +const execute = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools: {}, limits: { timeoutMs: 1_000 } })) + +const value = async (code: string) => { + const result = await execute(code) + if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) + return result.value +} + +describe("Test262 Promise statics", () => { + test("statics are callable and return promises", async () => { + // Sources: + // test/built-ins/Promise/all/S25.4.4.1_A1.1_T1.js + // test/built-ins/Promise/allSettled/is-function.js + // test/built-ins/Promise/allSettled/returns-promise.js + // test/built-ins/Promise/race/S25.4.4.3_A1.1_T1.js + // test/built-ins/Promise/resolve/S25.4.4.5_A1.1_T1.js + // test/built-ins/Promise/reject/S25.4.4.4_A1.1_T1.js + expect( + await value(` + const values = [ + Promise.all([]), + Promise.allSettled([]), + Promise.race([undefined]), + Promise.resolve(), + Promise.reject(), + ] + const callable = [ + typeof Promise.all, + typeof Promise.allSettled, + typeof Promise.race, + typeof Promise.resolve, + typeof Promise.reject, + ] + try { await values[4] } catch {} + return [callable, values.map((item) => item instanceof Promise)] + `), + ).toEqual([ + ["function", "function", "function", "function", "function"], + [true, true, true, true, true], + ]) + }) + + test("Promise.all returns fresh arrays for empty and settled inputs", async () => { + // Sources: + // test/built-ins/Promise/all/S25.4.4.1_A2.1_T1.js + // test/built-ins/Promise/all/S25.4.4.1_A2.3_T1.js + // test/built-ins/Promise/all/S25.4.4.1_A2.3_T2.js + // test/built-ins/Promise/all/S25.4.4.1_A2.3_T3.js + // test/built-ins/Promise/all/S25.4.4.1_A7.1_T1.js + expect( + await value(` + const input = [] + const emptyPromise = Promise.all(input) + const empty = await emptyPromise + const onePromise = Promise.all([Promise.resolve(3)]) + const one = await onePromise + return [ + emptyPromise instanceof Promise, + empty instanceof Array, + empty.length, + empty !== input, + onePromise instanceof Promise, + one instanceof Array, + one.length, + one[0], + ] + `), + ).toEqual([true, true, 0, true, true, true, 1, 3]) + }) + + test("Promise.all adopts values and preserves input order and identity", async () => { + // Sources: + // test/built-ins/Promise/all/resolve-non-thenable.js + // test/built-ins/Promise/all/S25.4.4.1_A8.2_T1.js + // test/built-ins/Promise/all/S25.4.4.1_A8.2_T2.js + const result = await value(` + const first = { id: 1 } + const second = { id: 2 } + const values = await Promise.all([Promise.resolve(3), first, Promise.resolve(second)]) + const observe = async (promise) => { + try { await promise; return "fulfilled" } catch (reason) { return reason } + } + return [ + values.length, + values[0], + values[1] === first, + values[2] === second, + await observe(Promise.all([Promise.reject(1), Promise.resolve(2)])), + await observe(Promise.all([Promise.resolve(1), Promise.reject(2)])), + ] + `) + expect(result).toEqual([3, 3, true, true, 1, 2]) + }) + + test("Promise.allSettled returns fresh arrays and ordered outcome records", async () => { + // Sources: + // test/built-ins/Promise/allSettled/resolves-empty-array.js + // test/built-ins/Promise/allSettled/resolves-to-array.js + // test/built-ins/Promise/allSettled/resolved-all-fulfilled.js + // test/built-ins/Promise/allSettled/resolved-all-rejected.js + // test/built-ins/Promise/allSettled/resolved-all-mixed.js + // test/built-ins/Promise/allSettled/resolve-non-thenable.js + expect( + await value(` + const input = [] + const empty = await Promise.allSettled(input) + const reason = { id: 4 } + const object = { id: 5 } + const outcomes = await Promise.allSettled([ + Promise.resolve(1), + Promise.reject(2), + 3, + Promise.reject(reason), + object, + ]) + return [ + empty instanceof Array, + empty.length, + empty !== input, + outcomes, + outcomes[4].value === object, + outcomes.map((item) => Object.keys(item)), + ] + `), + ).toEqual([ + true, + 0, + true, + [ + { status: "fulfilled", value: 1 }, + { status: "rejected", reason: 2 }, + { status: "fulfilled", value: 3 }, + { status: "rejected", reason: { id: 4 } }, + { status: "fulfilled", value: { id: 5 } }, + ], + true, + [ + ["status", "value"], + ["status", "reason"], + ["status", "value"], + ["status", "reason"], + ["status", "value"], + ], + ]) + }) + + test("Promise.race preserves fulfillment, rejection, and iterable order", async () => { + // Sources: + // test/built-ins/Promise/race/S25.4.4.3_A6.2_T1.js + // test/built-ins/Promise/race/S25.4.4.3_A7.1_T1.js + // test/built-ins/Promise/race/S25.4.4.3_A7.2_T1.js + // test/built-ins/Promise/race/S25.4.4.3_A7.3_T1.js + // test/built-ins/Promise/race/S25.4.4.3_A7.3_T2.js + expect( + await value(` + const observe = async (promise) => { + try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } + } + return await Promise.all([ + observe(Promise.race([23])), + observe(Promise.race([Promise.reject(7)])), + observe(Promise.race([Promise.resolve(1), Promise.resolve(2)])), + observe(Promise.race([Promise.reject(3), Promise.resolve(4)])), + ]) + `), + ).toEqual([ + ["fulfilled", 23], + ["rejected", 7], + ["fulfilled", 1], + ["rejected", 3], + ]) + }) + + test("combinators consume supported string iterables", async () => { + // Sources: + // test/built-ins/Promise/all/iter-arg-is-string-resolve.js + // test/built-ins/Promise/allSettled/iter-arg-is-string-resolve.js + // test/built-ins/Promise/race/iter-arg-is-string-resolve.js + expect( + await value(` + return [ + await Promise.all("abc"), + await Promise.allSettled("ab"), + await Promise.race("abc"), + ] + `), + ).toEqual([ + ["a", "b", "c"], + [ + { status: "fulfilled", value: "a" }, + { status: "fulfilled", value: "b" }, + ], + "a", + ]) + }) + + test("Promise.resolve adopts values and preserves CodeMode-promise identity", async () => { + // Sources: + // test/built-ins/Promise/resolve/S25.4.4.5_A2.1_T1.js + // test/built-ins/Promise/resolve/resolve-non-obj.js + // test/built-ins/Promise/resolve/resolve-non-thenable.js + expect( + await value(` + const object = { id: 1 } + const promise = Promise.resolve(1) + return [ + await Promise.resolve(23), + await Promise.resolve(Promise.resolve(24)), + (await Promise.resolve(object)) === object, + [promise].includes(Promise.resolve(promise)), + ] + `), + ).toEqual([23, 24, true, true]) + }) + + test("Promise.reject preserves primitive and object reasons", async () => { + // Sources: + // test/built-ins/Promise/reject/S25.4.4.4_A2.1_T1.js + const result = await value(` + const object = { reason: true } + const reasons = [undefined, null, false, true, 0, "", 42, object] + const observe = async (reason) => { + try { await Promise.reject(reason); return false } catch (caught) { return caught === reason } + } + return await Promise.all(reasons.map(observe)) + `) + expect(result).toEqual([true, true, true, true, true, true, true, true]) + }) + + test("Promise.all resolves duplicate members into every slot", async () => { + // Sources: + // test/built-ins/Promise/all/invoke-resolve-on-promises-every-iteration-of-promise.js + // test/built-ins/Promise/all/invoke-resolve-on-values-every-iteration-of-promise.js + // (adapted: CodeMode has no observable Promise.resolve hook, so per-iteration + // handling of a repeated member is asserted through the resolved slots) + expect( + await value(` + const settled = Promise.resolve(3) + const computed = (async () => "computed")() + return [ + await Promise.all([settled, settled, settled]), + await Promise.all([computed, "plain", computed]), + ] + `), + ).toEqual([ + [3, 3, 3], + ["computed", "plain", "computed"], + ]) + }) + + test("Promise.allSettled records duplicate members independently", async () => { + // Source: test/built-ins/Promise/allSettled/invoke-resolve-on-promises-every-iteration-of-promise.js + // (adapted: per-iteration handling of a repeated member is asserted through the + // outcome records instead of a Promise.resolve hook) + expect( + await value(` + const good = Promise.resolve(1) + const bad = Promise.reject(2) + return await Promise.allSettled([good, bad, good, bad]) + `), + ).toEqual([ + { status: "fulfilled", value: 1 }, + { status: "rejected", reason: 2 }, + { status: "fulfilled", value: 1 }, + { status: "rejected", reason: 2 }, + ]) + }) + + test("combinators adopt members that settled before the call", async () => { + // Sources: + // test/built-ins/Promise/all/reject-immed.js + // test/built-ins/Promise/allSettled/reject-immed.js + // test/built-ins/Promise/race/reject-immed.js + // (adapted: immediately-rejecting thenables become CodeMode promises that settled, + // and were even observed, before the combinator call) + expect( + await value(` + const fulfilled = Promise.resolve("done") + const rejected = Promise.reject("failed") + try { await rejected } catch {} + const observe = async (promise) => { + try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } + } + return [ + await observe(Promise.all([fulfilled, rejected])), + await Promise.allSettled([rejected, fulfilled]), + await observe(Promise.race([rejected, fulfilled])), + ] + `), + ).toEqual([ + ["rejected", "failed"], + [ + { status: "rejected", reason: "failed" }, + { status: "fulfilled", value: "done" }, + ], + ["rejected", "failed"], + ]) + }) + + test("combinator results follow input order, not settlement order", async () => { + // Sources: + // test/built-ins/Promise/all/resolve-non-thenable.js + // test/built-ins/Promise/allSettled/resolved-all-mixed.js + // (adapted: members are created, and therefore settle, in reverse of input order; + // deferred settlement is not expressible without host-async work in this corpus) + expect( + await value(` + const third = Promise.resolve("c") + const failing = (async () => { throw "b" })() + try { await failing } catch {} + const second = (async () => "b")() + const first = Promise.resolve("a") + return [ + await Promise.all([first, second, third]), + await Promise.allSettled([first, failing, third]), + ] + `), + ).toEqual([ + ["a", "b", "c"], + [ + { status: "fulfilled", value: "a" }, + { status: "rejected", reason: "b" }, + { status: "fulfilled", value: "c" }, + ], + ]) + }) + + test("Promise.race ignores a rejected loser once the first contender wins", async () => { + // Source: test/built-ins/Promise/race/reject-ignored-immed.js + // (adapted: the losing rejection comes from an async function instead of a thenable; + // the exact-equality check also asserts the loser leaves no unhandled-rejection warning) + expect( + await execute(` + const loser = (async () => { throw "lost" })() + return await Promise.race([Promise.resolve("won"), loser]) + `), + ).toEqual({ ok: true, value: "won", toolCalls: [] }) + }) + + test("Promise.race([]) returns a promise whose CodeMode failure is catchable", async () => { + // Sources: + // test/built-ins/Promise/race/S25.4.4.3_A2.1_T1.js + // test/built-ins/Promise/race/S25.4.4.3_A5.1_T1.js + // (adapted: upstream requires Promise.race([]) to never settle; CodeMode intentionally + // rejects with a catchable diagnostic instead of hanging, so this asserts CodeMode + // divergence rather than the spec never-settles behavior) + expect( + await value(` + const empty = Promise.race([]) + try { + await empty + return "settled" + } catch (error) { + return [empty instanceof Promise, error instanceof Error] + } + `), + ).toEqual([true, true]) + }) + + test("Promise.resolve passes the same CodeMode promise through nested chains", async () => { + // Source: test/built-ins/Promise/resolve/S25.4.4.5_A2.2_T1.js + // (adapted: no executor construction, and identity is observed with Array includes + // because promises are not comparable data values in CodeMode) + expect( + await value(` + const promise = Promise.resolve({ id: 1 }) + return [ + [promise].includes(Promise.resolve(promise)), + [promise].includes(Promise.resolve(Promise.resolve(promise))), + (await Promise.resolve(Promise.resolve(promise))).id, + ] + `), + ).toEqual([true, true, 1]) + }) + + test("Promise.resolve of a rejected promise preserves identity and reason", async () => { + // Source: test/built-ins/Promise/resolve/S25.4.4.5_A2.3_T1.js + // (adapted: the source promise is already rejected instead of rejected later) + expect( + await value(` + const rejected = Promise.reject("oops") + const adopted = Promise.resolve(rejected) + const identity = [rejected].includes(adopted) + try { + await adopted + return "fulfilled" + } catch (reason) { + return [identity, reason] + } + `), + ).toEqual([true, "oops"]) + }) + + test("Promise.reject uses a promise reason without flattening it", async () => { + // Sources: + // test/built-ins/Promise/reject-via-fn-immed.js + // test/built-ins/Promise/reject-via-fn-deferred.js + // (adapted: the promise reason goes through Promise.reject instead of executor reject) + expect( + await value(` + const observe = async (reason) => { + try { + await Promise.reject(reason) + return "fulfilled" + } catch (caught) { + const identity = [reason].includes(caught) + try { return [identity, caught instanceof Promise, await caught] } + catch (inner) { return [identity, caught instanceof Promise, "rethrew " + inner] } + } + } + return [await observe(Promise.resolve(1)), await observe(Promise.reject("inner"))] + `), + ).toEqual([ + [true, true, 1], + [true, true, "rethrew inner"], + ]) + }) +}) + +describe("Test262 async functions and await", () => { + test("declaration, expression, and arrow forms return promises", async () => { + // Sources: + // test/language/statements/async-function/declaration-returns-promise.js + // test/language/expressions/async-function/expression-returns-promise.js + // test/language/expressions/async-arrow-function/arrow-returns-promise.js + expect( + await value(` + async function declaration() { return 1 } + const expression = async function() { return 2 } + const arrow = async () => 3 + const promises = [declaration(), expression(), arrow()] + return [promises.map((item) => item instanceof Promise), await Promise.all(promises)] + `), + ).toEqual([ + [true, true, true], + [1, 2, 3], + ]) + }) + + test("async bodies adopt returns and reject throws before and after await", async () => { + // Sources: + // test/language/statements/async-function/evaluation-body.js + // test/language/statements/async-function/evaluation-body-that-returns.js + // test/language/statements/async-function/evaluation-body-that-returns-after-await.js + // test/language/statements/async-function/evaluation-body-that-throws.js + // test/language/statements/async-function/evaluation-body-that-throws-after-await.js + expect( + await value(` + const order = [] + const plain = async () => { order.push("body"); return 42 } + const afterAwait = async () => { await Promise.resolve(); return 43 } + const throwsBefore = async () => { throw 1 } + const throwsAfter = async () => { await Promise.resolve(); throw 2 } + const observe = async (promise) => { + try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } + } + const first = plain() + return [ + order, + await observe(first), + await observe(afterAwait()), + await observe(throwsBefore()), + await observe(throwsAfter()), + ] + `), + ).toEqual([["body"], ["fulfilled", 42], ["fulfilled", 43], ["rejected", 1], ["rejected", 2]]) + }) + + test("default-parameter throws reject instead of escaping the call", async () => { + // Source: test/language/statements/async-function/evaluation-default-that-throws.js + expect( + await value(` + const fail = () => { throw new Error("default") } + const run = async (value = fail()) => value + let returned = false + try { + const promise = run() + returned = promise instanceof Promise + await promise + return [returned, "fulfilled"] + } catch (error) { + return [returned, error.message] + } + `), + ).toEqual([true, "default"]) + }) + + test("async try/finally completion records override earlier completion", async () => { + // Sources: the try-{return,throw,reject}-finally-{return,throw,reject}.js matrix under + // test/language/statements/async-function, test/language/expressions/async-function, + // and test/language/expressions/async-arrow-function. + expect( + await value(` + const observe = async (promise) => { + try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } + } + const returnReturn = async () => { try { return "early" } finally { return await Promise.resolve("override") } } + const returnThrow = async () => { try { return "early" } finally { throw "override" } } + const returnReject = async () => { try { return "early" } finally { await Promise.reject("override") } } + const throwReturn = async () => { try { throw "early" } finally { return await Promise.resolve("override") } } + const throwThrow = async () => { try { throw "early" } finally { throw "override" } } + const throwReject = async () => { try { throw "early" } finally { await Promise.reject("override") } } + const rejectReturn = async () => { try { await Promise.reject("early") } finally { return await Promise.resolve("override") } } + const rejectThrow = async () => { try { await Promise.reject("early") } finally { throw "override" } } + const rejectReject = async () => { try { await Promise.reject("early") } finally { await Promise.reject("override") } } + return await Promise.all([ + observe(returnReturn()), observe(returnThrow()), observe(returnReject()), + observe(throwReturn()), observe(throwThrow()), observe(throwReject()), + observe(rejectReturn()), observe(rejectThrow()), observe(rejectReject()), + ]) + `), + ).toEqual([ + ["fulfilled", "override"], + ["rejected", "override"], + ["rejected", "override"], + ["fulfilled", "override"], + ["rejected", "override"], + ["rejected", "override"], + ["fulfilled", "override"], + ["rejected", "override"], + ["rejected", "override"], + ]) + }) + + test("await preserves an object whose then property is not callable", async () => { + // Source: test/language/expressions/await/await-awaits-thenable-not-callable.js + expect( + await value(` + const thenable = { then: 42 } + return (await thenable) === thenable + `), + ).toBe(true) + }) + + test("await returns non-promise operands unchanged", async () => { + // Source: test/language/expressions/await/await-non-promise.js + // (adapted: only value pass-through is asserted here; the spec tick ordering around + // await of non-promises is covered by the failing interleaving test below) + expect( + await value(` + const object = { id: 1 } + const array = [1, 2] + return [ + await 1, + await "text", + await true, + (await null) === null, + (await undefined) === undefined, + (await object) === object, + (await array) === array, + ] + `), + ).toEqual([1, "text", true, true, true, true, true]) + }) +}) + +describe("Test262 expected Promise conformance", () => { + for (const name of ["all", "allSettled", "race", "any"] as const) { + test(`Promise.${name} rejects invalid input with TypeError`, async () => { + // Sources: + // test/built-ins/Promise/all/S25.4.4.1_A3.1_T1.js + // test/built-ins/Promise/all/S25.4.4.1_A3.1_T2.js + // test/built-ins/Promise/allSettled/iter-arg-is-number-reject.js + // test/built-ins/Promise/race/iter-arg-is-number-reject.js + // test/built-ins/Promise/any/iter-arg-is-number-reject.js + expect( + await value(` + try { + const promise = Promise.${name}(42) + const returned = promise instanceof Promise + await promise + return [returned, "fulfilled"] + } catch (error) { + return [true, error.name] + } + `), + ).toEqual([true, "TypeError"]) + }) + } + + test("Promise.all consumes sparse positions as undefined", async () => { + // Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior) + expect( + await value(` + const input = [] + input[1] = 1 + const result = await Promise.all(input) + return [result.length, result[0] === undefined, result[1]] + `), + ).toEqual([2, true, 1]) + }) + + test("Promise.allSettled consumes sparse positions as undefined", async () => { + // Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior) + expect( + await value(` + const input = [] + input[1] = 1 + const result = await Promise.allSettled(input) + return [result.length, result[0].status, result[0].value === undefined, result[1]] + `), + ).toEqual([2, "fulfilled", true, { status: "fulfilled", value: 1 }]) + }) + + test("Promise.race consumes a sparse first position as undefined", async () => { + // Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior) + expect( + await value(` + const input = [] + input[1] = 1 + return (await Promise.race(input)) === undefined + `), + ).toBe(true) + }) + + test("Promise.any consumes a sparse first position as an undefined fulfillment", async () => { + // Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior) + expect( + await value(` + const input = [] + input[1] = Promise.reject("loses") + return (await Promise.any(input)) === undefined + `), + ).toBe(true) + }) + + test("Promise.all settles after reactions attached to its inputs", async () => { + // Sources: + // test/built-ins/Promise/all/S25.4.4.1_A7.2_T1.js + // test/built-ins/Promise/all/S25.4.4.1_A8.1_T1.js + expect( + await value(` + const sequence = [1] + const input = Promise.resolve(1) + const aggregate = Promise.all([input]) + aggregate.then(() => sequence.push(4)) + input.then(() => sequence.push(3)).then(() => sequence.push(5)) + sequence.push(2) + await aggregate + await Promise.resolve() + return sequence + `), + ).toEqual([1, 2, 3, 4, 5]) + }) + + test("Promise.allSettled settles after reactions attached to its inputs", async () => { + // Sources: + // test/built-ins/Promise/allSettled/resolved-sequence.js + // test/built-ins/Promise/allSettled/resolved-sequence-extra-ticks.js + // test/built-ins/Promise/allSettled/resolved-sequence-mixed.js + // test/built-ins/Promise/allSettled/resolved-sequence-with-rejections.js + expect( + await value(` + const sequence = [1] + const input = Promise.resolve(1) + const aggregate = Promise.allSettled([input]) + aggregate.then(() => sequence.push(4)) + input.then(() => sequence.push(3)).then(() => sequence.push(5)) + sequence.push(2) + await aggregate + await Promise.resolve() + return sequence + `), + ).toEqual([1, 2, 3, 4, 5]) + }) + + test("Promise.race settles in a reaction after its winning input", async () => { + // Sources: + // test/built-ins/Promise/race/S25.4.4.3_A6.1_T1.js + // test/built-ins/Promise/race/resolved-sequence-extra-ticks.js + expect( + await value(` + const sequence = [1] + const race = Promise.race([1]) + race.then(() => sequence.push(4)) + Promise.resolve().then(() => sequence.push(3)).then(() => sequence.push(5)) + sequence.push(2) + await race + await Promise.resolve() + return sequence + `), + ).toEqual([1, 2, 3, 4, 5]) + }) + + test("then reactions route and propagate fulfillment and rejection", async () => { + // Sources: + // test/built-ins/Promise/prototype/then/prfm-fulfilled.js + // test/built-ins/Promise/prototype/then/prfm-rejected.js + // test/built-ins/Promise/prototype/then/rxn-handler-identity.js + // test/built-ins/Promise/prototype/then/rxn-handler-thrower.js + // test/built-ins/Promise/prototype/then/rxn-handler-fulfilled-return-normal.js + // test/built-ins/Promise/prototype/then/rxn-handler-fulfilled-return-abrupt.js + // test/built-ins/Promise/prototype/then/rxn-handler-rejected-return-normal.js + // test/built-ins/Promise/prototype/then/rxn-handler-rejected-return-abrupt.js + expect( + await value(` + const observe = async (promise) => { + try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } + } + return await Promise.all([ + observe(Promise.resolve(1).then((value) => value + 1)), + observe(Promise.reject(2).then(undefined, (reason) => reason + 1)), + observe(Promise.resolve(3).then(undefined)), + observe(Promise.reject(4).then(undefined)), + observe(Promise.resolve(5).then(() => { throw 6 })), + observe(Promise.reject(7).then(undefined, () => { throw 8 })), + ]) + `), + ).toEqual([ + ["fulfilled", 2], + ["fulfilled", 3], + ["fulfilled", 3], + ["rejected", 4], + ["rejected", 6], + ["rejected", 8], + ]) + }) + + test("then reactions preserve breadth-first queue order", async () => { + // Source: test/built-ins/Promise/prototype/then/S25.4.4_A1.1_T1.js + expect( + await value(` + const sequence = [1] + const promise = Promise.resolve() + const first = promise.then(() => sequence.push(3)).then(() => sequence.push(5)).then(() => sequence.push(7)) + const second = promise.then(() => sequence.push(4)).then(() => sequence.push(6)).then(() => sequence.push(8)) + sequence.push(2) + await Promise.all([first, second]) + return sequence + `), + ).toEqual([1, 2, 3, 4, 5, 6, 7, 8]) + }) + + test("then rejects direct self-resolution for fulfilled and rejected sources", async () => { + // Sources: + // test/built-ins/Promise/prototype/then/resolve-settled-fulfilled-self.js + // test/built-ins/Promise/prototype/then/resolve-settled-rejected-self.js + // test/built-ins/Promise/prototype/then/resolve-pending-fulfilled-self.js + // test/built-ins/Promise/prototype/then/resolve-pending-rejected-self.js + expect( + await value(` + const observe = async (promise) => { + try { await promise; return "fulfilled" } catch (reason) { return reason.name } + } + let fulfilled + let rejected + fulfilled = Promise.resolve().then(() => fulfilled) + rejected = Promise.reject().then(undefined, () => rejected) + return await Promise.all([observe(fulfilled), observe(rejected)]) + `), + ).toEqual(["TypeError", "TypeError"]) + }) + + test("catch delegates rejection handling and preserves fulfillment", async () => { + // Sources: + // test/built-ins/Promise/prototype/catch/S25.4.5.1_A2.1_T1.js + // test/built-ins/Promise/prototype/catch/S25.4.5.1_A3.1_T1.js + // test/built-ins/Promise/prototype/catch/S25.4.5.1_A3.1_T2.js + expect( + await value(` + return [ + await Promise.resolve(1).catch(() => 2), + await Promise.reject(3).catch((reason) => reason + 1), + ] + `), + ).toEqual([1, 4]) + }) + + test("finally preserves or replaces the original settlement", async () => { + // Sources: + // test/built-ins/Promise/prototype/finally/resolution-value-no-override.js + // test/built-ins/Promise/prototype/finally/rejection-reason-no-fulfill.js + // test/built-ins/Promise/prototype/finally/rejection-reason-override-with-throw.js + expect( + await value(` + const observe = async (promise) => { + try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } + } + return await Promise.all([ + observe(Promise.resolve(1).finally(() => 2)), + observe(Promise.reject(3).finally(() => 4)), + observe(Promise.reject(5).finally(() => { throw 6 })), + ]) + `), + ).toEqual([ + ["fulfilled", 1], + ["rejected", 3], + ["rejected", 6], + ]) + }) + + test("then ignores non-callable handlers", async () => { + // Sources: + // test/built-ins/Promise/prototype/then/S25.4.5.3_A4.1_T1.js + // test/built-ins/Promise/prototype/then/S25.4.5.3_A4.1_T2.js + // test/built-ins/Promise/prototype/then/S25.4.5.3_A5.1_T1.js + // test/built-ins/Promise/prototype/then/S25.4.5.3_A5.2_T1.js + // (adapted: only non-callable handlers are probed; callables that are not plain + // functions, such as tool references, intentionally throw in CodeMode) + expect( + await value(` + const observe = async (promise) => { + try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } + } + return await Promise.all([ + observe(Promise.resolve(1).then(2)), + observe(Promise.resolve(4).then(null, null)), + observe(Promise.resolve(5).then({}, "x")), + observe(Promise.reject(3).then(null, "x")), + observe(Promise.reject(6).then(7, {})), + ]) + `), + ).toEqual([ + ["fulfilled", 1], + ["fulfilled", 4], + ["fulfilled", 5], + ["rejected", 3], + ["rejected", 6], + ]) + }) + + test("finally waits for a returned promise and preserves or replaces settlement", async () => { + // Sources: + // test/built-ins/Promise/prototype/finally/resolved-observable-then-calls.js + // test/built-ins/Promise/prototype/finally/rejected-observable-then-calls.js + // test/built-ins/Promise/prototype/finally/resolution-value-no-override.js + expect( + await value(` + const observe = async (promise) => { + try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } + } + const order = [] + const cleanup = async () => { + await Promise.resolve() + order.push("cleanup") + } + const settled = await Promise.resolve("kept").finally(() => cleanup()) + order.push("settled:" + settled) + return [ + await observe(Promise.resolve(1).finally(() => Promise.resolve(99))), + order, + await observe(Promise.resolve(2).finally(() => Promise.reject(3))), + await observe(Promise.reject(4).finally(() => Promise.resolve(99))), + ] + `), + ).toEqual([ + ["fulfilled", 1], + ["cleanup", "settled:kept"], + ["rejected", 3], + ["rejected", 4], + ]) + }) + + test("then adopts a returned rejected promise", async () => { + // Sources: + // test/built-ins/Promise/prototype/then/rxn-handler-fulfilled-return-abrupt.js + // test/built-ins/Promise/resolve/resolve-promise.js + // (adapted: the fulfillment handler returns an already-rejected promise instead of + // throwing, and the rejection handler recovers with a fulfilled promise) + expect( + await value(` + const observe = async (promise) => { + try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } + } + return await Promise.all([ + observe(Promise.resolve(1).then(() => Promise.reject("bad"))), + observe(Promise.reject(2).then(undefined, () => Promise.resolve("ok"))), + ]) + `), + ).toEqual([ + ["rejected", "bad"], + ["fulfilled", "ok"], + ]) + }) + + test("independent reactions on one source each observe the same settlement", async () => { + // Source: test/built-ins/Promise/prototype/then/S25.4.4_A2.1_T1.js + // (adapted: the multiple-reactions family is asserted through the values every + // reaction returns instead of a shared completion counter) + expect( + await value(` + const fulfilled = Promise.resolve(7) + const rejected = Promise.reject(8) + return await Promise.all([ + fulfilled.then((value) => "first:" + value), + fulfilled.then((value) => "second:" + value), + rejected.catch((reason) => "first:" + reason), + rejected.catch((reason) => "second:" + reason), + ]) + `), + ).toEqual(["first:7", "second:7", "first:8", "second:8"]) + }) + + test("await always resumes in a later reaction and interleaves async functions", async () => { + // Sources: + // test/language/expressions/await/async-await-interleaved.js + // test/language/expressions/await/await-non-promise.js + expect( + await value(` + const sequence = [] + const first = async () => { sequence.push("first:1"); await 0; sequence.push("first:2") } + const second = async () => { sequence.push("second:1"); await 0; sequence.push("second:2") } + await Promise.all([first(), second()]) + return sequence + `), + ).toEqual(["first:1", "second:1", "first:2", "second:2"]) + }) + + test("an async function rejects when it resolves with its own promise", async () => { + // Adapted from the self-resolution requirement represented by: + // test/built-ins/Promise/resolve-self.js + // test/built-ins/Promise/resolve/S25.4.4.5_A4.1_T1.js + expect( + await value(` + let promise + const run = async () => { + await Promise.resolve() + return promise + } + promise = run() + try { + await promise + return "fulfilled" + } catch (error) { + return error.name + } + `), + ).toBe("TypeError") + }) + + test.failing("Promise.resolve recursively assimilates callable thenables", async () => { + // Source: test/built-ins/Promise/resolve/resolve-thenable.js + expect( + await value(` + const value = { id: 1 } + const nested = { then: (resolve) => resolve(value) } + const thenable = { then: (resolve) => resolve(nested) } + return (await Promise.resolve(thenable)) === value + `), + ).toBe(true) + }) + + test.failing("Promise combinators assimilate callable thenable inputs", async () => { + // Sources: + // test/built-ins/Promise/all/reject-immed.js + // test/built-ins/Promise/all/reject-ignored-immed.js + // test/built-ins/Promise/allSettled/reject-ignored-immed.js + // test/built-ins/Promise/race/resolve-thenable.js + expect( + await value(` + const fulfills = { then: (resolve) => resolve(1) } + const rejects = { then: (_, reject) => reject(2) } + const resolvesFirst = { then: (resolve, reject) => { resolve(3); reject(4) } } + const observe = async (promise) => { + try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } + } + return [ + await observe(Promise.all([fulfills, rejects])), + await Promise.allSettled([fulfills, resolvesFirst]), + await observe(Promise.race([rejects])), + ] + `), + ).toEqual([ + ["rejected", 2], + [ + { status: "fulfilled", value: 1 }, + { status: "fulfilled", value: 3 }, + ], + ["rejected", 2], + ]) + }) + + test.failing("await assimilates callable thenables", async () => { + // Source: test/language/expressions/await/await-awaits-thenables.js + expect( + await value(` + const thenable = { then: (resolve) => resolve(42) } + return await thenable + `), + ).toBe(42) + }) + + test.failing("await rejects when a callable thenable throws", async () => { + // Source: test/language/expressions/await/await-awaits-thenables-that-throw.js + expect( + await value(` + const error = { id: 1 } + const thenable = { then: () => { throw error } } + try { + await thenable + return false + } catch (caught) { + return caught === error + } + `), + ).toBe(true) + }) +}) + +describe("Test262 Promise.any", () => { + test("is a callable static that returns a promise", async () => { + // Sources: + // test/built-ins/Promise/any/is-function.js + // test/built-ins/Promise/any/returns-promise.js + expect( + await value(` + const promise = Promise.any([1]) + return [typeof Promise.any, promise instanceof Promise, await promise] + `), + ).toEqual(["function", true, 1]) + }) + + test("fulfills with the first fulfilled member, ignoring rejections", async () => { + // Sources: + // test/built-ins/Promise/any/resolved-sequence-mixed.js + // test/built-ins/Promise/any/resolved-sequence-with-rejections.js + // test/built-ins/Promise/any/reject-ignored-immed.js + expect( + await value(` + return [ + await Promise.any([Promise.reject("a"), Promise.resolve(1), Promise.resolve(2)]), + await Promise.any([Promise.reject("a"), "plain", Promise.reject("b")]), + ] + `), + ).toEqual([1, "plain"]) + }) + + test("a fulfillment wins over a later rejection of another member", async () => { + // Sources: + // test/built-ins/Promise/any/resolve-ignores-late-rejection.js + // test/built-ins/Promise/any/resolve-ignores-late-rejection-deferred.js + expect( + await value(` + let rejectLate + const late = new Promise((_, reject) => { rejectLate = reject }) + const result = await Promise.any([late, Promise.resolve("won")]) + rejectLate("too late") + return result + `), + ).toBe("won") + }) + + test("rejects with an AggregateError carrying the reasons in input order", async () => { + // Sources: + // test/built-ins/Promise/any/reject-all-mixed.js + // test/built-ins/Promise/any/reject-immed.js + // test/built-ins/Promise/any/reject-deferred.js + expect( + await value(` + let rejectLate + const late = new Promise((_, reject) => { rejectLate = reject }) + const aggregate = Promise.any([Promise.reject("first"), late, Promise.reject("third")]) + rejectLate("second") + try { + await aggregate + return "fulfilled" + } catch (error) { + return { + isAggregate: error instanceof AggregateError, + isError: error instanceof Error, + name: error.name, + message: error.message, + errors: error.errors, + } + } + `), + ).toEqual({ + isAggregate: true, + isError: true, + name: "AggregateError", + message: "All promises were rejected", + errors: ["first", "second", "third"], + }) + }) + + test("rejects an empty input with an empty AggregateError", async () => { + // Source: test/built-ins/Promise/any/iter-arg-is-empty-iterable-reject.js + expect( + await value(` + try { + await Promise.any([]) + return "fulfilled" + } catch (error) { + return [error instanceof AggregateError, error.errors.length] + } + `), + ).toEqual([true, 0]) + }) + + test("consumes a string input as its characters", async () => { + // Source: test/built-ins/Promise/any/iter-arg-is-string-resolve.js + expect(await value(`return await Promise.any("abc")`)).toBe("a") + }) + + test("rejects an empty string input with an empty AggregateError", async () => { + // Source: test/built-ins/Promise/any/iter-arg-is-empty-string-reject.js + expect( + await value(` + try { + await Promise.any("") + return "fulfilled" + } catch (error) { + return [error instanceof AggregateError, error.errors.length] + } + `), + ).toEqual([true, 0]) + }) + + test("fulfills with the first member that does not reject", async () => { + // Sources: + // test/built-ins/Promise/any/resolve-from-reject-catch.js + // test/built-ins/Promise/any/resolve-from-resolve-reject-catch.js + expect( + await value(` + return await Promise.any([ + Promise.reject("a"), + new Promise((resolve, reject) => reject("b")), + Promise.all([Promise.reject("c")]), + Promise.resolve(Promise.reject("d").catch((reason) => reason)), + ]) + `), + ).toBe("d") + }) + + test("settles after reactions attached to its inputs", async () => { + // Source: test/built-ins/Promise/any/resolved-sequence.js + expect( + await value(` + const sequence = [1] + const input = Promise.resolve(1) + const aggregate = Promise.any([input]) + aggregate.then(() => sequence.push(4)) + input.then(() => sequence.push(3)).then(() => sequence.push(5)) + sequence.push(2) + await aggregate + await Promise.resolve() + return sequence + `), + ).toEqual([1, 2, 3, 4, 5]) + }) +}) + +describe("Test262 AggregateError", () => { + test("constructs from an errors collection and an optional message", async () => { + // Sources: + // test/built-ins/AggregateError/errors-iterabletolist.js + // test/built-ins/AggregateError/message-undefined-no-prop.js + expect( + await value(` + const input = ["x", "y"] + const withMessage = new AggregateError(input, "msg") + const bare = new AggregateError([]) + return [ + withMessage.name, + withMessage.message, + withMessage.errors, + withMessage.errors !== input, + withMessage instanceof AggregateError, + withMessage instanceof Error, + bare.message, + bare.errors, + ] + `), + ).toEqual(["AggregateError", "msg", ["x", "y"], true, true, true, "", []]) + }) + + test("rejects a non-collection errors argument with TypeError", async () => { + // Source: test/built-ins/AggregateError/errors-iterabletolist-failures.js + expect( + await value(` + try { + new AggregateError(42) + return "constructed" + } catch (error) { + return error.name + } + `), + ).toBe("TypeError") + }) + + test("is callable without new", async () => { + // Source: test/built-ins/AggregateError/newtarget-is-undefined.js + expect( + await value(` + const error = AggregateError(["x"], "m") + return [error instanceof AggregateError, error instanceof Error, error.name, error.message, error.errors] + `), + ).toEqual([true, true, "AggregateError", "m", ["x"]]) + }) + + test("coerces a non-string message to a string", async () => { + // Source: test/built-ins/AggregateError/message-method-prop-cast.js (value coercion only; the + // upstream object-with-toString case is omitted because CodeMode has no user toString dispatch) + expect( + await value(` + return [ + new AggregateError([], 42).message, + new AggregateError([], false).message, + new AggregateError([], true).message, + new AggregateError([], null).message, + ] + `), + ).toEqual(["42", "false", "true", "null"]) + }) +}) + +describe("Test262 Promise constructor", () => { + test("constructs a promise, handing the executor callable resolve/reject", async () => { + // Sources: + // test/built-ins/Promise/constructor.js + // test/built-ins/Promise/exec-args.js + expect( + await value(` + let observed + const promise = new Promise((resolve, reject) => { + observed = [typeof resolve, typeof reject] + resolve("done") + }) + return [promise instanceof Promise, observed, await promise] + `), + ).toEqual([true, ["function", "function"], "done"]) + }) + + test("a missing or non-callable executor is a TypeError", async () => { + // Source: test/built-ins/Promise/executor-not-callable.js + expect( + await value(` + const outcomes = [] + for (const make of [() => new Promise(), () => new Promise(1), () => new Promise({})]) { + try { + make() + outcomes.push("constructed") + } catch (error) { + outcomes.push(error.name) + } + } + return outcomes + `), + ).toEqual(["TypeError", "TypeError", "TypeError"]) + }) + + test("resolves immediately or later through an escaping resolver", async () => { + // Sources: + // test/built-ins/Promise/resolve-non-thenable-immed.js + // test/built-ins/Promise/resolve-non-thenable-deferred.js + // test/built-ins/Promise/create-resolving-functions-resolve.js + expect( + await value(` + let settle + const deferred = new Promise((resolve) => { settle = resolve }) + const immediate = new Promise((resolve) => resolve("now")) + settle("later") + return [await immediate, await deferred] + `), + ).toEqual(["now", "later"]) + }) + + test("rejects through reject and through an abrupt executor completion", async () => { + // Sources: + // test/built-ins/Promise/reject-via-fn-immed.js + // test/built-ins/Promise/reject-via-abrupt.js + expect( + await value(` + const observe = async (promise) => { + try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason.message ?? reason] } + } + return [ + await observe(new Promise((_, reject) => reject("nope"))), + await observe(new Promise(() => { throw new Error("boom") })), + ] + `), + ).toEqual([ + ["rejected", "nope"], + ["rejected", "boom"], + ]) + }) + + test("only the first settlement counts", async () => { + // Sources: + // test/built-ins/Promise/reject-ignored-via-fn-immed.js + // test/built-ins/Promise/resolve-ignored-via-fn-immed.js + expect( + await value(` + return [ + await new Promise((resolve, reject) => { resolve("first"); reject("second"); resolve("third") }), + await new Promise((resolve) => { resolve(resolve("inner") === undefined ? "unreached" : "also unreached") }), + ] + `), + ).toEqual(["first", "inner"]) + }) + + test("escaped resolvers keep first-settle-wins in both directions", async () => { + // Sources: + // test/built-ins/Promise/reject-ignored-via-fn-deferred.js + // test/built-ins/Promise/resolve-ignored-via-fn-deferred.js + expect( + await value(` + let resolveRejected, rejectRejected + const rejected = new Promise((resolve, reject) => { resolveRejected = resolve; rejectRejected = reject }) + rejectRejected("first") + const lateResolve = resolveRejected("late") + let resolveFulfilled, rejectFulfilled + const fulfilled = new Promise((resolve, reject) => { resolveFulfilled = resolve; rejectFulfilled = reject }) + resolveFulfilled() + const lateReject = rejectFulfilled(new Promise(() => {})) + try { + await rejected + return "fulfilled" + } catch (reason) { + return [reason, lateResolve === undefined, (await fulfilled) === undefined, lateReject === undefined] + } + `), + ).toEqual(["first", true, true, true]) + }) + + test("a queued reaction chain observes a later rejection through a handler-less then", async () => { + // Sources: + // test/built-ins/Promise/reject-via-fn-immed-queue.js + // test/built-ins/Promise/reject-via-fn-deferred-queue.js + // test/built-ins/Promise/reject-via-abrupt-queue.js + expect( + await value(` + const observe = (promise) => promise.then(() => "wrong").then(() => "also wrong", (reason) => "caught:" + reason) + let reject + const deferred = new Promise((_, r) => { reject = r }) + const chained = observe(deferred) + reject("boom") + return [ + await observe(new Promise((_, r) => r("immed"))), + await chained, + await observe(new Promise(() => { throw "abrupt" })), + ] + `), + ).toEqual(["caught:immed", "caught:boom", "caught:abrupt"]) + }) + + test("an exception after resolve is ignored", async () => { + // Source: test/built-ins/Promise/exception-after-resolve-in-executor.js + expect(await value(`return await new Promise((resolve) => { resolve("kept"); throw new Error("dropped") })`)).toBe( + "kept", + ) + }) + + test("resolving with a promise adopts its settlement", async () => { + // Sources: + // test/built-ins/Promise/resolve-thenable-immed.js (promise-adoption portion) + // test/built-ins/Promise/all/S25.4.4.1_A2.3_T1.js (resolution adoption semantics) + expect( + await value(` + const adoptedValue = await new Promise((resolve) => resolve(Promise.resolve("adopted"))) + try { + await new Promise((resolve) => resolve(Promise.reject("bad"))) + return [adoptedValue, "fulfilled"] + } catch (reason) { + return [adoptedValue, reason] + } + `), + ).toEqual(["adopted", "bad"]) + }) + + test("resolving with the promise itself rejects with TypeError", async () => { + // Source: test/built-ins/Promise/resolve-self.js + expect( + await value(` + let settle + const promise = new Promise((resolve) => { settle = resolve }) + settle(promise) + try { + await promise + return "fulfilled" + } catch (error) { + return error.name + } + `), + ).toBe("TypeError") + }) + + test("executor runs synchronously before the constructor returns", async () => { + // Source: test/built-ins/Promise/executor-call-context-strict.js (synchronous Call(executor) step) + expect( + await value(` + const sequence = [] + sequence.push("before") + new Promise((resolve) => { sequence.push("executor"); resolve() }) + sequence.push("after") + return sequence + `), + ).toEqual(["before", "executor", "after"]) + }) + + test("calling Promise without new throws TypeError", async () => { + // Source: test/built-ins/Promise/undefined-newtarget.js + expect( + await value(` + try { + Promise(() => {}) + return "called" + } catch (error) { + return error.name + } + `), + ).toBe("TypeError") + }) +}) diff --git a/packages/codemode/test/promise.test.ts b/packages/codemode/test/promise.test.ts index 545d463abf..f5b52eeefd 100644 --- a/packages/codemode/test/promise.test.ts +++ b/packages/codemode/test/promise.test.ts @@ -1,10 +1,11 @@ import { describe, expect, test } from "bun:test" -import { Effect, Schema } from "effect" +import { Deferred, Effect, Schema } from "effect" import { CodeMode, Tool, toolError } from "../src/index.js" // Wave 5 acceptance suite: first-class promise values. Un-awaited tool calls start eagerly on -// supervised fibers, `await` settles them, and Promise.all/allSettled/race/resolve/reject are -// ordinary functions over arbitrary arrays mixing promises and plain values. +// supervised fibers, `await` settles them, Promise.all/allSettled/race/resolve/reject are +// ordinary functions over arbitrary arrays mixing promises and plain values, and +// .then/.catch/.finally chain reactions onto any promise. type Trace = { starts: Array @@ -16,18 +17,41 @@ type Trace = { const makeTrace = (): Trace => ({ starts: [], active: 0, maxActive: 0, completed: 0, interrupted: 0 }) -/** Echoes `id` after `ms` milliseconds, recording start order, live concurrency, and interruption. */ -const sleepyTool = (trace: Trace) => +/** + * Deterministic tool set: ordering and interruption are structural, never temporal. + * + * - `echo` settles immediately with its id. + * - `gated` blocks until `open` releases the same id. Tool fibers start eagerly at the + * call site, so several gated calls are provably live at once before any `open` runs. + * - `pending` never settles; tests assert its interruption instead of racing a timer. + * + * Real clocks remain only in the wall-clock timeout tests (`timeoutMs`, `stubborn` + * cleanup), where elapsed time is the behavior under test. + */ +const echoTool = (trace: Trace) => Tool.make({ - description: "Echo an id after a delay", - input: Schema.Struct({ id: Schema.Number, ms: Schema.optionalKey(Schema.Number) }), + description: "Echo an id immediately", + input: Schema.Struct({ id: Schema.Number }), output: Schema.Number, - run: ({ id, ms }) => + run: ({ id }) => + Effect.sync(() => { + trace.starts.push(id) + trace.completed += 1 + return id + }), + }) + +const gatedTool = (trace: Trace, gate: (id: number) => Deferred.Deferred) => + Tool.make({ + description: "Echo an id once its gate opens", + input: Schema.Struct({ id: Schema.Number }), + output: Schema.Number, + run: ({ id }) => Effect.gen(function* () { trace.starts.push(id) trace.active += 1 trace.maxActive = Math.max(trace.maxActive, trace.active) - yield* Effect.sleep(ms ?? 20) + yield* Deferred.await(gate(id)) trace.active -= 1 trace.completed += 1 return id @@ -41,6 +65,35 @@ const sleepyTool = (trace: Trace) => ), }) +const openTool = (gate: (id: number) => Deferred.Deferred) => + Tool.make({ + description: "Open the gate for an id", + input: Schema.Struct({ id: Schema.Number }), + output: Schema.Boolean, + run: ({ id }) => Deferred.succeed(gate(id), undefined), + }) + +const pendingTool = (trace: Trace) => + Tool.make({ + description: "Never settle", + input: Schema.Struct({ id: Schema.Number }), + output: Schema.Number, + run: ({ id }) => + Effect.gen(function* () { + trace.starts.push(id) + trace.active += 1 + trace.maxActive = Math.max(trace.maxActive, trace.active) + return yield* Effect.never + }).pipe( + Effect.onInterrupt(() => + Effect.sync(() => { + trace.active -= 1 + trace.interrupted += 1 + }), + ), + ), + }) + const failingTool = Tool.make({ description: "Always refuse", input: Schema.Struct({}), @@ -48,14 +101,67 @@ const failingTool = Tool.make({ run: () => Effect.fail(toolError("Lookup refused")), }) +const interruptedTool = Tool.make({ + description: "Interrupt this call", + input: Schema.Struct({}), + output: Schema.String, + run: () => Effect.interrupt, +}) + +const completedTool = (trace: Trace) => + Tool.make({ + description: "Return the number of completed calls", + input: Schema.Struct({}), + output: Schema.Number, + run: () => Effect.succeed(trace.completed), + }) + +/** Never settles, and holds interruption cleanup for `cleanupMs` so completion cleanup can outlast a timeout. */ +const stubbornTool = (trace: Trace) => + Tool.make({ + description: "Never settle; clean up slowly when interrupted", + input: Schema.Struct({ cleanupMs: Schema.Number }), + output: Schema.Number, + run: ({ cleanupMs }) => + Effect.never.pipe( + Effect.onInterrupt(() => + Effect.andThen( + Effect.sleep(cleanupMs), + Effect.sync(() => { + trace.interrupted += 1 + }), + ), + ), + ), + }) + const run = ( code: string, options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {}, ): Promise => { const trace = options.trace ?? makeTrace() + const gates = new Map>() + const gate = (id: number): Deferred.Deferred => { + const existing = gates.get(id) + if (existing) return existing + const created = Deferred.makeUnsafe() + gates.set(id, created) + return created + } return Effect.runPromise( CodeMode.execute({ - tools: { host: { sleepy: sleepyTool(trace), fail: failingTool } }, + tools: { + host: { + echo: echoTool(trace), + gated: gatedTool(trace, gate), + open: openTool(gate), + pending: pendingTool(trace), + fail: failingTool, + interrupt: interruptedTool, + completed: completedTool(trace), + stubborn: stubbornTool(trace), + }, + }, code, ...(options.limits ? { limits: options.limits } : {}), }), @@ -75,12 +181,50 @@ const error = async (code: string, options: { trace?: Trace; limits?: CodeMode.E } describe("first-class promise values", () => { + test("async functions return promises with isolated concurrent invocations", async () => { + expect( + await value(` + const load = async (id) => { + const result = await tools.host.echo({ id }) + return [id, result] + } + const first = load(1) + const second = load(2) + return [first instanceof Promise, second instanceof Promise, await Promise.all([first, second])] + `), + ).toEqual([ + true, + true, + [ + [1, 1], + [2, 2], + ], + ]) + }) + + test("async function errors reject instead of throwing at the call site", async () => { + expect( + await value(` + const fail = async () => { throw new Error("boom") } + const promise = fail() + try { + await promise + return "no" + } catch (error) { + return error.message + } + `), + ).toBe("boom") + }) + test("an un-awaited tool call starts eagerly, in call order, before any await", async () => { const trace = makeTrace() const result = await value( ` - const a = tools.host.sleepy({ id: 1, ms: 40 }) - const b = tools.host.sleepy({ id: 2, ms: 40 }) + const a = tools.host.gated({ id: 1 }) + const b = tools.host.gated({ id: 2 }) + await tools.host.open({ id: 1 }) + await tools.host.open({ id: 2 }) const rb = await b const ra = await a return [ra, rb] @@ -95,7 +239,7 @@ describe("first-class promise values", () => { test("awaiting the same promise twice settles once and never re-runs the call", async () => { const result = await run(` - const p = tools.host.sleepy({ id: 7 }) + const p = tools.host.echo({ id: 7 }) const x = await p const y = await p return [x, y] @@ -103,10 +247,10 @@ describe("first-class promise values", () => { expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toEqual([7, 7]) - expect(result.toolCalls).toStrictEqual([{ name: "host.sleepy" }]) + expect(result.toolCalls).toStrictEqual([{ name: "host.echo" }]) }) - test("await of a non-promise value is a passthrough no-op", async () => { + test("await of a non-promise value passes it through unchanged", async () => { expect(await value(`return await 42`)).toBe(42) expect(await value(`const x = await "s"; return x`)).toBe("s") expect(await value(`return await null`)).toBeNull() @@ -114,7 +258,7 @@ describe("first-class promise values", () => { }) test("returning an un-awaited tool call resolves it (async-function return semantics)", async () => { - expect(await value(`return tools.host.sleepy({ id: 9 })`)).toBe(9) + expect(await value(`return tools.host.echo({ id: 9 })`)).toBe(9) }) test("typeof a promise is 'object', and console.log renders it sensibly", async () => { @@ -130,8 +274,7 @@ describe("first-class promise values", () => { }) test("an awaited failure is catchable exactly like a synchronous throw", async () => { - expect( - await value(` + const result = await run(` const p = tools.host.fail({}) try { await p @@ -139,46 +282,230 @@ describe("first-class promise values", () => { } catch (e) { return e.message } - `), - ).toBe("Lookup refused") + `) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("Lookup refused") + expect(result.warnings).toBeUndefined() }) - test("a fire-and-forget call completes before the execution ends", async () => { + test("a fire-and-forget call is interrupted when the program returns", async () => { const trace = makeTrace() - const result = await value( + const result = await run( ` - tools.host.sleepy({ id: 1, ms: 30 }) + tools.host.pending({ id: 1 }) return "done" `, { trace }, ) - expect(result).toBe("done") - expect(trace.completed).toBe(1) - expect(trace.interrupted).toBe(0) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("done") + expect(result.warnings).toBeUndefined() + expect(trace.completed).toBe(0) + expect(trace.interrupted).toBe(1) }) - test("a never-awaited failing call surfaces as an unhandled-rejection diagnostic", async () => { - const diagnostic = await error(` + test("a never-awaited failing call preserves the result and reports the rejection", async () => { + const result = await run(` tools.host.fail({}) return "done" `) - expect(diagnostic.kind).toBe("ToolFailure") - expect(diagnostic.message).toContain("Unhandled rejection from an un-awaited tool call") - expect(diagnostic.message).toContain("Lookup refused") - expect(diagnostic.suggestions?.join(" ")).toContain("await tools.ns.tool(...)") + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("done") + expect(result.warnings).toStrictEqual([ + { kind: "ToolFailure", message: "Unhandled rejection from an un-awaited promise: Lookup refused" }, + ]) + expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result) + }) + + test("a never-awaited failing async function is reported with a successful result", async () => { + const result = await run(` + const fail = async () => { throw new Error("boom") } + fail() + return "done" + `) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("done") + expect(result.warnings).toStrictEqual([ + { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: boom" }, + ]) + }) + + test("output truncation bounds warning diagnostics with an in-band marker", async () => { + const result = await run( + ` + for (let i = 0; i < 100; i += 1) Promise.reject(new Error("x".repeat(1_000))) + return "done" + `, + { limits: { maxOutputBytes: 64 } }, + ) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.truncated).toBe(true) + expect(result.warnings).toStrictEqual([ + { kind: "Truncated", message: "100 additional warnings omitted by the output limit." }, + ]) + }) + + test("a budget-consuming value does not starve warnings", async () => { + const result = await run( + ` + Promise.reject(new Error("boom")) + return "x".repeat(500) + `, + { limits: { maxOutputBytes: 128 } }, + ) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.truncated).toBe(true) + expect(typeof result.value).toBe("string") + expect(result.warnings).toStrictEqual([ + { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: boom" }, + ]) + }) + + test("an un-awaited async function's pending chain is interrupted at the return", async () => { + const trace = makeTrace() + const result = await run( + ` + const run = async () => { + await tools.host.pending({ id: 1 }) + tools.host.fail({}) + } + run() + return "done" + `, + { trace }, + ) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("done") + expect(result.warnings).toBeUndefined() + expect(trace.starts).toEqual([1]) + expect(trace.completed).toBe(0) + expect(trace.interrupted).toBe(1) + }) + + test("reports every unhandled rejection in promise creation order", async () => { + const result = await run(` + Promise.reject(new Error("first")) + tools.host.fail({}) + Promise.reject(new Error("third")) + return "done" + `) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.warnings).toStrictEqual([ + { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: first" }, + { kind: "ToolFailure", message: "Unhandled rejection from an un-awaited promise: Lookup refused" }, + { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: third" }, + ]) + }) + + test("orders an async function rejection before promises created inside its body", async () => { + const result = await run(` + const outer = async () => { + Promise.reject(new Error("inner")) + throw new Error("outer") + } + outer() + return "done" + `) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.warnings).toStrictEqual([ + { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: outer" }, + { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: inner" }, + ]) + }) + + test("un-awaited interruptions settle without becoming rejections", async () => { + const result = await run(` + tools.host.interrupt({}) + Promise.all([tools.host.interrupt({})]) + return "done" + `) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("done") + expect(result.warnings).toBeUndefined() + }) + + test("a fatal program error cancels outstanding work without reporting unhandled rejections", async () => { + const trace = makeTrace() + const result = await run( + ` + tools.host.pending({ id: 1 }) + throw new Error("boom") + `, + { trace }, + ) + expect(result.ok).toBe(false) + if (result.ok) return + expect(result.error.message).toBe("Uncaught: boom") + expect("warnings" in result).toBe(false) + expect(trace.completed).toBe(0) + expect(trace.interrupted).toBe(1) + }) + + test("async-function promises remain owned by the execution after the function returns", async () => { + const trace = makeTrace() + expect( + await value( + ` + const launch = async () => { + tools.host.pending({ id: 1 }) + Promise.all([tools.host.pending({ id: 2 })]) + return "returned" + } + return await launch() + `, + { trace }, + ), + ).toBe("returned") + // Both calls outlive launch() itself - they belong to the execution, not the function - + // and are interrupted only when the whole program returns. + expect(trace.starts).toEqual([1, 2]) + expect(trace.completed).toBe(0) + expect(trace.interrupted).toBe(2) }) }) describe("promises at data boundaries", () => { test("returning an un-awaited promise inside data is a clear await-hinting diagnostic", async () => { - const diagnostic = await error(`return { result: tools.host.sleepy({ id: 1 }) }`) + const diagnostic = await error(`return { result: tools.host.echo({ id: 1 }) }`) expect(diagnostic.kind).toBe("InvalidDataValue") expect(diagnostic.message).toContain("un-awaited Promise") expect(diagnostic.message).toContain("await tools.ns.tool(...)") }) + test("collection helpers do not let un-awaited promises cross the result boundary", async () => { + const diagnostic = await error(`return Array.from([Promise.resolve(1)])`) + expect(diagnostic.kind).toBe("InvalidDataValue") + expect(diagnostic.message).toContain("un-awaited Promise") + }) + + test("invalid returned data cancels pending work", async () => { + const trace = makeTrace() + const result = await run( + ` + const pending = tools.host.pending({ id: 1 }) + return { pending } + `, + { trace, limits: { timeoutMs: 100 } }, + ) + expect(result.ok).toBe(false) + if (result.ok) return + expect(result.error.kind).toBe("InvalidDataValue") + expect(trace.completed).toBe(0) + expect(trace.interrupted).toBe(1) + }) + test("passing an un-awaited promise as a tool argument is a clear diagnostic", async () => { - const diagnostic = await error(`return await tools.host.sleepy({ id: tools.host.sleepy({ id: 1 }) })`) + const diagnostic = await error(`return await tools.host.echo({ id: tools.host.echo({ id: 1 }) })`) expect(diagnostic.kind).toBe("InvalidDataValue") expect(diagnostic.message).toContain("un-awaited Promise") }) @@ -196,10 +523,65 @@ describe("promises at data boundaries", () => { }) describe("Promise.all over arbitrary arrays", () => { + test("combinators return promises that can be assigned and awaited later", async () => { + expect( + await value(` + const all = Promise.all([Promise.resolve(1)]) + const settled = Promise.allSettled([Promise.reject("no")]) + const race = Promise.race([Promise.resolve(2)]) + const promises = [all instanceof Promise, settled instanceof Promise, race instanceof Promise] + return [promises, await all, await settled, await race] + `), + ).toEqual([[true, true, true], [1], [{ status: "rejected", reason: "no" }], 2]) + }) + + test("separately-created aggregate batches overlap before either is awaited", async () => { + const trace = makeTrace() + expect( + await value( + ` + const first = Promise.all([tools.host.gated({ id: 1 })]) + const second = Promise.all([tools.host.gated({ id: 2 })]) + await tools.host.open({ id: 1 }) + await tools.host.open({ id: 2 }) + return [await first, await second] + `, + { trace }, + ), + ).toEqual([[1], [2]]) + expect(trace.starts).toEqual([1, 2]) + expect(trace.maxActive).toBeGreaterThan(1) + }) + + test("an aggregate created before a try block rejects at its later await", async () => { + expect( + await value(` + const aggregate = Promise.all([tools.host.fail({})]) + try { + await aggregate + return "no" + } catch (error) { + return error.message + } + `), + ).toBe("Lookup refused") + }) + + test("awaiting an aggregate repeatedly does not rerun its members", async () => { + const result = await run(` + const aggregate = Promise.all([tools.host.echo({ id: 7 })]) + return [await aggregate, await aggregate] + `) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toEqual([[7], [7]]) + expect(result.toolCalls).toStrictEqual([{ name: "host.echo" }]) + }) + test("mixes promises and plain values, preserving order", async () => { expect( await value(` - return await Promise.all([tools.host.sleepy({ id: 1 }), "plain", tools.host.sleepy({ id: 2 }), 42]) + return await Promise.all([tools.host.echo({ id: 1 }), "plain", tools.host.echo({ id: 2 }), 42]) `), ).toEqual([1, "plain", 2, 42]) }) @@ -208,9 +590,9 @@ describe("Promise.all over arbitrary arrays", () => { expect( await value(` const calls = [] - calls.push(tools.host.sleepy({ id: 1 })) + calls.push(tools.host.echo({ id: 1 })) calls.push(7) - const more = [tools.host.sleepy({ id: 2 })] + const more = [tools.host.echo({ id: 2 })] const batch = [...calls, ...more, "x"] return await Promise.all(batch) `), @@ -222,7 +604,9 @@ describe("Promise.all over arbitrary arrays", () => { const result = await value( ` const ids = [1, 2, 3, 4] - return await Promise.all(ids.map((id) => tools.host.sleepy({ id, ms: 40 }))) + const calls = ids.map((id) => tools.host.gated({ id })) + for (const id of ids) await tools.host.open({ id }) + return await Promise.all(calls) `, { trace }, ) @@ -232,20 +616,36 @@ describe("Promise.all over arbitrary arrays", () => { expect(trace.maxActive).toBeGreaterThan(1) }) - test("caps live tool-call concurrency at the fixed internal constant (8)", async () => { + test("runs async map callbacks concurrently", async () => { + const trace = makeTrace() + const result = await value( + ` + const ids = [1, 2, 3, 4] + const calls = ids.map(async (id) => await tools.host.gated({ id })) + for (const id of ids) await tools.host.open({ id }) + return await Promise.all(calls) + `, + { trace }, + ) + expect(result).toEqual([1, 2, 3, 4]) + expect(trace.maxActive).toBeGreaterThan(1) + }) + + test("does not cap live tool-call concurrency", async () => { const trace = makeTrace() const result = await value( ` const ids = [] for (let i = 0; i < 20; i += 1) ids.push(i) - const results = await Promise.all(ids.map((id) => tools.host.sleepy({ id, ms: 10 }))) + const calls = ids.map((id) => tools.host.gated({ id })) + for (const id of ids) await tools.host.open({ id }) + const results = await Promise.all(calls) return results.length `, { trace }, ) expect(result).toBe(20) - expect(trace.maxActive).toBeGreaterThan(1) - expect(trace.maxActive).toBeLessThanOrEqual(8) + expect(trace.maxActive).toBe(20) }) test("resolves the empty array", async () => { @@ -253,16 +653,86 @@ describe("Promise.all over arbitrary arrays", () => { }) test("rejects with the first failure, catchable in-program", async () => { - expect( - await value(` + const result = await run(` try { - await Promise.all([tools.host.sleepy({ id: 1 }), tools.host.fail({})]) + await Promise.all([tools.host.echo({ id: 1 }), tools.host.fail({})]) return "no" } catch (e) { return e.message } - `), - ).toBe("Lookup refused") + `) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("Lookup refused") + expect(result.warnings).toBeUndefined() + }) + + test("rejects before an earlier slow promise fulfills", async () => { + const trace = makeTrace() + expect( + await value( + ` + try { + await Promise.all([ + tools.host.pending({ id: 1 }), + tools.host.fail({}), + ]) + return -1 + } catch { + return await tools.host.completed({}) + } + `, + { trace }, + ), + ).toBe(0) + // The surviving member is observed (Promise.all handled it), so completion interrupts + // it instead of waiting for it. + expect(trace.completed).toBe(0) + expect(trace.interrupted).toBe(1) + }) + + test("fail-fast does not cancel a sibling the program still holds and awaits", async () => { + const trace = makeTrace() + expect( + await value( + ` + const slow = tools.host.gated({ id: 1 }) + try { + await Promise.all([slow, tools.host.fail({})]) + return "no" + } catch {} + await tools.host.open({ id: 1 }) + return await slow + `, + { trace }, + ), + ).toBe(1) + expect(trace.completed).toBe(1) + expect(trace.interrupted).toBe(0) + }) + + test("a slower observed sibling is interrupted at completion after failing fast", async () => { + const trace = makeTrace() + expect( + await value( + ` + const failLater = async () => { + await tools.host.pending({ id: 1 }) + throw new Error("later") + } + const aggregate = Promise.all([Promise.reject(new Error("first")), failLater()]) + try { + await aggregate + return "no" + } catch (error) { + return error.message + } + `, + { trace }, + ), + ).toBe("first") + expect(trace.completed).toBe(0) + expect(trace.interrupted).toBe(1) }) test("a non-collection argument is a clear error", async () => { @@ -272,7 +742,7 @@ describe("Promise.all over arbitrary arrays", () => { test("exceeding maxToolCalls inside Promise.all is a ToolCallLimitExceeded diagnostic", async () => { const diagnostic = await error( - `return await Promise.all([tools.host.sleepy({ id: 1 }), tools.host.sleepy({ id: 2 }), tools.host.sleepy({ id: 3 })])`, + `return await Promise.all([tools.host.echo({ id: 1 }), tools.host.echo({ id: 2 }), tools.host.echo({ id: 3 })])`, { limits: { maxToolCalls: 2 } }, ) expect(diagnostic.kind).toBe("ToolCallLimitExceeded") @@ -284,7 +754,7 @@ describe("Promise.allSettled", () => { expect( await value(` return await Promise.allSettled([ - tools.host.sleepy({ id: 5 }), + tools.host.echo({ id: 5 }), tools.host.fail({}), "plain", Promise.reject(new Error("boom")), @@ -304,50 +774,65 @@ describe("Promise.allSettled", () => { return settled.filter((s) => s.status === "rejected").length `) expect(result.ok).toBe(true) - if (result.ok) expect(result.value).toBe(2) + if (!result.ok) return + expect(result.value).toBe(2) + expect(result.warnings).toBeUndefined() }) }) describe("Promise.race", () => { - test("first settlement wins and losers are interrupted", async () => { + test("first settlement wins and a direct loser is interrupted at completion", async () => { const trace = makeTrace() const result = await value( ` - const fast = tools.host.sleepy({ id: 1, ms: 10 }) - const slow = tools.host.sleepy({ id: 2, ms: 5000 }) + const fast = tools.host.echo({ id: 1 }) + const slow = tools.host.pending({ id: 2 }) return await Promise.race([fast, slow]) `, { trace }, ) expect(result).toBe(1) - expect(trace.interrupted).toBe(1) + // The loser is observed (the race handled it), so the execution does not wait for it. expect(trace.completed).toBe(1) + expect(trace.interrupted).toBe(1) }) - test("awaiting an interrupted loser afterwards is a catchable program failure", async () => { + test("a direct loser remains awaitable after the race settles", async () => { expect( await value(` - const fast = tools.host.sleepy({ id: 1, ms: 10 }) - const slow = tools.host.sleepy({ id: 2, ms: 5000 }) + const fast = tools.host.echo({ id: 1 }) + const slow = tools.host.gated({ id: 2 }) const winner = await Promise.race([fast, slow]) - try { - await slow - return "no" - } catch (e) { - return { winner, caught: e.message } - } + await tools.host.open({ id: 2 }) + return { winner, loser: await slow } `), - ).toEqual({ - winner: 1, - caught: "This tool call was interrupted because another value settled a Promise.race first.", - }) + ).toEqual({ winner: 1, loser: 2 }) + }) + + test("a nested aggregate loser and its members are interrupted at completion", async () => { + const trace = makeTrace() + expect( + await value( + ` + const nested = Promise.all([ + tools.host.pending({ id: 1 }), + tools.host.pending({ id: 2 }), + ]) + return await Promise.race(["immediate", nested]) + `, + { trace }, + ), + ).toBe("immediate") + // The nested aggregate and its members are all observed, so nothing waits for them. + expect(trace.completed).toBe(0) + expect(trace.interrupted).toBe(2) }) test("a rejection can win the race", async () => { expect( await value(` try { - await Promise.race([tools.host.fail({}), tools.host.sleepy({ id: 1, ms: 5000 })]) + await Promise.race([tools.host.fail({}), tools.host.pending({ id: 1 })]) return "no" } catch (e) { return e.message @@ -358,12 +843,21 @@ describe("Promise.race", () => { test("a plain value wins over pending promises", async () => { const trace = makeTrace() - expect( - await value(`return await Promise.race([tools.host.sleepy({ id: 1, ms: 5000 }), "immediate"])`, { trace }), - ).toBe("immediate") + expect(await value(`return await Promise.race([tools.host.pending({ id: 1 }), "immediate"])`, { trace })).toBe( + "immediate", + ) + expect(trace.completed).toBe(0) expect(trace.interrupted).toBe(1) }) + test("a rejected race loser is observed by the aggregate", async () => { + const result = await run(`return await Promise.race(["winner", tools.host.fail({})])`) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("winner") + expect(result.warnings).toBeUndefined() + }) + test("an empty race is a clear error instead of hanging", async () => { const diagnostic = await error(`return await Promise.race([])`) expect(diagnostic.message).toContain("never settle") @@ -374,7 +868,10 @@ describe("Promise.resolve / Promise.reject", () => { test("resolve wraps plain values and passes promises through", async () => { expect(await value(`return await Promise.resolve(42)`)).toBe(42) expect(await value(`return await Promise.resolve(Promise.resolve("nested"))`)).toBe("nested") - expect(await value(`return await Promise.resolve(tools.host.sleepy({ id: 3 }))`)).toBe(3) + expect(await value(`return await Promise.resolve(tools.host.echo({ id: 3 }))`)).toBe(3) + expect(await value(`const promise = Promise.resolve(1); return [promise].includes(Promise.resolve(promise))`)).toBe( + true, + ) }) test("reject produces a promise whose await throws the reason", async () => { @@ -389,6 +886,34 @@ describe("Promise.resolve / Promise.reject", () => { `), ).toBe("nope") }) + + test("a rejection observed after settlement is handled", async () => { + expect( + await value(` + const rejected = Promise.reject(new Error("handled")) + await tools.host.echo({ id: 1 }) + try { + await rejected + return "no" + } catch (error) { + return error.message + } + `), + ).toBe("handled") + }) + + test("an abandoned rejected promise is reported as unhandled", async () => { + const result = await run(` + Promise.reject(new Error("abandoned")) + return "done" + `) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("done") + expect(result.warnings).toStrictEqual([ + { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: abandoned" }, + ]) + }) }) describe("timeout interruption of forked calls", () => { @@ -396,8 +921,8 @@ describe("timeout interruption of forked calls", () => { const trace = makeTrace() const result = await run( ` - const a = tools.host.sleepy({ id: 1, ms: 60000 }) - const b = tools.host.sleepy({ id: 2, ms: 60000 }) + const a = tools.host.pending({ id: 1 }) + const b = tools.host.pending({ id: 2 }) return await a `, { trace, limits: { timeoutMs: 100 } }, @@ -414,7 +939,7 @@ describe("timeout interruption of forked calls", () => { test("the timeout also interrupts calls inside Promise.all", async () => { const trace = makeTrace() const result = await run( - `return await Promise.all([tools.host.sleepy({ id: 1, ms: 60000 }), tools.host.sleepy({ id: 2, ms: 60000 })])`, + `return await Promise.all([tools.host.pending({ id: 1 }), tools.host.pending({ id: 2 })])`, { trace, limits: { timeoutMs: 100 } }, ) expect(result.ok).toBe(false) @@ -422,35 +947,369 @@ describe("timeout interruption of forked calls", () => { expect(result.error.kind).toBe("TimeoutExceeded") expect(trace.interrupted).toBe(2) }) + + test("a non-settling race loser cannot hold the execution to the timeout", async () => { + const trace = makeTrace() + const result = await run(`return await Promise.race(["winner", tools.host.pending({ id: 1 })])`, { + trace, + limits: { timeoutMs: 100 }, + }) + // Completion interrupts the observed loser immediately; the race result survives. + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("winner") + expect(result.warnings).toBeUndefined() + expect(trace.starts).toEqual([1]) + expect(trace.completed).toBe(0) + expect(trace.interrupted).toBe(1) + }) + + test("a timeout during completion cleanup keeps the computed value and warns", async () => { + const trace = makeTrace() + const result = await run( + ` + tools.host.stubborn({ cleanupMs: 400 }) + return "done" + `, + { trace, limits: { timeoutMs: 100 } }, + ) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("done") + expect(result.warnings).toStrictEqual([ + { + kind: "TimeoutExceeded", + message: + "The program returned, but background work was still running at the 100ms timeout and was interrupted. Await all started promises.", + }, + ]) + expect(trace.interrupted).toBe(1) + expect(trace.completed).toBe(0) + }) + + test("a timeout during completion cleanup reports the timeout warning before settled rejections", async () => { + const result = await run( + ` + tools.host.fail({}) + tools.host.stubborn({ cleanupMs: 400 }) + return "done" + `, + { limits: { timeoutMs: 100 } }, + ) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("done") + expect(result.warnings).toStrictEqual([ + { + kind: "TimeoutExceeded", + message: + "The program returned, but background work was still running at the 100ms timeout and was interrupted. Await all started promises.", + }, + { kind: "ToolFailure", message: "Unhandled rejection from an un-awaited promise: Lookup refused" }, + ]) + }) +}) + +describe("promise chaining", () => { + test("then transforms tool results and adopts returned promises across a chain", async () => { + expect( + await value(` + return await tools.host + .echo({ id: 2 }) + .then((id) => tools.host.echo({ id: id + 1 })) + .then((id) => id * 10) + `), + ).toBe(30) + }) + + test("handlers are deferred and run in attach order", async () => { + expect( + await value(` + const order = [] + const promise = Promise.resolve(1) + promise.then(() => order.push("h1")) + promise.then(() => order.push("h2")) + order.push("sync") + await promise + return order + `), + ).toEqual(["sync", "h1", "h2"]) + }) + + test("catch recovers a tool failure and preserves fulfillment", async () => { + expect( + await value(` + return [ + await tools.host.fail({}).catch((error) => error.message), + await tools.host.echo({ id: 4 }).catch(() => "unused"), + ] + `), + ).toEqual(["Lookup refused", 4]) + }) + + test("finally observes settlement without changing the value", async () => { + expect( + await value(` + const events = [] + const result = await tools.host.echo({ id: 5 }).finally(() => events.push("cleanup")) + return [result, events] + `), + ).toEqual([5, ["cleanup"]]) + }) + + test("a settled, un-awaited rejected chain tail warns exactly once", async () => { + const result = await run(` + Promise.reject(new Error("boom")).then((value) => value) + await Promise.resolve() + return "done" + `) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("done") + // The source rejection belongs to the chain (no warning); only the derived tail warns. + expect(result.warnings).toStrictEqual([ + { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: boom" }, + ]) + }) + + test("a catch handler silences the chain's rejection warning", async () => { + const result = await run(` + Promise.reject(new Error("boom")).catch(() => "handled") + await Promise.resolve() + return "done" + `) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.warnings).toBeUndefined() + }) + + test("unsupported callable handlers fail loudly with a wrap hint", async () => { + const diagnostic = await error(`return await tools.host.echo({ id: 1 }).then(tools.host.completed)`) + expect(diagnostic.message).toContain("Promise.prototype.then cannot use this callable as a handler") + expect(diagnostic.message).toContain("wrap it in an arrow function") + }) + + test("chaining methods are opaque references until called", async () => { + expect(await value(`return typeof tools.host.echo({ id: 1 }).then`)).toBe("function") + }) +}) + +describe("combinator settlement timing", () => { + test("a combinator settling one reaction turn after the program returns is interrupted silently", async () => { + // The aggregate's one-turn settlement delay (V8 parity) means an immediately-returning + // program abandons it while still pending: interrupted like any pending work, so no + // rejection warning survives - the member itself was observed by the combinator. + const result = await run(` + Promise.all([Promise.reject(new Error("boom"))]) + return "done" + `) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("done") + expect(result.warnings).toBeUndefined() + }) + + test("a combinator settles one reaction turn after its members, as in V8", async () => { + // Regression for the race winner flip: Promise.all's settlement burns a reaction turn, + // so a plain resolved value entered in the same race wins, and a fail-fast aggregate + // cannot beat it into rejection. + expect( + await value(` + const pending = tools.host.pending({ id: 9 }) + const winner = await Promise.race([Promise.all([Promise.resolve(1)]), Promise.resolve(2)]) + try { + const raced = await Promise.race([Promise.all([Promise.reject("x"), pending]), Promise.resolve("ok")]) + return [winner, "fulfilled", raced] + } catch (reason) { + return [winner, "rejected", reason] + } + `), + ).toEqual([2, "fulfilled", "ok"]) + }) }) describe("unsupported promise surface", () => { - test(".then/.catch/.finally give a clear await-instead error", async () => { - for (const method of ["then", "catch", "finally"]) { - const diagnostic = await error(`return tools.host.sleepy({ id: 1 }).${method}((x) => x)`) - expect(diagnostic.kind).toBe("UnsupportedSyntax") - expect(diagnostic.message).toContain(`Promise.prototype.${method} is not supported`) - expect(diagnostic.message).toContain("await") - } - }) - test("other property reads on a promise hint at the missing await", async () => { - const diagnostic = await error(`return tools.host.sleepy({ id: 1 }).value`) + const diagnostic = await error(`return tools.host.echo({ id: 1 }).value`) expect(diagnostic.kind).toBe("InvalidDataValue") expect(diagnostic.message).toContain("un-awaited Promise") expect(diagnostic.message).toContain("await it first") }) test("unknown Promise statics list what is available", async () => { - const diagnostic = await error(`return await Promise.any([tools.host.sleepy({ id: 1 })])`) - expect(diagnostic.message).toContain("Promise.any is not available") - expect(diagnostic.message).toContain("Promise.allSettled") - }) - - test("new Promise(...) points at tool calls instead", async () => { - const diagnostic = await error(`return new Promise((resolve) => resolve(1))`) - expect(diagnostic.kind).toBe("UnsupportedSyntax") - expect(diagnostic.message).toContain("new Promise(...) is not supported") - expect(diagnostic.message).toContain("already return promises") + const diagnostic = await error(`return await Promise.withResolvers()`) + expect(diagnostic.message).toContain("Promise.withResolvers is not available") + expect(diagnostic.message).toContain("Promise.any") + }) +}) + +describe("Promise.any", () => { + test("first tool success wins; failing and losing calls are handled silently", async () => { + const trace = makeTrace() + const result = await run( + ` + const winner = await Promise.any([ + tools.host.fail({}), + tools.host.echo({ id: 1 }), + tools.host.pending({ id: 2 }), + ]) + return winner + `, + { trace }, + ) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe(1) + // The slow loser stays execution-owned and is interrupted at completion; the tool + // failure was observed by the aggregate, so no rejection warning survives. + expect(result.warnings).toBeUndefined() + expect(trace.interrupted).toBe(1) + }) + + test("all members failing rejects with catch-normalized reasons in input order", async () => { + expect( + await value(` + try { + await Promise.any([tools.host.fail({}), Promise.reject("plain")]) + return "fulfilled" + } catch (error) { + return [error.name, error.errors.map((reason) => reason.message ?? reason)] + } + `), + ).toEqual(["AggregateError", ["Lookup refused", "plain"]]) + }) + + test("settles one reaction turn after its deciding member, as in V8", async () => { + expect(await value(`return await Promise.race([Promise.any([Promise.resolve(1)]), Promise.resolve(2)])`)).toBe(2) + }) + + test("a tie is decided by settlement order, not input order", async () => { + // Handlers run in attach order, so `first` settles before `second` and wins + // despite its later input position - as in real JS. + expect( + await value(` + const first = Promise.resolve().then(() => "one") + const second = Promise.resolve().then(() => "two") + return await Promise.any([second, first]) + `), + ).toBe("one") + }) + + test("an abandoned rejecting aggregate is interrupted silently at the return", async () => { + const result = await run(` + Promise.any([Promise.reject(new Error("boom"))]) + return "done" + `) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("done") + expect(result.warnings).toBeUndefined() + }) +}) + +describe("promise construction", () => { + test("a deferred gate coordinates tool results across async functions", async () => { + expect( + await value(` + let openGate + const gate = new Promise((resolve) => { openGate = resolve }) + const worker = (async () => { + const id = await gate + return id * 2 + })() + openGate(await tools.host.echo({ id: 21 })) + return await worker + `), + ).toBe(42) + }) + + test("the .then(resolve) bridge settles a constructed promise", async () => { + expect( + await value(` + const bridged = new Promise((resolve, reject) => { + tools.host.echo({ id: 7 }).then(resolve, reject) + }) + return await bridged + `), + ).toBe(7) + }) + + test("constructed promises participate in combinators", async () => { + expect( + await value(` + let settle + const manual = new Promise((resolve) => { settle = resolve }) + const race = Promise.race([manual, tools.host.pending({ id: 3 })]) + const all = Promise.all([manual, "plain"]) + const any = Promise.any([manual, new Promise(() => {})]) + settle("manual") + return [await race, await all, await any] + `), + ).toEqual(["manual", ["manual", "plain"], "manual"]) + }) + + test("resolving with a pending promise adopts its later settlement", async () => { + expect( + await value(` + let innerResolve, innerReject + const adopted = new Promise((resolve) => resolve(new Promise((resolve) => { innerResolve = resolve }))) + const adoptedRejection = new Promise((resolve) => resolve(new Promise((_, reject) => { innerReject = reject }))) + innerResolve("later") + innerReject("bad") + try { + return [await adopted, await adoptedRejection] + } catch (reason) { + return [await adopted, reason] + } + `), + ).toEqual(["later", "bad"]) + }) + + test("an async executor's post-await resolve settles the promise", async () => { + expect( + await value(` + const result = new Promise(async (resolve) => { + const id = await tools.host.echo({ id: 5 }) + resolve(id * 2) + }) + return await result + `), + ).toBe(10) + }) + + test("a never-settled promise is abandoned silently at the return", async () => { + const result = await run(` + const forever = new Promise(() => {}) + forever.then(() => {}) + return "done" + `) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("done") + expect(result.warnings).toBeUndefined() + }) + + test("an un-awaited constructed rejection is reported like any unhandled rejection", async () => { + const result = await run(` + new Promise((_, reject) => reject(new Error("dropped"))) + await Promise.resolve() + await Promise.resolve() + return "done" + `) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("done") + expect(result.warnings).toHaveLength(1) + expect(result.warnings?.[0].message).toContain("Unhandled rejection") + expect(result.warnings?.[0].message).toContain("dropped") + }) + + test("resolver functions cannot cross the data boundary", async () => { + const diagnostic = await error(` + let escaped + new Promise((resolve) => { escaped = resolve }) + return { escaped } + `) + expect(diagnostic.kind).toBe("InvalidDataValue") }) }) diff --git a/packages/codemode/test/signature.test.ts b/packages/codemode/test/signature.test.ts index 232c6dcb22..dea0e890ac 100644 --- a/packages/codemode/test/signature.test.ts +++ b/packages/codemode/test/signature.test.ts @@ -342,9 +342,7 @@ describe("JSDoc signatures in catalogs and search results", () => { const runtime = CodeMode.make({ tools: { github: { list_issues: listIssues }, orders: { lookup: lookupOrder } } }) const search = async (query: string) => { - const result = await Effect.runPromise( - runtime.execute(`return await tools.$codemode.search({ query: ${JSON.stringify(query)} })`), - ) + const result = await Effect.runPromise(runtime.execute(`return search({ query: ${JSON.stringify(query)} })`)) expect(result.ok).toBe(true) if (!result.ok) throw new Error("search failed") return result.value as { items: Array<{ path: string; signature: string }>; remaining: number } @@ -436,9 +434,7 @@ describe("non-identifier tool paths", () => { }) test("search results return callable bracket-notation paths and signatures", async () => { - const result = await Effect.runPromise( - runtime.execute(`return await tools.$codemode.search({ query: "resolve library" })`), - ) + const result = await Effect.runPromise(runtime.execute(`return search({ query: "resolve library" })`)) expect(result.ok).toBe(true) if (!result.ok) throw new Error("search failed") diff --git a/packages/codemode/test/stdlib.test.ts b/packages/codemode/test/stdlib.test.ts index f7831a0603..94d8c57b07 100644 --- a/packages/codemode/test/stdlib.test.ts +++ b/packages/codemode/test/stdlib.test.ts @@ -3,7 +3,7 @@ import { Effect, Schema } from "effect" import { CodeMode, Tool } from "../src/index.js" // Standard-library value types: Date, RegExp, Map, Set. Programs use them as ordinary JS; -// intra-sandbox checkpoints (Object.* helpers, spread, coercion inputs) preserve the live +// intra-CodeMode checkpoints (Object.* helpers, spread, coercion inputs) preserve the live // values, while at the host boundary (final result, tool arguments, JSON.stringify) they // serialize exactly as JSON.stringify would: Date -> ISO string (invalid -> null), // URL -> href, and RegExp/Map/Set/URLSearchParams -> {}. @@ -19,6 +19,28 @@ const error = async (code: string) => { return result.error } +describe("Number and Math", () => { + test("Math.random returns a number in [0, 1)", async () => { + expect(await value(`const n = Math.random(); return typeof n === "number" && n >= 0 && n < 1`)).toBe(true) + }) + + test("Number exposes native non-finite constants", async () => { + expect( + await value( + `return [Number.isNaN(Number.NaN), Number.POSITIVE_INFINITY === Infinity, Number.NEGATIVE_INFINITY === -Infinity]`, + ), + ).toEqual([true, true, true]) + }) + + test("Number valueOf returns its primitive receiver", async () => { + expect(await value(`return (42).valueOf()`)).toBe(42) + }) + + test("Number valueOf does not enable boxed numbers", async () => { + expect((await error(`return new Number(42)`)).kind).toBe("UnsupportedSyntax") + }) +}) + describe("Date", () => { test("Date.now() returns a number", async () => { expect(await value(`return typeof Date.now()`)).toBe("number") @@ -47,7 +69,7 @@ describe("Date", () => { ).toEqual([2024, 2, 5, 6, 7, 8, 9]) }) - test("invalid dates yield NaN times, guardable in-sandbox", async () => { + test("invalid dates yield NaN times, guardable in-CodeMode", async () => { expect(await value(`return Number.isNaN(new Date("garbage").getTime())`)).toBe(true) expect(await value(`return new Date("garbage").toJSON()`)).toBeNull() }) @@ -132,9 +154,7 @@ describe("RegExp", () => { ).toEqual(["1", "22"]) }) - test("string match: non-global carries index, global lists all matches", async () => { - expect(await value(`const m = "a1b22".match(/\\d+/); return [m[0], m.index]`)).toEqual(["1", 1]) - expect(await value(`return "a1b22".match(/\\d+/g)`)).toEqual(["1", "22"]) + test("an unmatched string pattern returns null", async () => { expect(await value(`return "abc".match(/\\d/)`)).toBeNull() }) @@ -142,13 +162,6 @@ describe("RegExp", () => { expect(await value(`return "a1b22".matchAll(/(\\d+)/g).map((m) => m[1])`)).toEqual(["1", "22"]) }) - test("replace and replaceAll with patterns and $1 substitution", async () => { - expect(await value(`return "a1b2".replace(/\\d/, "#")`)).toBe("a#b2") - expect(await value(`return "a1b2".replace(/\\d/g, "#")`)).toBe("a#b#") - expect(await value(`return "a1b2".replaceAll(/\\d/g, "#")`)).toBe("a#b#") - expect(await value(`return "hi bob".replace(/b(o)b/, "[$1]")`)).toBe("hi [o]") - }) - test("function replacers receive captures, offsets, input, and named groups", async () => { expect( await value(` @@ -214,12 +227,6 @@ describe("RegExp", () => { expect(await value(`try { "a".replaceAll(/a/, "b"); return "no" } catch { return "caught" }`)).toBe("caught") }) - test("split and search accept patterns", async () => { - expect(await value(`return "a1b22c".split(/\\d+/)`)).toEqual(["a", "b", "c"]) - expect(await value(`return "ab42".search(/\\d/)`)).toBe(2) - expect(await value(`return "ab".search(/\\d/)`)).toBe(-1) - }) - test("new RegExp constructs from strings; invalid patterns are catchable", async () => { expect(await value(`return new RegExp("a+", "i").test("AAA")`)).toBe(true) expect(await value(`try { new RegExp("("); return "no" } catch { return "caught" }`)).toBe("caught") @@ -586,6 +593,121 @@ describe("Set", () => { }) describe("stdlib integration", () => { + test("Object.is uses SameValue semantics", async () => { + expect( + await value(` + const object = {} + return [ + Object.is(NaN, NaN), + Object.is(0, -0), + Object.is(object, object), + Object.is({}, {}), + ] + `), + ).toEqual([true, false, true, false]) + }) + + test("Object.is rejects opaque runtime references", async () => { + expect((await error(`return Object.is(Math.max, Math.max)`)).kind).toBe("InvalidDataValue") + }) + + test("Object values and entries accept arrays", async () => { + expect(await value(`return [Object.values(["a", "b"]), Object.entries(["a", "b"])]`)).toEqual([ + ["a", "b"], + [ + ["0", "a"], + ["1", "b"], + ], + ]) + expect(await value(`const match = /a/.exec("ba"); return [Object.values(match), Object.entries(match)]`)).toEqual([ + ["a", 1], + [ + ["0", "a"], + ["index", 1], + ], + ]) + expect(await value(`return Object.keys(Object.values({ match: /a/.exec("ba") })[0])`)).toEqual(["0", "index"]) + }) + + test("Object.fromEntries accepts every supported entry collection", async () => { + expect( + await value(` + return [ + Object.fromEntries([["a", 1]]), + Object.fromEntries(new Map([["b", 2]])), + Object.fromEntries(new Set([["c", 3]])), + Object.fromEntries(new URLSearchParams("d=4")), + Object.fromEntries([{ 0: "e", 1: 5 }]), + Object.fromEntries(new Set([[{}, 6], [new Date(0), 7], [null, 8], [undefined, 9]])), + ] + `), + ).toEqual([ + { a: 1 }, + { b: 2 }, + { c: 3 }, + { d: "4" }, + { e: 5 }, + { "[object Object]": 6, "1970-01-01T00:00:00.000Z": 7, null: 8, undefined: 9 }, + ]) + expect(await value(`try { Object.fromEntries(new Set([Math.max])); return false } catch { return true }`)).toBe( + true, + ) + expect( + await value(`try { Object.fromEntries(new Map([["fn", Math.max]])); return false } catch { return true }`), + ).toBe(true) + }) + + test("deterministic Math methods match the host runtime", async () => { + const result = await value(` + return [ + Math.acos(0.5), Math.acosh(2), Math.asin(0.5), Math.asinh(2), Math.atan(1), Math.atan2(1, 2), Math.atanh(0.5), + Math.cos(0.5), Math.cosh(0.5), Math.sin(0.5), Math.sinh(0.5), Math.tan(0.5), Math.tanh(0.5), + Math.log1p(0.5), Math.expm1(0.5), Math.f16round(1.337), Math.fround(1.337), Math.clz32(1), Math.imul(2, 3), + ] + `) + expect(result).toEqual([ + Math.acos(0.5), + Math.acosh(2), + Math.asin(0.5), + Math.asinh(2), + Math.atan(1), + Math.atan2(1, 2), + Math.atanh(0.5), + Math.cos(0.5), + Math.cosh(0.5), + Math.sin(0.5), + Math.sinh(0.5), + Math.tan(0.5), + Math.tanh(0.5), + Math.log1p(0.5), + Math.expm1(0.5), + Math.f16round(1.337), + Math.fround(1.337), + Math.clz32(1), + Math.imul(2, 3), + ]) + }) + + test("Object.assign mutates and returns its target", async () => { + expect( + await value(` + const target = { a: 1 } + const result = Object.assign(target, { b: 2 }) + return { target, result, same: target === result } + `), + ).toEqual({ target: { a: 1, b: 2 }, result: { a: 1, b: 2 }, same: true }) + expect(await value(`try { Object.assign(null, { a: 1 }); return false } catch { return true }`)).toBe(true) + }) + + test("assignment resolves and reads its left side before evaluating the right side", async () => { + expect(await value(`let x = 1; x += (x = 5); return x`)).toBe(6) + expect(await value(`let i = 0; const values = [9]; values[i++] = i; return [values, i]`)).toEqual([[1], 1]) + expect(await value(`let i = 0; const values = [10, 20]; values[i++] += i; return [values, i]`)).toEqual([ + [11, 20], + 1, + ]) + }) + test("typeof reports constructors as functions and never throws", async () => { expect(await value(`return typeof Map`)).toBe("function") expect(await value(`return typeof ((x) => x)`)).toBe("function") @@ -598,11 +720,11 @@ describe("stdlib integration", () => { expect(await value(`const fn = () => 1; return !fn`)).toBe(false) }) - test("object spread of sandbox values is a no-op, like JS", async () => { + test("object spread of CodeMode values is a no-op, like JS", async () => { expect(await value(`return { ...new Map([["a", 1]]), kept: true }`)).toEqual({ kept: true }) }) - test("dates inside Map values survive in-sandbox reads", async () => { + test("dates inside Map values survive in-CodeMode reads", async () => { expect( await value(` const m = new Map([["start", new Date(1000)]]) @@ -644,7 +766,7 @@ describe("stdlib integration", () => { }) }) -describe("sandbox values at intra-sandbox checkpoints", () => { +describe("CodeMode values at intra-CodeMode checkpoints", () => { test("Object.values/entries keep Dates usable", async () => { expect(await value(`return Object.values({ d: new Date(0) })[0].getTime()`)).toBe(0) expect(await value(`const [key, d] = Object.entries({ d: new Date(0) })[0]; return key + ":" + d.getTime()`)).toBe( @@ -652,13 +774,50 @@ describe("sandbox values at intra-sandbox checkpoints", () => { ) }) + test("Object.values/entries preserve nested object identity", async () => { + expect( + await value(` + const child = { selected: false } + const rows = { a: child } + Object.values(rows)[0].selected = true + return child.selected + `), + ).toBe(true) + expect( + await value(` + const child = { selected: false } + const rows = { a: child } + Object.entries(rows)[0][1].selected = true + return child.selected + `), + ).toBe(true) + }) + + test("Object enumeration preserves promises and callable references", async () => { + expect( + await value(` + const pending = Promise.resolve(1) + const source = { pending } + return [Object.keys(source), Object.hasOwn(source, "pending"), await Object.values(source)[0], await Object.entries(source)[0][1]] + `), + ).toEqual([["pending"], true, 1, 1]) + expect(await value(`return Object.values({ max: Math.max })[0](1, 2)`)).toBe(2) + }) + + test("Object enumeration rejects invalid receivers and gives promises an await hint", async () => { + const diagnostic = await error(`return Object.keys(Promise.resolve({ a: 1 }))`) + expect(diagnostic.kind).toBe("InvalidDataValue") + expect(diagnostic.message).toContain("await") + expect((await error(`return Object.keys(Math)`)).kind).toBe("InvalidDataValue") + }) + test("Object.assign keeps Maps usable", async () => { expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe( 1, ) }) - test("object and array spread keep sandbox values usable", async () => { + test("object and array spread keep CodeMode values usable", async () => { expect( await value(` const src = { m: new Map([["a", 1]]) } @@ -670,15 +829,62 @@ describe("sandbox values at intra-sandbox checkpoints", () => { expect(await value(`const list = [new Date(1000)]; const copy = [...list]; return copy[0].getTime()`)).toBe(1000) }) - test("Array.from over arrays keeps nested sandbox values usable", async () => { + test("Array.from over arrays keeps nested CodeMode values usable", async () => { expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5) }) + test("Array.from and Array.of preserve nested object identity", async () => { + expect( + await value(` + const child = { selected: false } + Array.from([child])[0].selected = true + return child.selected + `), + ).toBe(true) + expect( + await value(` + const child = { selected: false } + Array.of(child)[0].selected = true + return child.selected + `), + ).toBe(true) + }) + + test("Array.from and Array.of preserve promises and callable references", async () => { + expect( + await value(` + const pending = Promise.resolve(1) + return [await Array.from([pending])[0], await Array.of(pending)[0]] + `), + ).toEqual([1, 1]) + expect(await value(`return [Array.from([Math.max])[0](1, 2), Array.of(Math.max)[0](3, 4)]`)).toEqual([2, 4]) + }) + + test("Array.from preserves identity across supported collection shapes", async () => { + expect( + await value(` + const child = { selected: false } + const fromArrayLike = Array.from({ 0: child, length: 1 }) + const fromMap = Array.from(new Map([["child", child]])) + const fromSet = Array.from(new Set([child])) + fromArrayLike[0].selected = true + return [fromMap[0][1] === child, fromSet[0] === child, child.selected] + `), + ).toEqual([true, true, true]) + }) + + test("Array.from rejects invalid receivers and gives promises an await hint", async () => { + const diagnostic = await error(`return Array.from(Promise.resolve([1]))`) + expect(diagnostic.kind).toBe("InvalidDataValue") + expect(diagnostic.message).toContain("await") + expect((await error(`return Array.from(() => 1)`)).kind).toBe("InvalidDataValue") + }) + test("regexes stay callable through Object.values", async () => { expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true) }) - test("Object.* helpers see sandbox values as empty objects, never internals", async () => { + test("Object.* helpers see CodeMode values as empty objects, never internals", async () => { expect(await value(`return Object.keys(new Map([["a", 1]]))`)).toEqual([]) expect(await value(`return Object.values(new Date(0))`)).toEqual([]) expect(await value(`return Object.entries(new Set([1]))`)).toEqual([]) diff --git a/packages/codemode/test/string-core-test262.test.ts b/packages/codemode/test/string-core-test262.test.ts new file mode 100644 index 0000000000..dc804af858 --- /dev/null +++ b/packages/codemode/test/string-core-test262.test.ts @@ -0,0 +1,580 @@ +/* + * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75: + * - test/built-ins/String/prototype/toLowerCase/S15.5.4.16_A2_T1.js + * - test/built-ins/String/prototype/toLowerCase/special_casing.js + * - test/built-ins/String/prototype/toLowerCase/special_casing_conditional.js + * - test/built-ins/String/prototype/toLowerCase/Final_Sigma_U180E.js + * - test/built-ins/String/prototype/toLowerCase/supplementary_plane.js + * - test/built-ins/String/prototype/toUpperCase/S15.5.4.18_A2_T1.js + * - test/built-ins/String/prototype/toUpperCase/special_casing.js + * - test/built-ins/String/prototype/toUpperCase/supplementary_plane.js + * - test/built-ins/String/prototype/trim/15.5.4.20-3-1.js + * - test/built-ins/String/prototype/trim/15.5.4.20-3-2.js + * - test/built-ins/String/prototype/trim/15.5.4.20-3-3.js + * - test/built-ins/String/prototype/trim/15.5.4.20-3-4.js + * - test/built-ins/String/prototype/trim/15.5.4.20-3-5.js + * - test/built-ins/String/prototype/trim/15.5.4.20-3-6.js + * - test/built-ins/String/prototype/trim/15.5.4.20-3-7.js + * - test/built-ins/String/prototype/trim/15.5.4.20-3-8.js + * - test/built-ins/String/prototype/trim/15.5.4.20-3-9.js + * - test/built-ins/String/prototype/trim/15.5.4.20-3-10.js + * - test/built-ins/String/prototype/trim/15.5.4.20-3-11.js + * - test/built-ins/String/prototype/trim/15.5.4.20-3-12.js + * - test/built-ins/String/prototype/trim/15.5.4.20-3-13.js + * - test/built-ins/String/prototype/trim/15.5.4.20-3-14.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-1.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-2.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-3.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-4.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-5.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-6.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-8.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-10.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-11.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-12.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-13.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-14.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-16.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-18.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-19.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-20.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-21.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-22.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-24.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-27.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-28.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-29.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-30.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-32.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-34.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-35.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-36.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-37.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-38.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-39.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-40.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-41.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-42.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-43.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-44.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-45.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-46.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-47.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-48.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-49.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-50.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-51.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-52.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-53.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-54.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-55.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-56.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-57.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-58.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-59.js + * - test/built-ins/String/prototype/trim/15.5.4.20-4-60.js + * - test/built-ins/String/prototype/trim/u180e.js + * - test/built-ins/String/prototype/trimStart/this-value-whitespace.js + * - test/built-ins/String/prototype/trimStart/this-value-line-terminator.js + * - test/built-ins/String/prototype/trimEnd/this-value-whitespace.js + * - test/built-ins/String/prototype/trimEnd/this-value-line-terminator.js + * - test/built-ins/String/prototype/repeat/repeat-string-n-times.js + * - test/built-ins/String/prototype/repeat/empty-string-returns-empty.js + * - test/built-ins/String/prototype/repeat/count-is-zero-returns-empty-string.js + * - test/built-ins/String/prototype/repeat/count-coerced-to-zero-returns-empty-string.js + * - test/built-ins/String/prototype/padStart/fill-string-empty.js + * - test/built-ins/String/prototype/padStart/normal-operation.js + * - test/built-ins/String/prototype/padStart/fill-string-omitted.js + * - test/built-ins/String/prototype/padStart/max-length-not-greater-than-string.js + * - test/built-ins/String/prototype/padEnd/fill-string-empty.js + * - test/built-ins/String/prototype/padEnd/normal-operation.js + * - test/built-ins/String/prototype/padEnd/fill-string-omitted.js + * - test/built-ins/String/prototype/padEnd/max-length-not-greater-than-string.js + * - test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T4.js + * - test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T7.js + * - test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T8.js + * - test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T1.js + * - test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T2.js + * - test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T3.js + * - test/built-ins/String/prototype/charAt/S9.4_A1.js + * - test/built-ins/String/prototype/charAt/S9.4_A2.js + * - test/built-ins/String/prototype/charAt/pos-rounding.js + * - test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T4.js + * - test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T7.js + * - test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T8.js + * - test/built-ins/String/prototype/charCodeAt/pos-rounding.js + * - test/built-ins/String/prototype/codePointAt/return-single-code-unit.js + * - test/built-ins/String/prototype/codePointAt/return-first-code-unit.js + * - test/built-ins/String/prototype/codePointAt/return-utf16-decode.js + * - test/built-ins/String/prototype/codePointAt/return-code-unit-coerced-position.js + * - test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-less-than-zero.js + * - test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-equal-or-more-than-size.js + * - test/built-ins/String/prototype/at/returns-code-unit.js + * - test/built-ins/String/prototype/at/returns-item.js + * - test/built-ins/String/prototype/at/returns-item-relative-index.js + * - test/built-ins/String/prototype/at/returns-undefined-for-out-of-range-index.js + * - test/built-ins/String/prototype/at/index-non-numeric-argument-tointeger.js + * - test/built-ins/String/prototype/concat/S15.5.4.6_A1_T4.js + * - test/built-ins/String/prototype/toString/string-primitive.js + * - test/built-ins/String/prototype/normalize/return-normalized-string.js + * - test/built-ins/String/prototype/normalize/return-normalized-string-using-default-parameter.js + * - test/built-ins/String/prototype/normalize/form-is-not-valid-throws.js + * - test/built-ins/String/prototype/localeCompare/15.5.4.9_CE.js + * - test/built-ins/String/fromCharCode/S15.5.3.2_A2.js + * - test/built-ins/String/fromCharCode/S15.5.3.2_A3_T1.js + * - test/built-ins/String/fromCharCode/S9.7_A1.js + * - test/built-ins/String/fromCharCode/S9.7_A2.1.js + * - test/built-ins/String/fromCharCode/S9.7_A2.2.js + * - test/built-ins/String/fromCharCode/S9.7_A3.2_T1.js + * - test/built-ins/String/fromCodePoint/arguments-is-empty.js + * - test/built-ins/String/fromCodePoint/return-string-value.js + * - test/built-ins/String/fromCodePoint/argument-is-not-integer.js + * - test/built-ins/String/fromCodePoint/number-is-out-of-range.js + * + * Copyright 2009 the Sputnik authors. All rights reserved. + * Copyright (C) 2009 the Sputnik authors. All rights reserved. + * Copyright (c) 2012 Ecma International. All rights reserved. + * Copyright 2012 Norbert Lindenberg. All rights reserved. + * Copyright 2012 Mozilla Corporation. All rights reserved. + * Copyright 2013 Microsoft Corporation. All rights reserved. + * Copyright (C) 2015 the V8 project authors. All rights reserved. + * Copyright (C) 2015 André Bargull. All rights reserved. + * Copyright (C) 2016 the V8 project authors. All rights reserved. + * Copyright (C) 2016 André Bargull. All rights reserved. + * Copyright (C) 2016 Jordan Harband. All rights reserved. + * Copyright (C) 2016 Mathias Bynens. All rights reserved. + * Copyright (c) 2017 Valerie Young. All rights reserved. + * Copyright (C) 2017 Valerie Young. All rights reserved. + * Copyright (C) 2020 Rick Waldron. All rights reserved. + * Copyright (C) 2022 Richard Gibson. All rights reserved. + * Test262 portions are governed by the BSD license in LICENSE.test262. + */ +import { describe, expect, test } from "bun:test" +import { Effect } from "effect" +import { CodeMode } from "../src/index.js" + +type Argument = string | number | undefined +type Outcome = "undefined" | "length" | "RangeError" +type Assertion = { + label: string + input?: string + args?: ReadonlyArray + expected?: string | number + outcome?: Outcome +} +type Vector = { + path: string + method: string + static?: boolean + assertions: ReadonlyArray +} + +const value = async (code: string) => { + const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} })) + if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) + return result.value +} + +const literal = (input: Argument) => { + if (input === undefined) return "undefined" + if (typeof input === "string") return JSON.stringify(input) + if (Number.isNaN(input)) return "NaN" + if (input === Infinity) return "Infinity" + if (input === -Infinity) return "-Infinity" + if (Object.is(input, -0)) return "-0" + return JSON.stringify(input) +} + +const vectors: Array = [] +const add = (path: string, method: string, assertions: ReadonlyArray, staticMethod = false) => { + vectors.push({ path, method, assertions, static: staticMethod }) +} +const assertion = (label: string, input: string, expected: string | number, args: ReadonlyArray = []) => ({ + label, + input, + args, + expected, +}) + +add("test/built-ins/String/prototype/toLowerCase/S15.5.4.16_A2_T1.js", "toLowerCase", [ + assertion("#1 direct value", "Hello, WoRlD!", "hello, world!"), + assertion("#2 String value", "Hello, WoRlD!", "hello, world!"), +]) +add("test/built-ins/String/prototype/toLowerCase/special_casing.js", "toLowerCase", [ + assertion( + "103 SpecialCasing mappings", + "\u00DF\u0130\uFB00\uFB01\uFB02\uFB03\uFB04\uFB05\uFB06\u0587\uFB13\uFB14\uFB15\uFB16\uFB17\u0149\u0390\u03B0\u01F0\u1E96\u1E97\u1E98\u1E99\u1E9A\u1F50\u1F52\u1F54\u1F56\u1FB6\u1FC6\u1FD2\u1FD3\u1FD6\u1FD7\u1FE2\u1FE3\u1FE4\u1FE6\u1FE7\u1FF6\u1F80\u1F81\u1F82\u1F83\u1F84\u1F85\u1F86\u1F87\u1F88\u1F89\u1F8A\u1F8B\u1F8C\u1F8D\u1F8E\u1F8F\u1F90\u1F91\u1F92\u1F93\u1F94\u1F95\u1F96\u1F97\u1F98\u1F99\u1F9A\u1F9B\u1F9C\u1F9D\u1F9E\u1F9F\u1FA0\u1FA1\u1FA2\u1FA3\u1FA4\u1FA5\u1FA6\u1FA7\u1FA8\u1FA9\u1FAA\u1FAB\u1FAC\u1FAD\u1FAE\u1FAF\u1FB3\u1FBC\u1FC3\u1FCC\u1FF3\u1FFC\u1FB2\u1FB4\u1FC2\u1FC4\u1FF2\u1FF4\u1FB7\u1FC7\u1FF7", + "\u00DF\u0069\u0307\uFB00\uFB01\uFB02\uFB03\uFB04\uFB05\uFB06\u0587\uFB13\uFB14\uFB15\uFB16\uFB17\u0149\u0390\u03B0\u01F0\u1E96\u1E97\u1E98\u1E99\u1E9A\u1F50\u1F52\u1F54\u1F56\u1FB6\u1FC6\u1FD2\u1FD3\u1FD6\u1FD7\u1FE2\u1FE3\u1FE4\u1FE6\u1FE7\u1FF6\u1F80\u1F81\u1F82\u1F83\u1F84\u1F85\u1F86\u1F87\u1F80\u1F81\u1F82\u1F83\u1F84\u1F85\u1F86\u1F87\u1F90\u1F91\u1F92\u1F93\u1F94\u1F95\u1F96\u1F97\u1F90\u1F91\u1F92\u1F93\u1F94\u1F95\u1F96\u1F97\u1FA0\u1FA1\u1FA2\u1FA3\u1FA4\u1FA5\u1FA6\u1FA7\u1FA0\u1FA1\u1FA2\u1FA3\u1FA4\u1FA5\u1FA6\u1FA7\u1FB3\u1FB3\u1FC3\u1FC3\u1FF3\u1FF3\u1FB2\u1FB4\u1FC2\u1FC4\u1FF2\u1FF4\u1FB7\u1FC7\u1FF7", + ), +]) +add("test/built-ins/String/prototype/toLowerCase/special_casing_conditional.js", "toLowerCase", [ + assertion("single sigma", "\u03A3", "\u03C3"), + assertion("preceded by cased", "A\u03A3", "a\u03C2"), + assertion("preceded by supplementary cased", "\uD835\uDCA2\u03A3", "\uD835\uDCA2\u03C2"), + assertion("preceded by full stop", "A.\u03A3", "a.\u03C2"), + assertion("preceded by soft hyphen", "A\u00AD\u03A3", "a\u00AD\u03C2"), + assertion("preceded by combining mark", "A\uD834\uDE42\u03A3", "a\uD834\uDE42\u03C2"), + assertion("preceded by uncased combining mark", "\u0345\u03A3", "\u0345\u03C3"), + assertion("preceded by cased and combining mark", "\u0391\u0345\u03A3", "\u03B1\u0345\u03C2"), + assertion("followed by cased", "A\u03A3B", "a\u03C3b"), + assertion("followed by supplementary cased", "A\u03A3\uD835\uDCA2", "a\u03C3\uD835\uDCA2"), + assertion("followed by full stop and cased", "A\u03A3.b", "a\u03C3.b"), + assertion("followed by soft hyphen and cased", "A\u03A3\u00ADB", "a\u03C3\u00ADb"), + assertion("followed by combining mark and cased", "A\u03A3\uD834\uDE42B", "a\u03C3\uD834\uDE42b"), + assertion("followed by uncased combining mark", "A\u03A3\u0345", "a\u03C2\u0345"), + assertion("followed by combining mark and cased Greek", "A\u03A3\u0345\u0391", "a\u03C3\u0345\u03B1"), +]) +add("test/built-ins/String/prototype/toLowerCase/Final_Sigma_U180E.js", "toLowerCase", [ + assertion("preceded by U+180E", "A\u180E\u03A3", "a\u180E\u03C2"), + assertion("preceded by U+180E and followed by cased", "A\u180E\u03A3B", "a\u180E\u03C3b"), + assertion("followed by U+180E", "A\u03A3\u180E", "a\u03C2\u180E"), + assertion("followed by U+180E and cased", "A\u03A3\u180EB", "a\u03C3\u180Eb"), + assertion("surrounded by U+180E", "A\u180E\u03A3\u180E", "a\u180E\u03C2\u180E"), + assertion("surrounded by U+180E and followed by cased", "A\u180E\u03A3\u180EB", "a\u180E\u03C3\u180Eb"), +]) +add("test/built-ins/String/prototype/toLowerCase/supplementary_plane.js", "toLowerCase", [ + assertion( + "40 Deseret mappings", + "\uD801\uDC00\uD801\uDC01\uD801\uDC02\uD801\uDC03\uD801\uDC04\uD801\uDC05\uD801\uDC06\uD801\uDC07\uD801\uDC08\uD801\uDC09\uD801\uDC0A\uD801\uDC0B\uD801\uDC0C\uD801\uDC0D\uD801\uDC0E\uD801\uDC0F\uD801\uDC10\uD801\uDC11\uD801\uDC12\uD801\uDC13\uD801\uDC14\uD801\uDC15\uD801\uDC16\uD801\uDC17\uD801\uDC18\uD801\uDC19\uD801\uDC1A\uD801\uDC1B\uD801\uDC1C\uD801\uDC1D\uD801\uDC1E\uD801\uDC1F\uD801\uDC20\uD801\uDC21\uD801\uDC22\uD801\uDC23\uD801\uDC24\uD801\uDC25\uD801\uDC26\uD801\uDC27", + "\uD801\uDC28\uD801\uDC29\uD801\uDC2A\uD801\uDC2B\uD801\uDC2C\uD801\uDC2D\uD801\uDC2E\uD801\uDC2F\uD801\uDC30\uD801\uDC31\uD801\uDC32\uD801\uDC33\uD801\uDC34\uD801\uDC35\uD801\uDC36\uD801\uDC37\uD801\uDC38\uD801\uDC39\uD801\uDC3A\uD801\uDC3B\uD801\uDC3C\uD801\uDC3D\uD801\uDC3E\uD801\uDC3F\uD801\uDC40\uD801\uDC41\uD801\uDC42\uD801\uDC43\uD801\uDC44\uD801\uDC45\uD801\uDC46\uD801\uDC47\uD801\uDC48\uD801\uDC49\uD801\uDC4A\uD801\uDC4B\uD801\uDC4C\uD801\uDC4D\uD801\uDC4E\uD801\uDC4F", + ), +]) +add("test/built-ins/String/prototype/toUpperCase/S15.5.4.18_A2_T1.js", "toUpperCase", [ + assertion("#1 direct value", "Hello, WoRlD!", "HELLO, WORLD!"), + assertion("#2 String value", "Hello, WoRlD!", "HELLO, WORLD!"), +]) +add("test/built-ins/String/prototype/toUpperCase/special_casing.js", "toUpperCase", [ + assertion( + "103 SpecialCasing mappings", + "\u00DF\u0130\uFB00\uFB01\uFB02\uFB03\uFB04\uFB05\uFB06\u0587\uFB13\uFB14\uFB15\uFB16\uFB17\u0149\u0390\u03B0\u01F0\u1E96\u1E97\u1E98\u1E99\u1E9A\u1F50\u1F52\u1F54\u1F56\u1FB6\u1FC6\u1FD2\u1FD3\u1FD6\u1FD7\u1FE2\u1FE3\u1FE4\u1FE6\u1FE7\u1FF6\u1F80\u1F81\u1F82\u1F83\u1F84\u1F85\u1F86\u1F87\u1F88\u1F89\u1F8A\u1F8B\u1F8C\u1F8D\u1F8E\u1F8F\u1F90\u1F91\u1F92\u1F93\u1F94\u1F95\u1F96\u1F97\u1F98\u1F99\u1F9A\u1F9B\u1F9C\u1F9D\u1F9E\u1F9F\u1FA0\u1FA1\u1FA2\u1FA3\u1FA4\u1FA5\u1FA6\u1FA7\u1FA8\u1FA9\u1FAA\u1FAB\u1FAC\u1FAD\u1FAE\u1FAF\u1FB3\u1FBC\u1FC3\u1FCC\u1FF3\u1FFC\u1FB2\u1FB4\u1FC2\u1FC4\u1FF2\u1FF4\u1FB7\u1FC7\u1FF7", + "\u0053\u0053\u0130\u0046\u0046\u0046\u0049\u0046\u004C\u0046\u0046\u0049\u0046\u0046\u004C\u0053\u0054\u0053\u0054\u0535\u0552\u0544\u0546\u0544\u0535\u0544\u053B\u054E\u0546\u0544\u053D\u02BC\u004E\u0399\u0308\u0301\u03A5\u0308\u0301\u004A\u030C\u0048\u0331\u0054\u0308\u0057\u030A\u0059\u030A\u0041\u02BE\u03A5\u0313\u03A5\u0313\u0300\u03A5\u0313\u0301\u03A5\u0313\u0342\u0391\u0342\u0397\u0342\u0399\u0308\u0300\u0399\u0308\u0301\u0399\u0342\u0399\u0308\u0342\u03A5\u0308\u0300\u03A5\u0308\u0301\u03A1\u0313\u03A5\u0342\u03A5\u0308\u0342\u03A9\u0342\u1F08\u0399\u1F09\u0399\u1F0A\u0399\u1F0B\u0399\u1F0C\u0399\u1F0D\u0399\u1F0E\u0399\u1F0F\u0399\u1F08\u0399\u1F09\u0399\u1F0A\u0399\u1F0B\u0399\u1F0C\u0399\u1F0D\u0399\u1F0E\u0399\u1F0F\u0399\u1F28\u0399\u1F29\u0399\u1F2A\u0399\u1F2B\u0399\u1F2C\u0399\u1F2D\u0399\u1F2E\u0399\u1F2F\u0399\u1F28\u0399\u1F29\u0399\u1F2A\u0399\u1F2B\u0399\u1F2C\u0399\u1F2D\u0399\u1F2E\u0399\u1F2F\u0399\u1F68\u0399\u1F69\u0399\u1F6A\u0399\u1F6B\u0399\u1F6C\u0399\u1F6D\u0399\u1F6E\u0399\u1F6F\u0399\u1F68\u0399\u1F69\u0399\u1F6A\u0399\u1F6B\u0399\u1F6C\u0399\u1F6D\u0399\u1F6E\u0399\u1F6F\u0399\u0391\u0399\u0391\u0399\u0397\u0399\u0397\u0399\u03A9\u0399\u03A9\u0399\u1FBA\u0399\u0386\u0399\u1FCA\u0399\u0389\u0399\u1FFA\u0399\u038F\u0399\u0391\u0342\u0399\u0397\u0342\u0399\u03A9\u0342\u0399", + ), +]) +add("test/built-ins/String/prototype/toUpperCase/supplementary_plane.js", "toUpperCase", [ + assertion( + "40 Deseret mappings", + "\uD801\uDC28\uD801\uDC29\uD801\uDC2A\uD801\uDC2B\uD801\uDC2C\uD801\uDC2D\uD801\uDC2E\uD801\uDC2F\uD801\uDC30\uD801\uDC31\uD801\uDC32\uD801\uDC33\uD801\uDC34\uD801\uDC35\uD801\uDC36\uD801\uDC37\uD801\uDC38\uD801\uDC39\uD801\uDC3A\uD801\uDC3B\uD801\uDC3C\uD801\uDC3D\uD801\uDC3E\uD801\uDC3F\uD801\uDC40\uD801\uDC41\uD801\uDC42\uD801\uDC43\uD801\uDC44\uD801\uDC45\uD801\uDC46\uD801\uDC47\uD801\uDC48\uD801\uDC49\uD801\uDC4A\uD801\uDC4B\uD801\uDC4C\uD801\uDC4D\uD801\uDC4E\uD801\uDC4F", + "\uD801\uDC00\uD801\uDC01\uD801\uDC02\uD801\uDC03\uD801\uDC04\uD801\uDC05\uD801\uDC06\uD801\uDC07\uD801\uDC08\uD801\uDC09\uD801\uDC0A\uD801\uDC0B\uD801\uDC0C\uD801\uDC0D\uD801\uDC0E\uD801\uDC0F\uD801\uDC10\uD801\uDC11\uD801\uDC12\uD801\uDC13\uD801\uDC14\uD801\uDC15\uD801\uDC16\uD801\uDC17\uD801\uDC18\uD801\uDC19\uD801\uDC1A\uD801\uDC1B\uD801\uDC1C\uD801\uDC1D\uD801\uDC1E\uD801\uDC1F\uD801\uDC20\uD801\uDC21\uD801\uDC22\uD801\uDC23\uD801\uDC24\uD801\uDC25\uD801\uDC26\uD801\uDC27", + ), +]) + +const whitespace = "\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF" +const lineTerminators = "\u000A\u000D\u2028\u2029" +const trim = (file: string, input: string, expected: string) => + add(`test/built-ins/String/prototype/trim/${file}`, "trim", [assertion("upstream assertion", input, expected)]) + +trim("15.5.4.20-3-1.js", lineTerminators, "") +trim("15.5.4.20-3-2.js", whitespace, "") +trim("15.5.4.20-3-3.js", whitespace + lineTerminators, "") +trim("15.5.4.20-3-4.js", whitespace + lineTerminators + "abc", "abc") +trim("15.5.4.20-3-5.js", "abc" + whitespace + lineTerminators, "abc") +trim("15.5.4.20-3-6.js", whitespace + lineTerminators + "abc" + whitespace + lineTerminators, "abc") +trim("15.5.4.20-3-7.js", "ab" + whitespace + lineTerminators + "cd", "ab" + whitespace + lineTerminators + "cd") +trim("15.5.4.20-3-8.js", "\0\u0000", "\0\u0000") +trim("15.5.4.20-3-9.js", "\0", "\0") +trim("15.5.4.20-3-10.js", "\u0000", "\u0000") +trim("15.5.4.20-3-11.js", "\0\u0000abc", "\0\u0000abc") +trim("15.5.4.20-3-12.js", "abc\0\u0000", "abc\0\u0000") +trim("15.5.4.20-3-13.js", "\0\u0000abc\0\u0000", "\0\u0000abc\0\u0000") +trim("15.5.4.20-3-14.js", "a\0\u0000bc", "a\0\u0000bc") +trim("15.5.4.20-4-1.js", "\u0009a bc \u0009", "a bc") +trim("15.5.4.20-4-2.js", " \u0009abc \u0009", "abc") +trim("15.5.4.20-4-3.js", "\u0009abc", "abc") +trim("15.5.4.20-4-4.js", "\u000Babc", "abc") +trim("15.5.4.20-4-5.js", "\u000Cabc", "abc") +trim("15.5.4.20-4-6.js", "\u0020abc", "abc") +trim("15.5.4.20-4-8.js", "\u00A0abc", "abc") +trim("15.5.4.20-4-10.js", "\uFEFFabc", "abc") +trim("15.5.4.20-4-11.js", "abc\u0009", "abc") +trim("15.5.4.20-4-12.js", "abc\u000B", "abc") +trim("15.5.4.20-4-13.js", "abc\u000C", "abc") +trim("15.5.4.20-4-14.js", "abc\u0020", "abc") +trim("15.5.4.20-4-16.js", "abc\u00A0", "abc") +trim("15.5.4.20-4-18.js", "abc\uFEFF", "abc") +trim("15.5.4.20-4-19.js", "\u0009abc\u0009", "abc") +trim("15.5.4.20-4-20.js", "\u000Babc\u000B", "abc") +trim("15.5.4.20-4-21.js", "\u000Cabc\u000C", "abc") +trim("15.5.4.20-4-22.js", "\u0020abc\u0020", "abc") +trim("15.5.4.20-4-24.js", "\u00A0abc\u00A0", "abc") +trim("15.5.4.20-4-27.js", "\u0009\u0009", "") +trim("15.5.4.20-4-28.js", "\u000B\u000B", "") +trim("15.5.4.20-4-29.js", "\u000C\u000C", "") +trim("15.5.4.20-4-30.js", "\u0020\u0020", "") +trim("15.5.4.20-4-32.js", "\u00A0\u00A0", "") +trim("15.5.4.20-4-34.js", "\uFEFF\uFEFF", "") +trim("15.5.4.20-4-35.js", "ab\u0009c", "ab\u0009c") +trim("15.5.4.20-4-36.js", "ab\u000Bc", "ab\u000Bc") +trim("15.5.4.20-4-37.js", "ab\u000Cc", "ab\u000Cc") +trim("15.5.4.20-4-38.js", "ab\u0020c", "ab\u0020c") +trim("15.5.4.20-4-39.js", "ab\u0085c", "ab\u0085c") +trim("15.5.4.20-4-40.js", "ab\u00A0c", "ab\u00A0c") +trim("15.5.4.20-4-41.js", "ab\u200Bc", "ab\u200Bc") +trim("15.5.4.20-4-42.js", "ab\uFEFFc", "ab\uFEFFc") +trim("15.5.4.20-4-43.js", "\u000Aabc", "abc") +trim("15.5.4.20-4-44.js", "\u000Dabc", "abc") +trim("15.5.4.20-4-45.js", "\u2028abc", "abc") +trim("15.5.4.20-4-46.js", "\u2029abc", "abc") +trim("15.5.4.20-4-47.js", "abc\u000A", "abc") +trim("15.5.4.20-4-48.js", "abc\u000D", "abc") +trim("15.5.4.20-4-49.js", "abc\u2028", "abc") +trim("15.5.4.20-4-50.js", "abc\u2029", "abc") +trim("15.5.4.20-4-51.js", "\u000Aabc\u000A", "abc") +trim("15.5.4.20-4-52.js", "\u000Dabc\u000D", "abc") +trim("15.5.4.20-4-53.js", "\u2028abc\u2028", "abc") +trim("15.5.4.20-4-54.js", "\u2029abc\u2029", "abc") +trim("15.5.4.20-4-55.js", "\u000A\u000A", "") +trim("15.5.4.20-4-56.js", "\u000D\u000D", "") +trim("15.5.4.20-4-57.js", "\u2028\u2028", "") +trim("15.5.4.20-4-58.js", "\u2029\u2029", "") +trim("15.5.4.20-4-59.js", "\u2029 abc", "abc") +trim("15.5.4.20-4-60.js", " ", "") +add("test/built-ins/String/prototype/trim/u180e.js", "trim", [ + assertion("trailing U+180E", "_\u180E", "_\u180E"), + assertion("only U+180E", "\u180E", "\u180E"), + assertion("leading U+180E", "\u180E_", "\u180E_"), +]) + +const directionalWhitespace = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF" +add("test/built-ins/String/prototype/trimStart/this-value-whitespace.js", "trimStart", [ + assertion("all whitespace", directionalWhitespace + "a" + directionalWhitespace + "b" + directionalWhitespace, "a" + directionalWhitespace + "b" + directionalWhitespace), +]) +add("test/built-ins/String/prototype/trimStart/this-value-line-terminator.js", "trimStart", [ + assertion("all line terminators", lineTerminators + "a" + lineTerminators + "b" + lineTerminators, "a" + lineTerminators + "b" + lineTerminators), +]) +add("test/built-ins/String/prototype/trimEnd/this-value-whitespace.js", "trimEnd", [ + assertion("all whitespace", directionalWhitespace + "a" + directionalWhitespace + "b" + directionalWhitespace, directionalWhitespace + "a" + directionalWhitespace + "b"), +]) +add("test/built-ins/String/prototype/trimEnd/this-value-line-terminator.js", "trimEnd", [ + assertion("all line terminators", lineTerminators + "a" + lineTerminators + "b" + lineTerminators, lineTerminators + "a" + lineTerminators + "b"), +]) +add("test/built-ins/String/prototype/repeat/repeat-string-n-times.js", "repeat", [ + assertion("repeat once", "abc", "abc", [1]), + assertion("repeat three times", "abc", "abcabcabc", [3]), + { label: "repeat 10000 times length", input: ".", args: [10000], expected: 10000, outcome: "length" }, +]) +add("test/built-ins/String/prototype/repeat/empty-string-returns-empty.js", "repeat", [ + assertion("count 1", "", "", [1]), + assertion("count 3", "", "", [3]), + assertion("maximum 32-bit count", "", "", [0xffffffff]), +]) +add("test/built-ins/String/prototype/repeat/count-is-zero-returns-empty-string.js", "repeat", [ + assertion("zero", "foo", "", [0]), +]) +add("test/built-ins/String/prototype/repeat/count-coerced-to-zero-returns-empty-string.js", "repeat", [ + assertion("fraction truncates to zero", "abc", "", [0.9]), +]) +add("test/built-ins/String/prototype/padStart/fill-string-empty.js", "padStart", [assertion("empty fill", "abc", "abc", [5, ""])]) +add("test/built-ins/String/prototype/padStart/normal-operation.js", "padStart", [ + assertion("truncated multi-character fill", "abc", "defdabc", [7, "def"]), + assertion("single-character fill", "abc", "**abc", [5, "*"]), + assertion("truncated surrogate pair", "abc", "\uD83D\uDCA9\uD83Dabc", [6, "\uD83D\uDCA9"]), +]) +add("test/built-ins/String/prototype/padStart/fill-string-omitted.js", "padStart", [ + assertion("omitted fill", "abc", " abc", [5]), + assertion("undefined fill", "abc", " abc", [5, undefined]), +]) +add("test/built-ins/String/prototype/padStart/max-length-not-greater-than-string.js", "padStart", [ + assertion("NaN", "abc", "abc", [NaN, "def"]), + assertion("negative infinity", "abc", "abc", [-Infinity, "def"]), + assertion("zero", "abc", "abc", [0, "def"]), + assertion("negative one", "abc", "abc", [-1, "def"]), + assertion("equal length", "abc", "abc", [3, "def"]), + assertion("fraction truncates", "abc", "abc", [3.9999, "def"]), +]) +add("test/built-ins/String/prototype/padEnd/fill-string-empty.js", "padEnd", [assertion("empty fill", "abc", "abc", [5, ""])]) +add("test/built-ins/String/prototype/padEnd/normal-operation.js", "padEnd", [ + assertion("truncated multi-character fill", "abc", "abcdefd", [7, "def"]), + assertion("single-character fill", "abc", "abc**", [5, "*"]), + assertion("truncated surrogate pair", "abc", "abc\uD83D\uDCA9\uD83D", [6, "\uD83D\uDCA9"]), +]) +add("test/built-ins/String/prototype/padEnd/fill-string-omitted.js", "padEnd", [ + assertion("omitted fill", "abc", "abc ", [5]), + assertion("undefined fill", "abc", "abc ", [5, undefined]), +]) +add("test/built-ins/String/prototype/padEnd/max-length-not-greater-than-string.js", "padEnd", [ + assertion("NaN", "abc", "abc", [NaN, "def"]), + assertion("negative infinity", "abc", "abc", [-Infinity, "def"]), + assertion("zero", "abc", "abc", [0, "def"]), + assertion("negative one", "abc", "abc", [-1, "def"]), + assertion("equal length", "abc", "abc", [3, "def"]), + assertion("fraction truncates", "abc", "abc", [3.9999, "def"]), +]) + +add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T4.js", "charAt", [assertion("omitted position", "lego", "l")]) +add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T7.js", "charAt", [assertion("undefined position", "lego", "l", [undefined])]) +add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T8.js", "charAt", [assertion("undefined position", "42", "4", [undefined])]) +add("test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T1.js", "charAt", ["A", "B", "C", "A", "B", "C"].map((expected, position) => assertion(`position ${position}`, "ABCABC", expected, [position]))) +add("test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T2.js", "charAt", [-2, -1].map((position) => assertion(`position ${position}`, "ABCABC", "", [position]))) +add("test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T3.js", "charAt", [6, 7].map((position) => assertion(`position ${position}`, "ABCABC", "", [position]))) +add("test/built-ins/String/prototype/charAt/S9.4_A1.js", "charAt", [assertion("NaN position", "abc", "a", [NaN])]) +add("test/built-ins/String/prototype/charAt/S9.4_A2.js", "charAt", [ + assertion("positive zero", "abc", "a", [0]), + assertion("negative zero", "abc", "a", [-0]), +]) +add("test/built-ins/String/prototype/charAt/pos-rounding.js", "charAt", [ + assertion("-0.99999", "abc", "a", [-0.99999]), + assertion("-0.00001", "abc", "a", [-0.00001]), + assertion("0.00001", "abc", "a", [0.00001]), + assertion("0.99999", "abc", "a", [0.99999]), + assertion("1.00001", "abc", "b", [1.00001]), + assertion("1.99999", "abc", "b", [1.99999]), +]) + +add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T4.js", "charCodeAt", [assertion("omitted position", "smart", 0x73)]) +add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T7.js", "charCodeAt", [assertion("undefined position", "lego", 0x6c, [undefined])]) +add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T8.js", "charCodeAt", [assertion("undefined position", "42", 0x34, [undefined])]) +add("test/built-ins/String/prototype/charCodeAt/pos-rounding.js", "charCodeAt", [ + assertion("-0.99999", "abc", 0x61, [-0.99999]), + assertion("-0.00001", "abc", 0x61, [-0.00001]), + assertion("0.00001", "abc", 0x61, [0.00001]), + assertion("0.99999", "abc", 0x61, [0.99999]), + assertion("1.00001", "abc", 0x62, [1.00001]), + assertion("1.99999", "abc", 0x62, [1.99999]), +]) + +add("test/built-ins/String/prototype/codePointAt/return-single-code-unit.js", "codePointAt", [ + assertion("a", "abc", 97, [0]), assertion("b", "abc", 98, [1]), assertion("c", "abc", 99, [2]), + assertion("ordinary BMP", "\uAAAA\uBBBB", 0xaaaa, [0]), assertion("before high-surrogate range", "\uD7FF\uAAAA", 0xd7ff, [0]), + assertion("low surrogate", "\uDC00\uAAAA", 0xdc00, [0]), assertion("trailing D800", "123\uD800", 0xd800, [3]), + assertion("trailing DAAA", "123\uDAAA", 0xdaaa, [3]), assertion("trailing DBFF", "123\uDBFF", 0xdbff, [3]), +]) +add("test/built-ins/String/prototype/codePointAt/return-first-code-unit.js", "codePointAt", [ + assertion("D800 before DBFF", "\uD800\uDBFF", 0xd800, [0]), assertion("D800 before E000", "\uD800\uE000", 0xd800, [0]), + assertion("DAAA before DBFF", "\uDAAA\uDBFF", 0xdaaa, [0]), assertion("DAAA before E000", "\uDAAA\uE000", 0xdaaa, [0]), + assertion("DBFF before DBFF", "\uDBFF\uDBFF", 0xdbff, [0]), assertion("DBFF before E000", "\uDBFF\uE000", 0xdbff, [0]), + assertion("D800 before NUL", "\uD800\u0000", 0xd800, [0]), assertion("D800 before FFFF", "\uD800\uFFFF", 0xd800, [0]), + assertion("DAAA before NUL", "\uDAAA\u0000", 0xdaaa, [0]), assertion("DAAA before FFFF", "\uDAAA\uFFFF", 0xdaaa, [0]), + assertion("DBFF before FFFF", "\uDBFF\uFFFF", 0xdbff, [0]), +]) +add("test/built-ins/String/prototype/codePointAt/return-utf16-decode.js", "codePointAt", [ + assertion("U+10000", "\uD800\uDC00", 65536, [0]), assertion("U+101D0", "\uD800\uDDD0", 66000, [0]), + assertion("U+103FF", "\uD800\uDFFF", 66559, [0]), assertion("U+BA800", "\uDAAA\uDC00", 763904, [0]), + assertion("U+BA9D0", "\uDAAA\uDDD0", 764368, [0]), assertion("U+BABFF", "\uDAAA\uDFFF", 764927, [0]), + assertion("U+10FC00", "\uDBFF\uDC00", 1113088, [0]), assertion("U+10FDD0", "\uDBFF\uDDD0", 1113552, [0]), + assertion("U+10FFFF", "\uDBFF\uDFFF", 1114111, [0]), +]) +add("test/built-ins/String/prototype/codePointAt/return-code-unit-coerced-position.js", "codePointAt", [ + assertion("NaN", "\uD800\uDC00", 65536, [NaN]), assertion("undefined", "\uD800\uDC00", 65536, [undefined]), +]) +add("test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-less-than-zero.js", "codePointAt", [ + { label: "negative one", input: "abc", args: [-1], outcome: "undefined" }, + { label: "negative infinity", input: "abc", args: [-Infinity], outcome: "undefined" }, +]) +add("test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-equal-or-more-than-size.js", "codePointAt", [ + { label: "equal to size", input: "abc", args: [3], outcome: "undefined" }, + { label: "greater than size", input: "abc", args: [4], outcome: "undefined" }, + { label: "positive infinity", input: "abc", args: [Infinity], outcome: "undefined" }, +]) + +add("test/built-ins/String/prototype/at/returns-code-unit.js", "at", [ + assertion("position 0", "12\uD80034", "1", [0]), assertion("position 1", "12\uD80034", "2", [1]), + assertion("unpaired surrogate", "12\uD80034", "\uD800", [2]), assertion("position 3", "12\uD80034", "3", [3]), + assertion("position 4", "12\uD80034", "4", [4]), +]) +add("test/built-ins/String/prototype/at/returns-item.js", "at", ["1", "2", "3", "4", "5"].map((expected, position) => assertion(`position ${position}`, "12345", expected, [position]))) +add("test/built-ins/String/prototype/at/returns-item-relative-index.js", "at", [ + assertion("zero", "12345", "1", [0]), assertion("negative one", "12345", "5", [-1]), + assertion("negative three", "12345", "3", [-3]), assertion("negative four", "12345", "2", [-4]), +]) +add("test/built-ins/String/prototype/at/returns-undefined-for-out-of-range-index.js", "at", [-2, 0, 1].map((position) => ({ label: `position ${position}`, input: "", args: [position], outcome: "undefined" }))) +add("test/built-ins/String/prototype/at/index-non-numeric-argument-tointeger.js", "at", [assertion("undefined", "01", "0", [undefined])]) + +add("test/built-ins/String/prototype/concat/S15.5.4.6_A1_T4.js", "concat", [assertion("no arguments", "lego", "lego")]) +add("test/built-ins/String/prototype/toString/string-primitive.js", "toString", [ + assertion("empty string", "", ""), assertion("non-empty string", "str", "str"), +]) + +add("test/built-ins/String/prototype/normalize/return-normalized-string.js", "normalize", [ + assertion("NFC short", "\u1E9B\u0323", "\u1E9B\u0323", ["NFC"]), + assertion("NFD short", "\u1E9B\u0323", "\u017F\u0323\u0307", ["NFD"]), + assertion("NFKC short", "\u1E9B\u0323", "\u1E69", ["NFKC"]), + assertion("NFKD short", "\u1E9B\u0323", "\u0073\u0323\u0307", ["NFKD"]), + assertion("NFC long", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFC"]), + assertion("NFD long", "\u00C5\u2ADC\u0958\u2126\u0344", "A\u030A\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFD"]), + assertion("NFKC long", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFKC"]), + assertion("NFKD long", "\u00C5\u2ADC\u0958\u2126\u0344", "A\u030A\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFKD"]), +]) +add("test/built-ins/String/prototype/normalize/return-normalized-string-using-default-parameter.js", "normalize", [ + assertion("omitted", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301"), + assertion("undefined", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", [undefined]), +]) +add("test/built-ins/String/prototype/normalize/form-is-not-valid-throws.js", "normalize", [ + { label: "bar", input: "foo", args: ["bar"], outcome: "RangeError" }, + { label: "NFC1", input: "foo", args: ["NFC1"], outcome: "RangeError" }, +]) + +add("test/built-ins/String/prototype/localeCompare/15.5.4.9_CE.js", "localeCompare", [ + assertion("D70", "o\u0308", 0, ["ö"]), assertion("reordered diaeresis", "ä\u0323", 0, ["a\u0323\u0308"]), + assertion("reordered marks", "a\u0308\u0323", 0, ["a\u0323\u0308"]), assertion("precomposed dot below", "ạ\u0308", 0, ["a\u0323\u0308"]), + assertion("breve after diaeresis", "ä\u0306", 0, ["a\u0308\u0306"]), assertion("diaeresis after breve", "ă\u0308", 0, ["a\u0306\u0308"]), + assertion("Hangul", "\u1111\u1171\u11B6", 0, ["퓛"]), assertion("angstrom compatibility", "Å", 0, ["Å"]), + assertion("angstrom decomposed", "Å", 0, ["A\u030A"]), assertion("reordered horn and dot", "x\u031B\u0323", 0, ["x\u0323\u031B"]), + assertion("Vietnamese precomposed 1", "ự", 0, ["ụ\u031B"]), assertion("Vietnamese decomposed", "ự", 0, ["u\u031B\u0323"]), + assertion("Vietnamese precomposed 2", "ự", 0, ["ư\u0323"]), assertion("Vietnamese reordered", "ự", 0, ["u\u0323\u031B"]), + assertion("cedilla", "Ç", 0, ["C\u0327"]), assertion("q reordered", "q\u0307\u0323", 0, ["q\u0323\u0307"]), + assertion("Hangul syllable", "가", 0, ["\u1100\u1161"]), assertion("ohm", "Ω", 0, ["Ω"]), + assertion("angstrom", "Å", 0, ["A\u030A"]), assertion("circumflex", "ô", 0, ["o\u0302"]), + assertion("s with marks", "ṩ", 0, ["s\u0323\u0307"]), assertion("d composed plus dot", "ḋ\u0323", 0, ["d\u0323\u0307"]), + assertion("d two precompositions", "ḋ\u0323", 0, ["ḍ\u0307"]), +]) + +add("test/built-ins/String/fromCharCode/S15.5.3.2_A2.js", "fromCharCode", [{ label: "no arguments", expected: "" }], true) +add("test/built-ins/String/fromCharCode/S15.5.3.2_A3_T1.js", "fromCharCode", [{ label: "ABBA", args: [65, 66, 66, 65], expected: "ABBA" }], true) +add("test/built-ins/String/fromCharCode/S9.7_A1.js", "fromCharCode", [ + { label: "NaN", args: [NaN], expected: 0 }, { label: "zero", args: [0], expected: 0 }, { label: "negative zero", args: [-0], expected: 0 }, + { label: "positive infinity", args: [Infinity], expected: 0 }, { label: "negative infinity", args: [-Infinity], expected: 0 }, +], true) +add("test/built-ins/String/fromCharCode/S9.7_A2.1.js", "fromCharCode", [ + [0, 0], [1, 1], [-1, 65535], [65535, 65535], [65534, 65534], [65536, 0], [4294967295, 65535], [4294967294, 65534], [4294967296, 0], +].map(([input, expected]) => ({ label: String(input), args: [input!], expected })), true) +add("test/built-ins/String/fromCharCode/S9.7_A2.2.js", "fromCharCode", [ + [-32767, 32769], [-32768, 32768], [-32769, 32767], [-65535, 1], [-65536, 0], [-65537, 65535], [65535, 65535], [65536, 0], [65537, 1], [131071, 65535], [131072, 0], [131073, 1], +].map(([input, expected]) => ({ label: String(input), args: [input!], expected })), true) +add("test/built-ins/String/fromCharCode/S9.7_A3.2_T1.js", "fromCharCode", [ + { label: "positive fraction", args: [1.2345], expected: 1 }, { label: "negative fraction", args: [-5.4321], expected: 65531 }, +], true) + +add("test/built-ins/String/fromCodePoint/arguments-is-empty.js", "fromCodePoint", [{ label: "no arguments", expected: "" }], true) +add("test/built-ins/String/fromCodePoint/return-string-value.js", "fromCodePoint", [ + { label: "NUL", args: [0], expected: "\x00" }, { label: "asterisk", args: [42], expected: "*" }, + { label: "AZ", args: [65, 90], expected: "AZ" }, { label: "Cyrillic", args: [0x404], expected: "\u0404" }, + { label: "hex supplementary", args: [0x2f804], expected: "\uD87E\uDC04" }, { label: "decimal supplementary", args: [194564], expected: "\uD87E\uDC04" }, + { label: "mixed supplementary", args: [0x1d306, 0x61, 0x1d307], expected: "\uD834\uDF06a\uD834\uDF07" }, + { label: "maximum code point", args: [1114111], expected: "\uDBFF\uDFFF" }, +], true) +add("test/built-ins/String/fromCodePoint/argument-is-not-integer.js", "fromCodePoint", [ + { label: "fraction", args: [3.14], outcome: "RangeError" }, { label: "fraction after valid", args: [42, 3.14], outcome: "RangeError" }, +], true) +add("test/built-ins/String/fromCodePoint/number-is-out-of-range.js", "fromCodePoint", [ + { label: "negative one", args: [-1], outcome: "RangeError" }, { label: "negative after valid", args: [1, -1], outcome: "RangeError" }, + { label: "above maximum", args: [1114112], outcome: "RangeError" }, { label: "infinity", args: [Infinity], outcome: "RangeError" }, +], true) + +describe("Test262-adapted core String behavior", () => { + for (const vector of vectors) { + test(vector.path, async () => { + const results = vector.assertions.map((item) => { + const args = (item.args ?? []).map(literal).join(", ") + const expression = vector.static + ? `String.${vector.method}(${args})` + : `${JSON.stringify(item.input)}.${vector.method}(${args})` + const observed = vector.static && vector.method === "fromCharCode" && typeof item.expected === "number" + ? `${expression}.charCodeAt(0)` + : expression + const checked = item.outcome === "undefined" + ? `${observed} === undefined` + : item.outcome === "length" + ? `${observed}.length` + : item.outcome === "RangeError" + ? `(() => { try { ${observed}; return false } catch (error) { return error instanceof RangeError } })()` + : observed + return `{ label: ${JSON.stringify(item.label)}, value: ${checked} }` + }) + const expected = vector.assertions.map((item) => ({ + label: item.label, + value: item.outcome === undefined || item.outcome === "length" ? item.expected! : true, + })) + expect(await value(`return [${results.join(",")}]`)).toEqual(expected) + }) + } +}) diff --git a/packages/codemode/test/string-regexp-test262.test.ts b/packages/codemode/test/string-regexp-test262.test.ts new file mode 100644 index 0000000000..80c8908229 --- /dev/null +++ b/packages/codemode/test/string-regexp-test262.test.ts @@ -0,0 +1,625 @@ +/* + * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75: + * - test/built-ins/String/prototype/split/separator-regexp.js + * - test/built-ins/String/prototype/split/arguments-are-regexp-s-and-3-and-instance-is-string-a-b-c-de-f.js + * - test/built-ins/String/prototype/split/argument-is-regexp-s-and-instance-is-string-a-b-c-de-f.js + * - test/built-ins/String/prototype/split/argument-is-regexp-d-and-instance-is-string-dfe23iu-34-65.js + * - test/built-ins/String/prototype/split/argument-is-regexp-reg-exp-d-and-instance-is-string-dfe23iu-34-65.js + * - test/built-ins/String/prototype/split/argument-is-regexp-a-z-and-instance-is-string-abc.js + * - test/built-ins/String/prototype/split/argument-is-reg-exp-a-z-and-instance-is-string-abc.js + * - test/built-ins/String/prototype/split/arguments-are-regexp-l-and-undefined-and-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/arguments-are-regexp-l-and-0-and-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/arguments-are-regexp-l-and-1-and-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/arguments-are-regexp-l-and-2-and-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/arguments-are-regexp-l-and-3-and-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/arguments-are-regexp-l-and-4-and-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/argument-is-regexp-l-and-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/argument-is-new-reg-exp-and-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-0-and-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-1-and-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-2-and-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-3-and-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-4-and-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-undefined-and-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/call-split-2-instance-is-string-one-two-three-four-five.js + * - test/built-ins/String/prototype/split/separator-regexp-comma-instance-is-string-one-1-two-2-four-4.js + * - test/built-ins/String/prototype/split/argument-is-regexp-x-and-instance-is-string-a-b-c-de-f.js + * - test/built-ins/String/prototype/replace/regexp-capture-by-index.js + * - test/built-ins/String/prototype/replace/S15.5.4.11_A1_T17.js + * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T1.js + * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T2.js + * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T3.js + * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T4.js + * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T5.js + * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T6.js + * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T7.js + * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T8.js + * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T9.js + * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T10.js + * - test/built-ins/String/prototype/replace/S15.5.4.11_A3_T1.js + * - test/built-ins/String/prototype/replace/S15.5.4.11_A3_T2.js + * - test/built-ins/String/prototype/replace/S15.5.4.11_A3_T3.js + * - test/built-ins/String/prototype/replace/S15.5.4.11_A5_T1.js + * - test/built-ins/String/prototype/replaceAll/searchValue-replacer-RegExp-call.js + * - test/built-ins/String/prototype/replaceAll/searchValue-empty-string.js + * - test/built-ins/String/prototype/replaceAll/searchValue-empty-string-this-empty-string.js + * - test/built-ins/String/prototype/replaceAll/replaceValue-value-replaces-string.js + * - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024.js + * - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0024.js + * - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0026.js + * - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0060.js + * - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0027.js + * - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024N.js + * - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024NN.js + * - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x003C.js + * - test/built-ins/String/prototype/match/S15.5.4.10_A1_T14.js + * - test/built-ins/String/prototype/match/S15.5.4.10_A2_T2.js + * - test/built-ins/String/prototype/match/S15.5.4.10_A2_T3.js + * - test/built-ins/String/prototype/match/S15.5.4.10_A2_T4.js + * - test/built-ins/String/prototype/match/S15.5.4.10_A2_T5.js + * - test/built-ins/String/prototype/match/S15.5.4.10_A2_T6.js + * - test/built-ins/String/prototype/match/S15.5.4.10_A2_T7.js + * - test/built-ins/String/prototype/match/S15.5.4.10_A2_T8.js + * - test/built-ins/String/prototype/match/S15.5.4.10_A2_T12.js + * - test/built-ins/String/prototype/matchAll/regexp-prototype-matchAll-v-u-flag.js + * - test/built-ins/String/prototype/search/S15.5.4.12_A1_T14.js + * - test/built-ins/String/prototype/search/S15.5.4.12_A2_T1.js + * - test/built-ins/String/prototype/search/S15.5.4.12_A2_T2.js + * - test/built-ins/String/prototype/search/S15.5.4.12_A2_T3.js + * - test/built-ins/String/prototype/search/S15.5.4.12_A2_T4.js + * - test/built-ins/String/prototype/search/S15.5.4.12_A2_T5.js + * - test/built-ins/String/prototype/search/S15.5.4.12_A2_T6.js + * - test/built-ins/String/prototype/search/S15.5.4.12_A2_T7.js + * - test/built-ins/String/prototype/search/S15.5.4.12_A3_T1.js + * - test/built-ins/String/prototype/search/S15.5.4.12_A3_T2.js + * + * Copyright 2009 the Sputnik authors. All rights reserved. + * Copyright (C) 2019 Leo Balter. All rights reserved. + * Copyright (C) 2020 Rick Waldron. All rights reserved. + * Copyright (C) 2023 Richard Gibson. All rights reserved. + * Copyright (C) 2024 Tan Meng. All rights reserved. + * Test262 portions are governed by the BSD license in LICENSE.test262. + */ +import { describe, expect, test } from "bun:test" +import { Effect } from "effect" +import { CodeMode } from "../src/index.js" + +type Vector = { + readonly path: string + readonly code: string + readonly expected: CodeMode.DataValue +} + +const value = async (code: string) => { + const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} })) + if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) + return result.value +} + +const run = (name: string, vectors: ReadonlyArray) => { + describe(name, () => { + for (const vector of vectors) { + test(vector.path, async () => { + expect(await value(vector.code)).toEqual(vector.expected) + }) + } + }) +} + +run("Test262-adapted regexp split behavior", [ + { + path: "test/built-ins/String/prototype/split/separator-regexp.js", + code: ` + return [ + "x".split(/^/), "x".split(/$/), "x".split(/.?/), "x".split(/.*/), "x".split(/.+/), + "x".split(/.*?/), "x".split(/.{1}/), "x".split(/.{1,}/), "x".split(/.{1,2}/), + "x".split(/()/), "x".split(/./), "x".split(/(?:)/), "x".split(/(...)/), + "x".split(/(|)/), "x".split(/[]/), "x".split(/[^]/), "x".split(/[.-.]/), + "x".split(/\\0/), "x".split(/\\b/), "x".split(/\\B/), "x".split(/\\d/), + "x".split(/\\D/), "x".split(/\\n/), "x".split(/\\r/), "x".split(/\\s/), + "x".split(/\\S/), "x".split(/\\v/), "x".split(/\\w/), "x".split(/\\W/), + ] + `, + expected: [ + ["x"], ["x"], ["", ""], ["", ""], ["", ""], ["x"], ["", ""], ["", ""], ["", ""], + ["x"], ["", ""], ["x"], ["x"], ["x"], ["x"], ["", ""], ["x"], ["x"], ["x"], + ["x"], ["x"], ["", ""], ["x"], ["x"], ["x"], ["", ""], ["x"], ["", ""], ["x"], + ], + }, + { + path: "test/built-ins/String/prototype/split/arguments-are-regexp-s-and-3-and-instance-is-string-a-b-c-de-f.js", + code: `return "a b c de f".split(/\\s/, 3)`, + expected: ["a", "b", "c"], + }, + { + path: "test/built-ins/String/prototype/split/argument-is-regexp-s-and-instance-is-string-a-b-c-de-f.js", + code: `return "a b c de f".split(/\\s/)`, + expected: ["a", "b", "c", "de", "f"], + }, + { + path: "test/built-ins/String/prototype/split/argument-is-regexp-d-and-instance-is-string-dfe23iu-34-65.js", + code: `return "dfe23iu 34 =+65--".split(/\\d+/)`, + expected: ["dfe", "iu ", " =+", "--"], + }, + { + path: "test/built-ins/String/prototype/split/argument-is-regexp-reg-exp-d-and-instance-is-string-dfe23iu-34-65.js", + code: `return "dfe23iu 34 =+65--".split(new RegExp("\\\\d+"))`, + expected: ["dfe", "iu ", " =+", "--"], + }, + { + path: "test/built-ins/String/prototype/split/argument-is-regexp-a-z-and-instance-is-string-abc.js", + code: `return "abc".split(/[a-z]/)`, + expected: ["", "", "", ""], + }, + { + path: "test/built-ins/String/prototype/split/argument-is-reg-exp-a-z-and-instance-is-string-abc.js", + code: `return "abc".split(new RegExp("[a-z]"))`, + expected: ["", "", "", ""], + }, + { + path: "test/built-ins/String/prototype/split/arguments-are-regexp-l-and-undefined-and-instance-is-string-hello.js", + code: `return "hello".split(/l/, undefined)`, + expected: ["he", "", "o"], + }, + { + path: "test/built-ins/String/prototype/split/arguments-are-regexp-l-and-0-and-instance-is-string-hello.js", + code: `return "hello".split(/l/, 0)`, + expected: [], + }, + { + path: "test/built-ins/String/prototype/split/arguments-are-regexp-l-and-1-and-instance-is-string-hello.js", + code: `return "hello".split(/l/, 1)`, + expected: ["he"], + }, + { + path: "test/built-ins/String/prototype/split/arguments-are-regexp-l-and-2-and-instance-is-string-hello.js", + code: `return "hello".split(/l/, 2)`, + expected: ["he", ""], + }, + { + path: "test/built-ins/String/prototype/split/arguments-are-regexp-l-and-3-and-instance-is-string-hello.js", + code: `return "hello".split(/l/, 3)`, + expected: ["he", "", "o"], + }, + { + path: "test/built-ins/String/prototype/split/arguments-are-regexp-l-and-4-and-instance-is-string-hello.js", + code: `return "hello".split(/l/, 4)`, + expected: ["he", "", "o"], + }, + { + path: "test/built-ins/String/prototype/split/argument-is-regexp-l-and-instance-is-string-hello.js", + code: `return "hello".split(/l/)`, + expected: ["he", "", "o"], + }, + { + path: "test/built-ins/String/prototype/split/argument-is-new-reg-exp-and-instance-is-string-hello.js", + code: `return "hello".split(new RegExp())`, + expected: ["h", "e", "l", "l", "o"], + }, + { + path: "test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-0-and-instance-is-string-hello.js", + code: `return "hello".split(new RegExp(), 0)`, + expected: [], + }, + { + path: "test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-1-and-instance-is-string-hello.js", + code: `return "hello".split(new RegExp(), 1)`, + expected: ["h"], + }, + { + path: "test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-2-and-instance-is-string-hello.js", + code: `return "hello".split(new RegExp(), 2)`, + expected: ["h", "e"], + }, + { + path: "test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-3-and-instance-is-string-hello.js", + code: `return "hello".split(new RegExp(), 3)`, + expected: ["h", "e", "l"], + }, + { + path: "test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-4-and-instance-is-string-hello.js", + code: `return "hello".split(new RegExp(), 4)`, + expected: ["h", "e", "l", "l"], + }, + { + path: "test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-undefined-and-instance-is-string-hello.js", + code: `return "hello".split(new RegExp(), undefined)`, + expected: ["h", "e", "l", "l", "o"], + }, + { + path: "test/built-ins/String/prototype/split/call-split-2-instance-is-string-one-two-three-four-five.js", + code: `return "one two three four five".split(/ /, 2)`, + expected: ["one", "two"], + }, + { + path: "test/built-ins/String/prototype/split/separator-regexp-comma-instance-is-string-one-1-two-2-four-4.js", + code: `return "one-1,two-2,four-4".split(/,/)`, + expected: ["one-1", "two-2", "four-4"], + }, + { + path: "test/built-ins/String/prototype/split/argument-is-regexp-x-and-instance-is-string-a-b-c-de-f.js", + code: `return "a b c de f".split(/X/)`, + expected: ["a b c de f"], + }, +]) + +run("Test262-adapted replace behavior", [ + { + path: "test/built-ins/String/prototype/replace/regexp-capture-by-index.js", + code: ` + const str = "foo-x-bar" + const patterns = ["x", /x/, /(x)/, /(x)($^)?/, /((((((((((x))))))))))/] + const replacements = ["|$0|", "|$00|", "|$000|", "|$1|", "|$01|", "|$010|", "|$2|", "|$02|", "|$020|", "|$10|", "|$100|", "|$20|", "|$200|"] + return replacements.flatMap((replacement) => patterns.map((pattern) => str.replace(pattern, replacement))) + `, + expected: [ + "foo-|$0|-bar", "foo-|$0|-bar", "foo-|$0|-bar", "foo-|$0|-bar", "foo-|$0|-bar", + "foo-|$00|-bar", "foo-|$00|-bar", "foo-|$00|-bar", "foo-|$00|-bar", "foo-|$00|-bar", + "foo-|$000|-bar", "foo-|$000|-bar", "foo-|$000|-bar", "foo-|$000|-bar", "foo-|$000|-bar", + "foo-|$1|-bar", "foo-|$1|-bar", "foo-|x|-bar", "foo-|x|-bar", "foo-|x|-bar", + "foo-|$01|-bar", "foo-|$01|-bar", "foo-|x|-bar", "foo-|x|-bar", "foo-|x|-bar", + "foo-|$010|-bar", "foo-|$010|-bar", "foo-|x0|-bar", "foo-|x0|-bar", "foo-|x0|-bar", + "foo-|$2|-bar", "foo-|$2|-bar", "foo-|$2|-bar", "foo-||-bar", "foo-|x|-bar", + "foo-|$02|-bar", "foo-|$02|-bar", "foo-|$02|-bar", "foo-||-bar", "foo-|x|-bar", + "foo-|$020|-bar", "foo-|$020|-bar", "foo-|$020|-bar", "foo-|0|-bar", "foo-|x0|-bar", + "foo-|$10|-bar", "foo-|$10|-bar", "foo-|x0|-bar", "foo-|x0|-bar", "foo-|x|-bar", + "foo-|$100|-bar", "foo-|$100|-bar", "foo-|x00|-bar", "foo-|x00|-bar", "foo-|x0|-bar", + "foo-|$20|-bar", "foo-|$20|-bar", "foo-|$20|-bar", "foo-|0|-bar", "foo-|x0|-bar", + "foo-|$200|-bar", "foo-|$200|-bar", "foo-|$200|-bar", "foo-|00|-bar", "foo-|x00|-bar", + ], + }, + { + path: "test/built-ins/String/prototype/replace/S15.5.4.11_A1_T17.js", + code: `return "asdf".replace(new RegExp(undefined, "g"), "1")`, + expected: "1a1s1d1f1", + }, + { + path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T1.js", + code: `return "She sells seashells by the seashore.".replace(/sh/g, "sch")`, + expected: "She sells seaschells by the seaschore.", + }, + { + path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T2.js", + code: `return "She sells seashells by the seashore.".replace(/sh/g, "$$sch")`, + expected: "She sells sea$schells by the sea$schore.", + }, + { + path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T3.js", + code: `return "She sells seashells by the seashore.".replace(/sh/g, "$&sch")`, + expected: "She sells seashschells by the seashschore.", + }, + { + path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T4.js", + code: `return "She sells seashells by the seashore.".replace(/sh/g, "$\`sch")`, + expected: "She sells seaShe sells seaschells by the seaShe sells seashells by the seaschore.", + }, + { + path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T5.js", + code: `return "She sells seashells by the seashore.".replace(/sh/g, "$'sch")`, + expected: "She sells seaells by the seashore.schells by the seaore.schore.", + }, + { + path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T6.js", + code: `return "She sells seashells by the seashore.".replace(/sh/, "sch")`, + expected: "She sells seaschells by the seashore.", + }, + { + path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T7.js", + code: `return "She sells seashells by the seashore.".replace(/sh/, "$$sch")`, + expected: "She sells sea$schells by the seashore.", + }, + { + path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T8.js", + code: `return "She sells seashells by the seashore.".replace(/sh/, "$&sch")`, + expected: "She sells seashschells by the seashore.", + }, + { + path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T9.js", + code: `return "She sells seashells by the seashore.".replace(/sh/, "$\`sch")`, + expected: "She sells seaShe sells seaschells by the seashore.", + }, + { + path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T10.js", + code: `return "She sells seashells by the seashore.".replace(/sh/, "$'sch")`, + expected: "She sells seaells by the seashore.schells by the seashore.", + }, + { + path: "test/built-ins/String/prototype/replace/S15.5.4.11_A3_T1.js", + code: `return "uid=31".replace(/(uid=)(\\d+)/, "$1115")`, + expected: "uid=115", + }, + { + path: "test/built-ins/String/prototype/replace/S15.5.4.11_A3_T2.js", + code: `return "uid=31".replace(/(uid=)(\\d+)/, "$1115")`, + expected: "uid=115", + }, + { + path: "test/built-ins/String/prototype/replace/S15.5.4.11_A3_T3.js", + code: `return "uid=31".replace(/(uid=)(\\d+)/, "$11A15")`, + expected: "uid=1A15", + }, + { + path: "test/built-ins/String/prototype/replace/S15.5.4.11_A5_T1.js", + code: `return "aaaaaaaaaa,aaaaaaaaaaaaaaa".replace(/^(a+)\\1*,\\1+$/, "$1")`, + expected: "aaaaa", + }, +]) + +run("Test262-adapted replaceAll behavior", [ + { + path: "test/built-ins/String/prototype/replaceAll/searchValue-replacer-RegExp-call.js", + code: ` + return [ + "abc abc abc".replaceAll(new RegExp("b", "g"), "z"), + "abc abc abc".replaceAll(new RegExp("b", "gy"), "z"), + "abc abc abc".replaceAll(new RegExp("b", "giy"), "z"), + "No Uppercase!".replaceAll(new RegExp("[A-Z]", "g"), ""), + "No Uppercase?".replaceAll(new RegExp("[A-Z]", "gy"), ""), + "NO UPPERCASE!".replaceAll(new RegExp("[A-Z]", "gy"), ""), + "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "$2-$1"), + "abcabcabcabc".replaceAll(new RegExp("(a(.))", "g"), "$1$2$3"), + "aabacadaeafagahaiajakalamano a azaya".replaceAll(new RegExp("(((((((((((((a(.).).).).).).).).))))))", "g"), "($10)-($12)-($1)"), + "abcba".replaceAll(new RegExp("b", "g"), "$'"), + "abcba".replaceAll(new RegExp("b", "g"), "$\`"), + "abcba".replaceAll(new RegExp("(?b)", "g"), "($)"), + "abcba".replaceAll(new RegExp("(?b)", "g"), "($b)", "g"), "($)"), + "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$)"), + "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($)"), + "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$$$)"), + "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$$)"), + "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$&)"), + "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$1)"), + "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$\`)"), + "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$')"), + "abcabcabcabc".replaceAll(new RegExp("a(?b)(ca)", "g"), "($$)"), + "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($&)"), + ] + `, + expected: [ + "azc azc azc", "abc abc abc", "abc abc abc", "o ppercase!", "o Uppercase?", " UPPERCASE!", + "ca-bbcca-bbc", "abb$3cabb$3cabb$3cabb$3c", + "(aabaca)-(aaba)-(aabacadaea)f(agahai)-(agah)-(agahaiajak)(alaman)-(alam)-(alamano a )azaya", + "acbacaa", "aacabca", "a(b)c(b)a", "a($)bc($)bc", "(abca)bc(abca)bc", + ], + }, + { + path: "test/built-ins/String/prototype/replaceAll/searchValue-empty-string.js", + code: `return ["aab c \\nx".replaceAll("", "_"), "a".replaceAll("", "_")]`, + expected: ["_a_a_b_ _c_ _ _\n_x_", "_a_"], + }, + { + path: "test/built-ins/String/prototype/replaceAll/searchValue-empty-string-this-empty-string.js", + code: `return "".replaceAll("", "abc")`, + expected: "abc", + }, + { + path: "test/built-ins/String/prototype/replaceAll/replaceValue-value-replaces-string.js", + code: `return ["aaab a a aac".replaceAll("aa", "z"), "aaab a a aac".replaceAll("aa", "a"), "aaab a a aac".replaceAll("a", "a"), "aaab a a aac".replaceAll("a", "z")]`, + expected: ["zab a a zc", "aab a a ac", "aaab a a aac", "zzzb z z zzc"], + }, + { + path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024.js", + code: ` + const str = "Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar." + return [str.replaceAll("ninguém", "$"), str.replaceAll("é", "$"), str.replaceAll("é", "$ -"), str.replaceAll("é", "$$$")] + `, + expected: [ + "Ninguém é igual a $. Todo o ser humano é um estranho ímpar.", + "Ningu$m $ igual a ningu$m. Todo o ser humano $ um estranho ímpar.", + "Ningu$ -m $ - igual a ningu$ -m. Todo o ser humano $ - um estranho ímpar.", + "Ningu$$m $$ igual a ningu$$m. Todo o ser humano $$ um estranho ímpar.", + ], + }, + { + path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0024.js", + code: ` + const str = "Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar." + return [str.replaceAll("ninguém", "$$"), str.replaceAll("é", "$$"), str.replaceAll("é", "$$ -"), str.replaceAll("é", "$$&"), str.replaceAll("é", "$$$"), str.replaceAll("é", "$$$$")] + `, + expected: [ + "Ninguém é igual a $. Todo o ser humano é um estranho ímpar.", + "Ningu$m $ igual a ningu$m. Todo o ser humano $ um estranho ímpar.", + "Ningu$ -m $ - igual a ningu$ -m. Todo o ser humano $ - um estranho ímpar.", + "Ningu$&m $& igual a ningu$&m. Todo o ser humano $& um estranho ímpar.", + "Ningu$$m $$ igual a ningu$$m. Todo o ser humano $$ um estranho ímpar.", + "Ningu$$m $$ igual a ningu$$m. Todo o ser humano $$ um estranho ímpar.", + ], + }, + { + path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0026.js", + code: ` + const str = "Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar." + return [str.replaceAll("ninguém", "$&"), str.replaceAll("ninguém", "($&)"), str.replaceAll("é", "($&)"), str.replaceAll("é", "($&) $&")] + `, + expected: [ + "Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar.", + "Ninguém é igual a (ninguém). Todo o ser humano é um estranho ímpar.", + "Ningu(é)m (é) igual a ningu(é)m. Todo o ser humano (é) um estranho ímpar.", + "Ningu(é) ém (é) é igual a ningu(é) ém. Todo o ser humano (é) é um estranho ímpar.", + ], + }, + { + path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0060.js", + code: ` + const str = "Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar." + return [str.replaceAll("ninguém", "$\`"), str.replaceAll("Ninguém", "$\`"), str.replaceAll("ninguém", "($\`)"), str.replaceAll("é", "($\`)")] + `, + expected: [ + "Ninguém é igual a Ninguém é igual a . Todo o ser humano é um estranho ímpar.", + " é igual a ninguém. Todo o ser humano é um estranho ímpar.", + "Ninguém é igual a (Ninguém é igual a ). Todo o ser humano é um estranho ímpar.", + "Ningu(Ningu)m (Ninguém ) igual a ningu(Ninguém é igual a ningu)m. Todo o ser humano (Ninguém é igual a ninguém. Todo o ser humano ) um estranho ímpar.", + ], + }, + { + path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0027.js", + code: ` + const str = "Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar." + return [str.replaceAll("ninguém", "$'"), str.replaceAll(".", "--- $'"), str.replaceAll("é", "($')")] + `, + expected: [ + "Ninguém é igual a . Todo o ser humano é um estranho ímpar.. Todo o ser humano é um estranho ímpar.", + "Ninguém é igual a ninguém--- Todo o ser humano é um estranho ímpar. Todo o ser humano é um estranho ímpar--- ", + "Ningu(m é igual a ninguém. Todo o ser humano é um estranho ímpar.)m ( igual a ninguém. Todo o ser humano é um estranho ímpar.) igual a ningu(m. Todo o ser humano é um estranho ímpar.)m. Todo o ser humano ( um estranho ímpar.) um estranho ímpar.", + ], + }, + { + path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024N.js", + code: ` + const str = "ABC AAA ABC AAA" + return ["$1", "$2", "$3", "$4", "$5", "$6", "$7", "$8", "$9"].map((replacement) => str.replaceAll("ABC", replacement)) + `, + expected: ["$1 AAA $1 AAA", "$2 AAA $2 AAA", "$3 AAA $3 AAA", "$4 AAA $4 AAA", "$5 AAA $5 AAA", "$6 AAA $6 AAA", "$7 AAA $7 AAA", "$8 AAA $8 AAA", "$9 AAA $9 AAA"], + }, + { + path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024NN.js", + code: ` + const str = "aaaaaaaaaaaaaaaa aaaaaaaa aaaaaaaaaaaaaaaa" + return [str.replaceAll("a", "$11"), str.replaceAll("a", "$29")] + `, + expected: [ + "$11$11$11$11$11$11$11$11$11$11$11$11$11$11$11$11 $11$11$11$11$11$11$11$11 $11$11$11$11$11$11$11$11$11$11$11$11$11$11$11$11", + "$29$29$29$29$29$29$29$29$29$29$29$29$29$29$29$29 $29$29$29$29$29$29$29$29 $29$29$29$29$29$29$29$29$29$29$29$29$29$29$29$29", + ], + }, + { + path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x003C.js", + code: `return "aaaaaaaaaaaaaaaa aaaaaaaa aaaaaaaaaaaaaaaa".replaceAll("a", "$<")`, + expected: "$<$<$<$<$<$<$<$<$<$<$<$<$<$<$<$< $<$<$<$<$<$<$<$< $<$<$<$<$<$<$<$<$<$<$<$<$<$<$<$<", + }, +]) + +run("Test262-adapted match behavior", [ + { + path: "test/built-ins/String/prototype/match/S15.5.4.10_A1_T14.js", + code: `const match = "ABBABABAB77BBAA".match(new RegExp("77")); return [match[0], match.index]`, + expected: ["77", 9], + }, + { + path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T2.js", + code: `return "343443444".match(/34/g)`, + expected: ["34", "34", "34"], + }, + { + path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T3.js", + code: `return "123456abcde7890".match(/\\d{1}/g)`, + expected: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], + }, + { + path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T4.js", + code: `return "123456abcde7890".match(/\\d{2}/g)`, + expected: ["12", "34", "56", "78", "90"], + }, + { + path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T5.js", + code: `return "123456abcde7890".match(/\\D{2}/g)`, + expected: ["ab", "cd"], + }, + { + path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T6.js", + code: `const match = "Boston, Mass. 02134".match(/([\\d]{5})([- ]?[\\d]{4})?$/); return [match[0], match[1], match[2] === undefined, match.length, match.index]`, + expected: ["02134", "02134", true, 3, 14], + }, + { + path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T7.js", + code: `return "Boston, Mass. 02134".match(/([\\d]{5})([- ]?[\\d]{4})?$/g)`, + expected: ["02134"], + }, + { + path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T8.js", + code: `const match = "Boston, MA 02134".match(/([\\d]{5})([- ]?[\\d]{4})?$/); return [match[0], match[1], match[2] === undefined, match.length, match.index]`, + expected: ["02134", "02134", true, 3, 11], + }, + { + path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T12.js", + code: `return "Boston, MA 02134".match(/([\\d]{5})([- ]?[\\d]{4})?$/g)`, + expected: ["02134"], + }, +]) + +run("Test262-adapted matchAll behavior", [ + { + path: "test/built-ins/String/prototype/matchAll/regexp-prototype-matchAll-v-u-flag.js", + code: ` + const text = "𠮷a𠮷b𠮷" + const collect = (regex) => { + const matches = text.matchAll(regex) + return matches.map((match) => match[0]).concat(matches.map((match) => match.index)) + } + const empty = text.matchAll(/(?:)/gu) + const complex = "a𠮷b􏿿c".matchAll(/\\P{ASCII}/gu) + return [ + collect(/𠮷/g), + collect(/𠮷/gu), + collect(/\\p{Script=Han}/gu), + collect(/./gu), + empty.map((match) => match[0]).concat(empty.map((match) => match.index)).length, + complex.map((match) => match[0]), + ] + `, + expected: [ + ["𠮷", "𠮷", "𠮷", 0, 3, 6], + ["𠮷", "𠮷", "𠮷", 0, 3, 6], + ["𠮷", "𠮷", "𠮷", 0, 3, 6], + ["𠮷", "a", "𠮷", "b", "𠮷", 0, 2, 3, 5, 6], + 12, + ["𠮷", "􏿿"], + ], + }, +]) + +run("Test262-adapted search behavior", [ + { + path: "test/built-ins/String/prototype/search/S15.5.4.12_A1_T14.js", + code: `return "ABBABABAB77BBAA".search(new RegExp("77"))`, + expected: 9, + }, + { + path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T1.js", + code: `return "test string".search("string")`, + expected: 5, + }, + { + path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T2.js", + code: `return "test string".search("String")`, + expected: -1, + }, + { + path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T3.js", + code: `return "test string".search(/String/i)`, + expected: 5, + }, + { + path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T4.js", + code: `return "one two three four five".search(/Four/)`, + expected: -1, + }, + { + path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T5.js", + code: `return "one two three four five".search(/four/)`, + expected: 14, + }, + { + path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T6.js", + code: `return "test string".search("notexist")`, + expected: -1, + }, + { + path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T7.js", + code: `return "test string probe".search("string pro")`, + expected: 5, + }, + { + path: "test/built-ins/String/prototype/search/S15.5.4.12_A3_T1.js", + code: `const text = "power of the power of the power of the great sword"; return [text.search(/the/), text.search(/the/g)]`, + expected: [9, 9], + }, + { + path: "test/built-ins/String/prototype/search/S15.5.4.12_A3_T2.js", + code: `const text = "power of the power of the power of the great sword"; return [text.search(/of/), text.search(/of/g)]`, + expected: [6, 6], + }, +]) diff --git a/packages/codemode/test/string-search-test262.test.ts b/packages/codemode/test/string-search-test262.test.ts new file mode 100644 index 0000000000..5dcd63c8ba --- /dev/null +++ b/packages/codemode/test/string-search-test262.test.ts @@ -0,0 +1,727 @@ +/* + * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75: + * - test/built-ins/String/prototype/split/call-split-l-0-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/call-split-l-1-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/call-split-l-2-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/call-split-l-3-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/call-split-l-4-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/call-split-l-na-n-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/call-split-l-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/call-split-ll-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/call-split-h-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/call-split-hello-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/call-split-hellothere-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/call-split-o-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/call-split-x-instance-is-string-hello.js + * - test/built-ins/String/prototype/split/call-split-x-instance-is-empty-string.js + * - test/built-ins/String/prototype/split/call-split-4-instance-is-string-one-1-two-2-four-4.js + * - test/built-ins/String/prototype/split/call-split-on-instance-is-string-one-1-two-2-four-4.js + * - test/built-ins/String/prototype/split/call-split-instance-is-string-one-two-three-four-five.js + * - test/built-ins/String/prototype/split/call-split-instance-is-string-one-two-three.js + * - test/built-ins/String/prototype/split/call-split-instance-is-string.js + * - test/built-ins/String/prototype/split/instance-is-string-one-two-three-four-five.js + * - test/built-ins/String/prototype/split/instance-is-string.js + * - test/built-ins/String/prototype/split/separator-colon-instance-is-string-one-1-two-2-four-4.js + * - test/built-ins/String/prototype/split/separator-comma-instance-is-string-one-two-three-four-five.js + * - test/built-ins/String/prototype/split/separator-empty-string-instance-is-string.js + * - test/built-ins/String/prototype/split/call-split-without-arguments-and-instance-is-empty-string.js + * - test/built-ins/String/prototype/split/separator-undef.js + * - test/built-ins/String/prototype/slice/S15.5.4.13_A1_T6.js + * - test/built-ins/String/prototype/slice/S15.5.4.13_A1_T14.js + * - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T1.js + * - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T2.js + * - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T3.js + * - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T4.js + * - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T5.js + * - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T6.js + * - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T7.js + * - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T8.js + * - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T9.js + * - test/built-ins/String/prototype/substring/S15.5.4.15_A1_T6.js + * - test/built-ins/String/prototype/substring/S15.5.4.15_A1_T14.js + * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T1.js + * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T2.js + * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T3.js + * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T4.js + * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T5.js + * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T6.js + * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T7.js + * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T8.js + * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T9.js + * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T10.js + * - test/built-ins/String/prototype/includes/String.prototype.includes_FailMissingLetter.js + * - test/built-ins/String/prototype/includes/String.prototype.includes_SuccessNoLocation.js + * - test/built-ins/String/prototype/includes/String.prototype.includes_FailBadLocation.js + * - test/built-ins/String/prototype/includes/String.prototype.includes_FailLocation.js + * - test/built-ins/String/prototype/includes/String.prototype.includes_Success.js + * - test/built-ins/String/prototype/includes/searchstring-found-with-position.js + * - test/built-ins/String/prototype/includes/searchstring-found-without-position.js + * - test/built-ins/String/prototype/includes/searchstring-not-found-with-position.js + * - test/built-ins/String/prototype/includes/searchstring-not-found-without-position.js + * - test/built-ins/String/prototype/includes/return-false-with-out-of-bounds-position.js + * - test/built-ins/String/prototype/includes/return-true-if-searchstring-is-empty.js + * - test/built-ins/String/prototype/includes/coerced-values-of-position.js + * - test/built-ins/String/prototype/startsWith/searchstring-found-with-position.js + * - test/built-ins/String/prototype/startsWith/searchstring-found-without-position.js + * - test/built-ins/String/prototype/startsWith/searchstring-not-found-with-position.js + * - test/built-ins/String/prototype/startsWith/searchstring-not-found-without-position.js + * - test/built-ins/String/prototype/startsWith/out-of-bounds-position.js + * - test/built-ins/String/prototype/startsWith/return-true-if-searchstring-is-empty.js + * - test/built-ins/String/prototype/startsWith/coerced-values-of-position.js + * - test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success.js + * - test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_2.js + * - test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_3.js + * - test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_4.js + * - test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Fail.js + * - test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Fail_2.js + * - test/built-ins/String/prototype/endsWith/searchstring-found-with-position.js + * - test/built-ins/String/prototype/endsWith/searchstring-found-without-position.js + * - test/built-ins/String/prototype/endsWith/searchstring-not-found-with-position.js + * - test/built-ins/String/prototype/endsWith/searchstring-not-found-without-position.js + * - test/built-ins/String/prototype/endsWith/return-false-if-search-start-is-less-than-zero.js + * - test/built-ins/String/prototype/endsWith/return-true-if-searchstring-is-empty.js + * - test/built-ins/String/prototype/endsWith/coerced-values-of-position.js + * - test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T1.js + * - test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T2.js + * - test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T3.js + * - test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T4.js + * - test/built-ins/String/prototype/indexOf/S15.5.4.7_A3_T1.js + * - test/built-ins/String/prototype/indexOf/S15.5.4.7_A3_T3.js + * - test/built-ins/String/prototype/indexOf/position-tointeger.js + * - test/built-ins/String/prototype/indexOf/searchstring-tostring.js + * - test/built-ins/String/prototype/lastIndexOf/not-a-substring.js + * + * Copyright 2009 the Sputnik authors. All rights reserved. + * Copyright (c) 2014 Ryan Lewis. All rights reserved. + * Copyright (C) 2015 the V8 project authors. All rights reserved. + * Copyright (C) 2016 the V8 project authors. All rights reserved. + * Copyright (C) 2017 Josh Wolfe. All rights reserved. + * Copyright (C) 2020 Leo Balter. All rights reserved. + * Copyright (C) 2026 Garham Lee. All rights reserved. + * Test262 portions are governed by the BSD license in LICENSE.test262. + */ +import { describe, expect, test } from "bun:test" +import { Effect } from "effect" +import { CodeMode } from "../src/index.js" + +const value = async (code: string) => { + const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} })) + if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) + return result.value +} + +const cases = [ + { + path: "test/built-ins/String/prototype/split/call-split-l-0-instance-is-string-hello.js", + code: `const result = "hello".split("l", 0); return [result.length, result[0] === undefined]`, + expected: [0, true], + labels: [ + "The value of __split.length is expected to equal the value of __expected.length", + "The value of __split[0] is expected to equal the value of __expected[0]", + ], + }, + { + path: "test/built-ins/String/prototype/split/call-split-l-1-instance-is-string-hello.js", + code: `const result = "hello".split("l", 1); return [result.length, result[0]]`, + expected: [1, "he"], + labels: [ + "The value of __split.length is expected to equal the value of __expected.length", + "The value of __split[0] is expected to equal the value of __expected[0]", + ], + }, + { + path: "test/built-ins/String/prototype/split/call-split-l-2-instance-is-string-hello.js", + code: `const result = "hello".split("l", 2); return [result.length, result[0], result[1]]`, + expected: [2, "he", ""], + labels: [ + "The value of __split.length is expected to equal the value of __expected.length", + "The value of __split[index] is expected to equal the value of __expected[index]", + "The value of __split[index] is expected to equal the value of __expected[index]", + ], + }, + { + path: "test/built-ins/String/prototype/split/call-split-l-3-instance-is-string-hello.js", + code: `const result = "hello".split("l", 3); return [result.length, result[0], result[1], result[2]]`, + expected: [3, "he", "", "o"], + labels: [ + "The value of __split.length is expected to equal the value of __expected.length", + "The value of __split[index] is expected to equal the value of __expected[index]", + "The value of __split[index] is expected to equal the value of __expected[index]", + "The value of __split[index] is expected to equal the value of __expected[index]", + ], + }, + { + path: "test/built-ins/String/prototype/split/call-split-l-4-instance-is-string-hello.js", + code: `const result = "hello".split("l", 4); return [result.length, result[0], result[1], result[2]]`, + expected: [3, "he", "", "o"], + labels: [ + "The value of __split.length is expected to equal the value of __expected.length", + "The value of __split[index] is expected to equal the value of __expected[index]", + "The value of __split[index] is expected to equal the value of __expected[index]", + "The value of __split[index] is expected to equal the value of __expected[index]", + ], + }, + { + path: "test/built-ins/String/prototype/split/call-split-l-na-n-instance-is-string-hello.js", + code: `const result = "hello".split("l", NaN); return [result.length, result[0] === undefined]`, + expected: [0, true], + labels: [ + "The value of __split.length is expected to equal the value of __expected.length", + "The value of __split[0] is expected to equal the value of __expected[0]", + ], + }, + { + path: "test/built-ins/String/prototype/split/call-split-l-instance-is-string-hello.js", + code: `const result = "hello".split("l"); return [result.length, result[0], result[1], result[2]]`, + expected: [3, "he", "", "o"], + labels: [ + "The value of __split.length is 3", + 'The value of __split[0] is "he"', + 'The value of __split[1] is ""', + 'The value of __split[2] is "o"', + ], + }, + { + path: "test/built-ins/String/prototype/split/call-split-ll-instance-is-string-hello.js", + code: `const result = "hello".split("ll"); return [result.length, result[0], result[1]]`, + expected: [2, "he", "o"], + labels: ["The value of __split.length is 2", 'The value of __split[0] is "he"', 'The value of __split[1] is "o"'], + }, + { + path: "test/built-ins/String/prototype/split/call-split-h-instance-is-string-hello.js", + code: `const result = "hello".split("h"); return [result.length, result[0], result[1]]`, + expected: [2, "", "ello"], + labels: ["The value of __split.length is 2", 'The value of __split[0] is ""', 'The value of __split[1] is "ello"'], + }, + { + path: "test/built-ins/String/prototype/split/call-split-hello-instance-is-string-hello.js", + code: `const result = "hello".split("hello"); return [result.length, result[0], result[1]]`, + expected: [2, "", ""], + labels: ["The value of __split.length is 2", 'The value of __split[0] is ""', 'The value of __split[1] is ""'], + }, + { + path: "test/built-ins/String/prototype/split/call-split-hellothere-instance-is-string-hello.js", + code: `const result = "hello".split("hellothere"); return [result.length, result[0]]`, + expected: [1, "hello"], + labels: ["The value of __split.length is 1", 'The value of __split[0] is "hello"'], + }, + { + path: "test/built-ins/String/prototype/split/call-split-o-instance-is-string-hello.js", + code: `const result = "hello".split("o"); return [result.length, result[0], result[1]]`, + expected: [2, "hell", ""], + labels: ["The value of __split.length is 2", 'The value of __split[0] is "hell"', 'The value of __split[1] is ""'], + }, + { + path: "test/built-ins/String/prototype/split/call-split-x-instance-is-string-hello.js", + code: `const result = "hello".split("x"); return [result.length, result[0]]`, + expected: [1, "hello"], + labels: ["The value of __split.length is 1", 'The value of __split[0] is "hello"'], + }, + { + path: "test/built-ins/String/prototype/split/call-split-x-instance-is-empty-string.js", + code: `const result = "".split("x"); return [result.length, result[0]]`, + expected: [1, ""], + labels: ["The value of __split.length is 1", 'The value of __split[0] is ""'], + }, + { + path: "test/built-ins/String/prototype/split/call-split-4-instance-is-string-one-1-two-2-four-4.js", + code: `const result = "one-1 two-2 four-4".split("-4"); return [result.length, result[0], result[1]]`, + expected: [2, "one-1 two-2 four", ""], + labels: [ + "The value of __split.length is 2", + 'The value of __split[0] is "one-1 two-2 four"', + 'The value of __split[1] is ""', + ], + }, + { + path: "test/built-ins/String/prototype/split/call-split-on-instance-is-string-one-1-two-2-four-4.js", + code: `const result = "one-1 two-2 four-4".split("on"); return [result.length, result[0], result[1]]`, + expected: [2, "", "e-1 two-2 four-4"], + labels: [ + "The value of __split.length is 2", + 'The value of __split[0] is ""', + 'The value of __split[1] is "e-1 two-2 four-4"', + ], + }, + { + path: "test/built-ins/String/prototype/split/call-split-instance-is-string-one-two-three-four-five.js", + code: `const result = "one two three four five".split(" "); return [result.length, ...result]`, + expected: [5, "one", "two", "three", "four", "five"], + labels: [ + "The value of __split.length is 5", 'The value of __split[0] is "one"', 'The value of __split[1] is "two"', + 'The value of __split[2] is "three"', 'The value of __split[3] is "four"', 'The value of __split[4] is "five"', + ], + }, + { + path: "test/built-ins/String/prototype/split/call-split-instance-is-string-one-two-three.js", + code: `const result = "one two three".split(""); return [result[0], result[1], result[11], result[12]]`, + expected: ["o", "n", "e", "e"], + labels: [ + 'The value of __split[0] is "o"', 'The value of __split[1] is "n"', + 'The value of __split[11] is "e"', 'The value of __split[12] is "e"', + ], + }, + { + path: "test/built-ins/String/prototype/split/call-split-instance-is-string.js", + code: `const result = " ".split(" "); return [result.length, result[0], result[1]]`, + expected: [2, "", ""], + labels: ["The value of __split.length is 2", 'The value of __split[0] is ""', 'The value of __split[1] is ""'], + }, + { + path: "test/built-ins/String/prototype/split/instance-is-string-one-two-three-four-five.js", + code: `const result = "one,two,three,four,five".split(); return [result.length, result[0]]`, + expected: [1, "one,two,three,four,five"], + labels: ["The value of __split.length is 1", 'The value of __split[0] is "one,two,three,four,five"'], + }, + { + path: "test/built-ins/String/prototype/split/instance-is-string.js", + code: `const result = " ".split(); return [result.length, result[0]]`, + expected: [1, " "], + labels: ["The value of __split.length is 1", 'The value of __split[0] is " "'], + }, + { + path: "test/built-ins/String/prototype/split/separator-colon-instance-is-string-one-1-two-2-four-4.js", + code: `const result = "one-1,two-2,four-4".split(":"); return [result.length, result[0]]`, + expected: [1, "one-1,two-2,four-4"], + labels: ["The value of __split.length is 1", 'The value of __split[0] is "one-1,two-2,four-4"'], + }, + { + path: "test/built-ins/String/prototype/split/separator-comma-instance-is-string-one-two-three-four-five.js", + code: `const result = "one,two,three,four,five".split(","); return [result.length, ...result]`, + expected: [5, "one", "two", "three", "four", "five"], + labels: [ + "The value of __split.length is 5", + 'The value of __split[0] is "one"', + 'The value of __split[1] is "two"', + 'The value of __split[2] is "three"', + 'The value of __split[3] is "four"', + 'The value of __split[4] is "five"', + ], + }, + { + path: "test/built-ins/String/prototype/split/separator-empty-string-instance-is-string.js", + code: `const result = " ".split(""); return [result.length, result[0]]`, + expected: [1, " "], + labels: ["The value of __split.length is 1", 'The value of __split[0] is " "'], + }, + { + path: "test/built-ins/String/prototype/split/call-split-without-arguments-and-instance-is-empty-string.js", + code: `const result = "".split(); return [result.length, result[0]]`, + expected: [1, ""], + labels: ["The value of __split.length is 1", 'The value of __split[0] is ""'], + }, + { + path: "test/built-ins/String/prototype/split/separator-undef.js", + code: `const result = "undefined is not a function".split(); return [Array.isArray(result), result.length, result[0]]`, + expected: [true, 1, "undefined is not a function"], + labels: ["implicit separator, result is array", "implicit separator, result.length", "implicit separator, [0] is the same string"], + }, + { + path: "test/built-ins/String/prototype/slice/S15.5.4.13_A1_T6.js", + code: `return ["undefined".slice(undefined, 3)]`, + expected: ["und"], + labels: ['#1: new String("undefined").slice(x,3) === "und"'], + }, + { + path: "test/built-ins/String/prototype/slice/S15.5.4.13_A1_T14.js", + code: `return ["report".slice(undefined)]`, + expected: ["report"], + labels: ['#1: "report".slice(function(){}()) === "report"'], + }, + { + path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T1.js", + code: `return [typeof "this is a string object".slice()]`, + expected: ["string"], + labels: ['#1: typeof __string.slice() === "string"'], + }, + { + path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T2.js", + code: `return ["this is a string object".slice(NaN, Infinity)]`, + expected: ["this is a string object"], + labels: ['#1: __string.slice(NaN, Infinity) === "this is a string object"'], + }, + { + path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T3.js", + code: `return ["".slice(1, 0)]`, + expected: [""], + labels: ['#1: __string.slice(1,0) === ""'], + }, + { + path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T4.js", + code: `return ["this is a string object".slice(Infinity, NaN)]`, + expected: [""], + labels: ['#1: __string.slice(Infinity, NaN) === ""'], + }, + { + path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T5.js", + code: `return ["this is a string object".slice(Infinity, Infinity)]`, + expected: [""], + labels: ['#1: __string.slice(Infinity, Infinity) === ""'], + }, + { + path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T6.js", + code: `return ["this is a string object".slice(-0.01, 0)]`, + expected: [""], + labels: ['#1: __string.slice(-0.01,0) === ""'], + }, + { + path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T7.js", + code: `const text = "this is a string object"; return [text.slice(text.length, text.length)]`, + expected: [""], + labels: ['#1: __string.slice(__string.length, __string.length) === ""'], + }, + { + path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T8.js", + code: `const text = "this is a string object"; return [text.slice(text.length + 1, 0)]`, + expected: [""], + labels: ['#1: __string.slice(__string.length+1, 0) === ""'], + }, + { + path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T9.js", + code: `return ["this is a string object".slice(-Infinity, -Infinity)]`, + expected: [""], + labels: ['#1: __string.slice(-Infinity, -Infinity) === ""'], + }, + { + path: "test/built-ins/String/prototype/substring/S15.5.4.15_A1_T6.js", + code: `return ["undefined".substring(undefined, 3)]`, + expected: ["und"], + labels: ['#1: new String("undefined").substring(x,3) === "und"'], + }, + { + path: "test/built-ins/String/prototype/substring/S15.5.4.15_A1_T14.js", + code: `return ["report".substring(undefined)]`, + expected: ["report"], + labels: ['#1: "report".substring(function(){}()) === "report"'], + }, + { + path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T1.js", + code: `return [typeof "this is a string object".substring()]`, + expected: ["string"], + labels: ['#1: typeof __string.substring() === "string"'], + }, + { + path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T2.js", + code: `return ["this is a string object".substring(NaN, Infinity)]`, + expected: ["this is a string object"], + labels: ['#1: __string.substring(NaN, Infinity) === "this is a string object"'], + }, + { + path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T3.js", + code: `return ["".substring(1, 0)]`, + expected: [""], + labels: ['#1: __string.substring(1,0) === ""'], + }, + { + path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T4.js", + code: `return ["this is a string object".substring(Infinity, NaN)]`, + expected: ["this is a string object"], + labels: ['#1: __string.substring(Infinity, NaN) === "this is a string object"'], + }, + { + path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T5.js", + code: `return ["this is a string object".substring(Infinity, Infinity)]`, + expected: [""], + labels: ['#1: __string.substring(Infinity, Infinity) === ""'], + }, + { + path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T6.js", + code: `return ["this is a string object".substring(-0.01, 0)]`, + expected: [""], + labels: ['#1: __string.substring(-0.01,0) === ""'], + }, + { + path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T7.js", + code: `const text = "this is a string object"; return [text.substring(text.length, text.length)]`, + expected: [""], + labels: ['#1: __string.substring(__string.length, __string.length) === ""'], + }, + { + path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T8.js", + code: `const text = "this is a string object"; return [text.substring(text.length + 1, 0)]`, + expected: ["this is a string object"], + labels: ['#1: __string.substring(__string.length+1, 0) === "this is a string object"'], + }, + { + path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T9.js", + code: `return ["this is a string object".substring(-Infinity, -Infinity)]`, + expected: [""], + labels: ['#1: __string.substring(-Infinity, -Infinity) === ""'], + }, + { + path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T10.js", + code: `return ["this_is_a_string object".substring(0, 8)]`, + expected: ["this_is_"], + labels: ['#1: __string.substring(0,8) === "this_is_"'], + }, + { + path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailMissingLetter.js", + code: `return ["word".includes("a", 0)]`, expected: [false], labels: ['"word".includes("a", 0)'], + }, + { + path: "test/built-ins/String/prototype/includes/String.prototype.includes_SuccessNoLocation.js", + code: `return ["word".includes("w")]`, expected: [true], labels: ['"word".includes("w")'], + }, + { + path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailBadLocation.js", + code: `return ["word".includes("w", 5)]`, expected: [false], labels: ['"word".includes("w", 5)'], + }, + { + path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailLocation.js", + code: `return ["word".includes("o", 3)]`, expected: [false], labels: ['"word".includes("o", 3)'], + }, + { + path: "test/built-ins/String/prototype/includes/String.prototype.includes_Success.js", + code: `return ["word".includes("w", 0)]`, expected: [true], labels: ['"word".includes("w", 0)'], + }, + { + path: "test/built-ins/String/prototype/includes/searchstring-found-with-position.js", + code: `const text = "The future is cool!"; return [text.includes("The future", 0), text.includes(" is ", 1), text.includes("cool!", 10)]`, + expected: [true, true, true], + labels: [ + 'Returns true for str.includes("The future", 0)', + 'Returns true for str.includes(" is ", 1)', + 'Returns true for str.includes("cool!", 10)', + ], + }, + { + path: "test/built-ins/String/prototype/includes/searchstring-found-without-position.js", + code: `const text = "The future is cool!"; return [text.includes("The future"), text.includes("is cool!"), text.includes(text)]`, + expected: [true, true, true], + labels: [ + 'Returns true for str.includes("The future")', + 'Returns true for str.includes("is cool!")', + "Returns true for str.includes(str)", + ], + }, + { + path: "test/built-ins/String/prototype/includes/searchstring-not-found-with-position.js", + code: `const text = "The future is cool!"; return [text.includes("The future", 1), text.includes(text, 1)]`, + expected: [false, false], + labels: ['Returns false on str.includes("The future", 1)', "Returns false on str.includes(str, 1)"], + }, + { + path: "test/built-ins/String/prototype/includes/searchstring-not-found-without-position.js", + code: `const text = "The future is cool!"; return [text.includes("Flash"), text.includes("FUTURE")]`, + expected: [false, false], labels: ["Flash if not included", "includes is case sensitive"], + }, + { + path: "test/built-ins/String/prototype/includes/return-false-with-out-of-bounds-position.js", + code: `const text = "The future is cool!"; return [ + text.includes("!", text.length + 1), text.includes("!", 100), text.includes("!", Infinity), text.includes("!", text.length), + ]`, + expected: [false, false, false, false], + labels: [ + 'str.includes("!", str.length + 1) returns false', 'str.includes("!", 100) returns false', + 'str.includes("!", Infinity) returns false', 'str.includes("!", str.length) returns false', + ], + }, + { + path: "test/built-ins/String/prototype/includes/return-true-if-searchstring-is-empty.js", + code: `const text = "The future is cool!"; return [text.includes("", text.length), text.includes(""), text.includes("", Infinity)]`, + expected: [true, true, true], + labels: ['str.includes("", str.length) returns true', 'str.includes("") returns true', 'str.includes("", Infinity) returns true'], + }, + { + path: "test/built-ins/String/prototype/includes/coerced-values-of-position.js", + code: `const text = "The future is cool!"; return [ + text.includes("The future", NaN), text.includes("The future", undefined), text.includes("The future", 0.4), + text.includes("The future", -1), text.includes("The future", 1.4), + ]`, + expected: [true, true, true, true, false], + labels: ["NaN coerced to 0", "undefined coerced to 0", "0.4 coerced to 0", "negative position", "1.4 coerced to 1"], + }, + { + path: "test/built-ins/String/prototype/startsWith/searchstring-found-with-position.js", + code: `const text = "The future is cool!"; return [text.startsWith("The future", 0), text.startsWith("future", 4), text.startsWith(" is cool!", 10)]`, + expected: [true, true, true], + labels: [ + 'str.startsWith("The future", 0) === true', 'str.startsWith("future", 4) === true', + 'str.startsWith(" is cool!", 10) === true', + ], + }, + { + path: "test/built-ins/String/prototype/startsWith/searchstring-found-without-position.js", + code: `const text = "The future is cool!"; return [text.startsWith("The "), text.startsWith("The future"), text.startsWith(text)]`, + expected: [true, true, true], + labels: ['str.startsWith("The ") === true', 'str.startsWith("The future") === true', "str.startsWith(str) === true"], + }, + { + path: "test/built-ins/String/prototype/startsWith/searchstring-not-found-with-position.js", + code: `const text = "The future is cool!"; return [text.startsWith("The future", 1), text.startsWith(text, 1)]`, + expected: [false, false], + labels: ['str.startsWith("The future", 1) === false', "str.startsWith(str, 1) === false"], + }, + { + path: "test/built-ins/String/prototype/startsWith/searchstring-not-found-without-position.js", + code: `const text = "The future is cool!"; return [text.startsWith("Flash"), text.startsWith("THE FUTURE"), text.startsWith("future is cool!")]`, + expected: [false, false, false], + labels: ['str.startsWith("Flash") === false', "startsWith is case sensitive", 'str.startsWith("future is cool!") === false'], + }, + { + path: "test/built-ins/String/prototype/startsWith/out-of-bounds-position.js", + code: `const text = "The future is cool!"; return [ + text.startsWith("!", text.length), text.startsWith("!", 100), text.startsWith("!", Infinity), + text.startsWith("The future", -1), text.startsWith("The future", -Infinity), + ]`, + expected: [false, false, false, true, true], + labels: [ + 'str.startsWith("!", str.length) returns false', 'str.startsWith("!", 100) returns false', + 'str.startsWith("!", Infinity) returns false', "position argument < 0 will search from the start of the string (-1)", + "position argument < 0 will search from the start of the string (-Infinity)", + ], + }, + { + path: "test/built-ins/String/prototype/startsWith/return-true-if-searchstring-is-empty.js", + code: `const text = "The future is cool!"; return [text.startsWith(""), text.startsWith("", text.length), text.startsWith("", Infinity)]`, + expected: [true, true, true], + labels: ['str.startsWith("") returns true', 'str.startsWith("", str.length) returns true', 'str.startsWith("", Infinity) returns true'], + }, + { + path: "test/built-ins/String/prototype/startsWith/coerced-values-of-position.js", + code: `const text = "The future is cool!"; return [ + text.startsWith("The future", NaN), text.startsWith("The future", undefined), + text.startsWith("The future", 0.4), text.startsWith("The future", 1.4), + ]`, + expected: [true, true, true, false], + labels: ["NaN coerced to 0", "undefined coerced to 0", "0.4 coerced to 0", "1.4 coerced to 1"], + }, + { + path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success.js", + code: `return ["word".endsWith("d")]`, expected: [true], labels: ['"word".endsWith("d")'], + }, + { + path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_2.js", + code: `return ["word".endsWith("d", 4)]`, expected: [true], labels: ['"word".endsWith("d", 4)'], + }, + { + path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_3.js", + code: `return ["word".endsWith("d", 25)]`, expected: [true], labels: ['"word".endsWith("d", 25)'], + }, + { + path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_4.js", + code: `return ["word".endsWith("r", 3)]`, expected: [true], labels: ['"word".endsWith("r", 3)'], + }, + { + path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Fail.js", + code: `return ["word".endsWith("r")]`, expected: [false], labels: ['"word".endsWith("r")'], + }, + { + path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Fail_2.js", + code: `return ["word".endsWith("d", 3)]`, expected: [false], labels: ['"word".endsWith("d", 3)'], + }, + { + path: "test/built-ins/String/prototype/endsWith/searchstring-found-with-position.js", + code: `const text = "The future is cool!"; return [text.endsWith("The future", 10), text.endsWith("future", 10), text.endsWith(" is cool!", text.length)]`, + expected: [true, true, true], + labels: [ + 'str.endsWith("The future", 10) === true', 'str.endsWith("future", 10) === true', + 'str.endsWith(" is cool!", str.length) === true', + ], + }, + { + path: "test/built-ins/String/prototype/endsWith/searchstring-found-without-position.js", + code: `const text = "The future is cool!"; return [text.endsWith("cool!"), text.endsWith("!"), text.endsWith(text)]`, + expected: [true, true, true], + labels: ['str.endsWith("cool!") === true', 'str.endsWith("!") === true', "str.endsWith(str) === true"], + }, + { + path: "test/built-ins/String/prototype/endsWith/searchstring-not-found-with-position.js", + code: `const text = "The future is cool!"; return [text.endsWith("is cool!", text.length - 1), text.endsWith("!", 1)]`, + expected: [false, false], + labels: ['str.endsWith("is cool!", str.length - 1) === false', 'str.endsWith("!", 1) === false'], + }, + { + path: "test/built-ins/String/prototype/endsWith/searchstring-not-found-without-position.js", + code: `const text = "The future is cool!"; return [text.endsWith("is Flash!"), text.endsWith("IS COOL!"), text.endsWith("The future")]`, + expected: [false, false, false], + labels: ['str.endsWith("is Flash!") === false', "endsWith is case sensitive", 'str.endsWith("The future") === false'], + }, + { + path: "test/built-ins/String/prototype/endsWith/return-false-if-search-start-is-less-than-zero.js", + code: `return ["web".endsWith("w", 0), "Bob".endsWith(" Bob")]`, + expected: [false, false], + labels: ['"web".endsWith("w", 0) returns false', '"Bob".endsWith(" Bob") returns false'], + }, + { + path: "test/built-ins/String/prototype/endsWith/return-true-if-searchstring-is-empty.js", + code: `const text = "The future is cool!"; return [ + text.endsWith(""), text.endsWith("", text.length), text.endsWith("", Infinity), + text.endsWith("", -1), text.endsWith("", -Infinity), + ]`, + expected: [true, true, true, true, true], + labels: [ + 'str.endsWith("") returns true', 'str.endsWith("", str.length) returns true', 'str.endsWith("", Infinity) returns true', + 'str.endsWith("", -1) returns true', 'str.endsWith("", -Infinity) returns true', + ], + }, + { + path: "test/built-ins/String/prototype/endsWith/coerced-values-of-position.js", + code: `const text = "The future is cool!"; return [ + text.endsWith("", NaN), text.endsWith("", undefined), text.endsWith("The future", 10.4), + ]`, + expected: [true, true, true], + labels: ["NaN coerced to 0", "undefined coerced to 0", "10.4 coerced to 10"], + }, + { + path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T1.js", + code: `return ["abcd".indexOf("abcdab")]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab")===-1'], + }, + { + path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T2.js", + code: `return ["abcd".indexOf("abcdab", 0)]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab",0)===-1'], + }, + { + path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T3.js", + code: `return ["abcd".indexOf("abcdab", 99)]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab",99)===-1'], + }, + { + path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T4.js", + code: `return ["abcd".indexOf("abcdab", NaN)]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab",NaN)===-1'], + }, + { + path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A3_T1.js", + code: `return ["$$abcdabcd".indexOf("ab", NaN)]`, expected: [2], labels: ['#1: "$$abcdabcd".indexOf("ab",NaN)===2'], + }, + { + path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A3_T3.js", + code: `return ["$$abcdabcd".indexOf("ab", -Infinity)]`, expected: [2], labels: ['#1: "$$abcdabcd".indexOf("ab", function(){return -Infinity;}())===2'], + }, + { + path: "test/built-ins/String/prototype/indexOf/position-tointeger.js", + code: `return [ + "aaaa".indexOf("aa", 0), "aaaa".indexOf("aa", 1), "aaaa".indexOf("aa", -0.9), + "aaaa".indexOf("aa", 0.9), "aaaa".indexOf("aa", 1.9), "aaaa".indexOf("aa", NaN), + "aaaa".indexOf("aa", Infinity), "aaaa".indexOf("aa", undefined), + "aaaa".indexOf("aa", 2), "aaaa".indexOf("aa", 2.9), + ]`, + expected: [0, 1, 0, 0, 1, 0, -1, 0, 2, 2], + labels: [ + "position 0", "position 1", "ToInteger: truncate towards 0 (-0.9)", "ToInteger: truncate towards 0 (0.9)", + "ToInteger: truncate towards 0 (1.9)", "ToInteger: NaN => 0", "position Infinity", + "ToInteger: undefined => NaN => 0", "position 2", "ToInteger: truncate towards 0 (2.9)", + ], + }, + { + path: "test/built-ins/String/prototype/indexOf/searchstring-tostring.js", + code: `return ["foo".indexOf(""), "__foo__".indexOf("foo")]`, + expected: [0, 2], labels: ['"foo".indexOf("")', '"__foo__".indexOf("foo")'], + }, + { + path: "test/built-ins/String/prototype/lastIndexOf/not-a-substring.js", + code: `return ["abc".lastIndexOf("d")]`, + expected: [-1], + labels: ["String.prototype.lastIndexOf returns -1 when searchString is shorter than this and searchString is not a substring of this."], + }, +] as const + +describe("Test262-adapted String search and extraction behavior", () => { + for (const item of cases) { + test(item.path, async () => { + const actual = await value(item.code) + if (!Array.isArray(actual)) throw new Error(`expected assertion values for ${item.path}`) + expect(actual.length, "adapted assertion count").toBe(item.expected.length) + item.expected.forEach((expected, index) => expect(actual[index], item.labels[index]!).toEqual(expected)) + }) + } +}) diff --git a/packages/codemode/test/tool-paths.test.ts b/packages/codemode/test/tool-paths.test.ts new file mode 100644 index 0000000000..c92739c5df --- /dev/null +++ b/packages/codemode/test/tool-paths.test.ts @@ -0,0 +1,164 @@ +import { describe, expect, test } from "bun:test" +import { Effect, Schema } from "effect" +import { CodeMode, Tool } from "../src/index.js" + +const echo = (description: string, result: string) => + Tool.make({ + description, + input: Schema.Struct({}), + output: Schema.String, + run: () => Effect.succeed(result), + }) + +const value = async (runtime: CodeMode.Runtime, code: string) => { + const result = await Effect.runPromise(runtime.execute(code)) + if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) + return result.value +} + +const failure = async (runtime: CodeMode.Runtime, code: string) => { + const result = await Effect.runPromise(runtime.execute(code)) + if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`) + return result.error +} + +describe("dotted tool names", () => { + const runtime = CodeMode.make({ tools: { api: { "issues.list": echo("List issues", "listed") } } }) + + test("a dotted name becomes nested namespaces in the catalog", () => { + const catalog = runtime.catalog() + expect(catalog).toHaveLength(1) + expect(catalog[0]?.path).toBe("api.issues.list") + expect(catalog[0]?.signature).toStartWith("tools.api.issues.list(input:") + expect(runtime.instructions()).toContain("tools.api.issues.list(input:") + }) + + test("the advertised dotted path is executable", async () => { + expect(await value(runtime, `return await tools.api.issues.list({})`)).toBe("listed") + }) + + test("bracket access with a dotted segment spells the same canonical path", async () => { + expect(await value(runtime, `return await tools.api["issues.list"]({})`)).toBe("listed") + expect(await value(runtime, `return await tools["api.issues"].list({})`)).toBe("listed") + }) + + test("intermediate segments enumerate like ordinary namespaces", async () => { + expect(await value(runtime, `return [Object.keys(tools.api), Object.keys(tools.api.issues)]`)).toEqual([ + ["issues"], + ["list"], + ]) + expect(await value(runtime, `return Object.keys(tools["api.issues"])`)).toEqual(["list"]) + }) + + test("a top-level dotted name nests from the root", async () => { + const flat = CodeMode.make({ tools: { "issues.list": echo("List issues", "flat") } }) + expect(flat.catalog()[0]?.path).toBe("issues.list") + expect(await value(flat, `return await tools.issues.list({})`)).toBe("flat") + }) +}) + +describe("callable namespaces", () => { + const runtime = CodeMode.make({ + tools: { issues: echo("All issues", "all"), "issues.list": echo("List issues", "list") }, + }) + + test("a path can hold a tool and child tools at once", async () => { + expect(await value(runtime, `return await tools.issues({})`)).toBe("all") + expect(await value(runtime, `return await tools.issues.list({})`)).toBe("list") + expect(runtime.catalog().map((tool) => tool.path)).toEqual(["issues", "issues.list"]) + }) + + test("a callable namespace enumerates its children", async () => { + expect(await value(runtime, `return Object.keys(tools.issues)`)).toEqual(["list"]) + }) + + test("search returns executable paths for both", async () => { + const result = await value(runtime, `return search({ query: "", namespace: "issues" })`) + expect((result as { items: Array<{ path: string }> }).items.map((item) => item.path)).toEqual([ + "tools.issues", + "tools.issues.list", + ]) + const exact = await value(runtime, `return search({ query: "tools.issues.list" })`) + expect((exact as { items: Array<{ path: string }> }).items.map((item) => item.path)).toEqual(["tools.issues.list"]) + }) + + test("an unknown child under a callable tool is an UnknownTool error", async () => { + const diagnostic = await failure(runtime, `return await tools.issues.missing({})`) + expect(diagnostic.kind).toBe("UnknownTool") + expect(diagnostic.message).toContain("Unknown tool 'issues.missing'") + }) + + test("a namespace without its own definition stays non-callable", async () => { + const nested = CodeMode.make({ tools: { "issues.list": echo("List issues", "list") } }) + const diagnostic = await failure(nested, `return await tools.issues({})`) + expect(diagnostic.kind).toBe("UnknownTool") + expect(diagnostic.message).toContain("Tool 'issues' is not callable") + }) +}) + +describe("blocked member names on tool paths", () => { + const runtime = CodeMode.make({ + tools: { + prototype: echo("Prototype tool", "proto"), + "issues.constructor": echo("Constructor tool", "ctor"), + nested: { ["__proto__"]: echo("Proto tool", "dunder") }, + }, + }) + + test("tools may use blocked member names because path segments never touch real properties", async () => { + expect(runtime.catalog().map((tool) => tool.path)).toEqual(["prototype", "issues.constructor", "nested.__proto__"]) + expect(await value(runtime, `return await tools.prototype({})`)).toBe("proto") + expect(await value(runtime, `return await tools.issues.constructor({})`)).toBe("ctor") + expect(await value(runtime, `return await tools["issues.constructor"]({})`)).toBe("ctor") + expect(await value(runtime, `return await tools.nested.__proto__({})`)).toBe("dunder") + expect(await value(runtime, `return Object.keys(tools.issues)`)).toEqual(["constructor"]) + }) + + test("a literal __proto__ key cannot poison a namespace into a fake definition", async () => { + const poisoned = CodeMode.make({ + tools: { ns: { "__proto__": echo("Hidden", "hidden"), real: echo("Real tool", "real") } }, + }) + expect(poisoned.catalog().map((tool) => tool.path)).toEqual(["ns.real"]) + expect(await value(poisoned, `return await tools.ns.real({})`)).toBe("real") + }) + + test("blocked member access on data values stays blocked", async () => { + const diagnostic = await failure(runtime, `const x = {}; return x.constructor`) + expect(diagnostic.message).toContain("constructor") + expect(Object.keys(Object.prototype)).toEqual([]) + }) +}) + +describe("empty segments", () => { + test("tool names with empty segments are rejected at make", () => { + for (const name of ["", "a..b", "trail.", ".lead"]) { + expect(() => CodeMode.make({ tools: { [name]: echo("Bad", "bad") } })).toThrow("empty segment") + } + }) +}) + +describe("canonical path collisions", () => { + test("the last definition supplied for a canonical path wins", async () => { + const runtime = CodeMode.make({ + tools: { "issues.list": echo("First", "first"), issues: { list: echo("Second", "second") } }, + }) + expect(await value(runtime, `return await tools.issues.list({})`)).toBe("second") + expect(runtime.catalog()).toHaveLength(1) + expect(runtime.catalog()[0]?.description).toBe("Second") + }) + + test("overriding one path keeps sibling tools from both shapes", async () => { + const runtime = CodeMode.make({ + tools: { + "issues.list": echo("First list", "first"), + issues: { list: echo("Second list", "second"), get: echo("Get issue", "got") }, + "issues.close": echo("Close issue", "closed"), + }, + }) + // Catalog order follows first appearance of each canonical path. + expect(runtime.catalog().map((tool) => tool.path)).toEqual(["issues.list", "issues.get", "issues.close"]) + expect(await value(runtime, `return await tools.issues.list({})`)).toBe("second") + expect(await value(runtime, `return await tools.issues.get({})`)).toBe("got") + expect(await value(runtime, `return await tools.issues.close({})`)).toBe("closed") + }) +}) diff --git a/packages/codemode/tsconfig.json b/packages/codemode/tsconfig.json index fe5c4d217b..0cbc049d87 100644 --- a/packages/codemode/tsconfig.json +++ b/packages/codemode/tsconfig.json @@ -2,6 +2,7 @@ "$schema": "https://json.schemastore.org/tsconfig", "extends": "@tsconfig/bun/tsconfig.json", "compilerOptions": { - "noUncheckedIndexedAccess": false + "noUncheckedIndexedAccess": false, + "noUnusedLocals": true } } diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 5023021661..c10a9aff34 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.18.11", + "version": "1.18.3", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/app/src/app.tsx b/packages/console/app/src/app.tsx index 1f1d0066ec..99b1010767 100644 --- a/packages/console/app/src/app.tsx +++ b/packages/console/app/src/app.tsx @@ -9,6 +9,7 @@ import "./app.css" import { LanguageProvider } from "~/context/language" import { I18nProvider, useI18n } from "~/context/i18n" import { strip } from "~/lib/language" +import { DesktopPromo } from "~/component/desktop-promo" function AppMeta() { const i18n = useI18n() @@ -33,6 +34,7 @@ export default function App() { {props.children} + diff --git a/packages/console/app/src/component/desktop-promo.tsx b/packages/console/app/src/component/desktop-promo.tsx index 504088e3c7..165f4fc690 100644 --- a/packages/console/app/src/component/desktop-promo.tsx +++ b/packages/console/app/src/component/desktop-promo.tsx @@ -7,17 +7,12 @@ import { useI18n } from "~/context/i18n" import { useLanguage } from "~/context/language" import { strip } from "~/lib/language" -const DISMISSED_COOKIE = "desktop_promo_dismissed" - export function DesktopPromo() { const i18n = useI18n() const language = useLanguage() const location = useLocation() + const [visible, setVisible] = createSignal(true) const request = getRequestEvent()?.request - const cookie = request?.headers.get("cookie") ?? (typeof document === "object" ? document.cookie : "") - const [visible, setVisible] = createSignal( - !cookie.split(";").some((value) => value.trim() === `${DISMISSED_COOKIE}=1`), - ) const hostname = request ? new URL(request.url).hostname : typeof window === "object" ? window.location.hostname : "" const primaryHost = hostname === "opencode.ai" || hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" @@ -41,14 +36,7 @@ export function DesktopPromo() { -