diff --git a/.changeset/README.md b/.changeset/README.md index fa4dbbf5508..5add9669349 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -4,7 +4,7 @@ This directory contains changeset files used to track changes for the next relea ## Adding a changeset -When making a user-facing change, run: +When making a user-facing change, prefer one concise changeset per PR, grouping related changes when possible. Run: ```sh bunx changeset add diff --git a/.changeset/align-slash-command-selection.md b/.changeset/align-slash-command-selection.md new file mode 100644 index 00000000000..d8fd2c43c16 --- /dev/null +++ b/.changeset/align-slash-command-selection.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Ensure Enter and Tab execute the slash command highlighted in the VS Code command picker. diff --git a/.changeset/calm-powershell-alerts.md b/.changeset/calm-powershell-alerts.md deleted file mode 100644 index ac702a7abc8..00000000000 --- a/.changeset/calm-powershell-alerts.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@kilocode/cli": patch -"kilo-code": patch ---- - -Run Windows PowerShell tool commands without `-EncodedCommand` to reduce antivirus false positives. diff --git a/.changeset/chat-fim-fallback.md b/.changeset/chat-fim-fallback.md deleted file mode 100644 index 73ff08611c5..00000000000 --- a/.changeset/chat-fim-fallback.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"kilo-code": patch -"@kilocode/kilo-gateway": patch ---- - -Use the matching FIM model for chat autocomplete when Next Edit is selected. diff --git a/.changeset/create-plan-dir.md b/.changeset/create-plan-dir.md deleted file mode 100644 index 4f8def38d8a..00000000000 --- a/.changeset/create-plan-dir.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Create the default `.kilo/plans` directory automatically when Plan mode starts. diff --git a/.changeset/edit-preview-agent-manager.md b/.changeset/edit-preview-agent-manager.md new file mode 100644 index 00000000000..65de1eab4ae --- /dev/null +++ b/.changeset/edit-preview-agent-manager.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Open edit, write, and patch changes in the Agent Manager side panel instead of a new editor tab. Clicking a tool or file name expands the change inline, while the open-diff button shows the full change in the Agent Manager panel, or in a diff tab outside Agent Manager. Multi-file patches now open every changed file, each sized to its own diff, and the panel keeps the unified or split view you last selected. diff --git a/.changeset/enable-background-subagents.md b/.changeset/enable-background-subagents.md new file mode 100644 index 00000000000..2492c3689d9 --- /dev/null +++ b/.changeset/enable-background-subagents.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Enable background subagents by default, including automatic completion notifications and foreground-to-background promotion. Running background agents now appear in a collapsible strip in the chat header, so they stay visible after the task card scrolls away and can be opened from there. diff --git a/.changeset/fix-max-steps-prefill.md b/.changeset/fix-max-steps-prefill.md new file mode 100644 index 00000000000..af5f6c67a6d --- /dev/null +++ b/.changeset/fix-max-steps-prefill.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Prevent provider errors when agents reach their step limit by sending the final summary instruction as user input instead of an assistant prefill. diff --git a/.changeset/jetbrains-advanced-logging.md b/.changeset/jetbrains-advanced-logging.md new file mode 100644 index 00000000000..946e7139d2c --- /dev/null +++ b/.changeset/jetbrains-advanced-logging.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": minor +--- + +Add an Advanced settings page with a Logging section to configure diagnostic log level and message previews, reveal the log in your file manager, and download the backend log in remote development. diff --git a/.changeset/jetbrains-clickable-prompt-mentions.md b/.changeset/jetbrains-clickable-prompt-mentions.md deleted file mode 100644 index afbcb1d4231..00000000000 --- a/.changeset/jetbrains-clickable-prompt-mentions.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Highlight rendered prompt file mentions and open them on click. diff --git a/.changeset/jetbrains-empty-mention-completion.md b/.changeset/jetbrains-empty-mention-completion.md deleted file mode 100644 index 3fb5501937c..00000000000 --- a/.changeset/jetbrains-empty-mention-completion.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Show JetBrains file mention suggestions immediately for empty `@` mentions and keep the completion popup stable while typing quickly. diff --git a/.changeset/jetbrains-file-mention-parity.md b/.changeset/jetbrains-file-mention-parity.md deleted file mode 100644 index 0f989b16040..00000000000 --- a/.changeset/jetbrains-file-mention-parity.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Hide raw file contents from mentioned files in JetBrains chat messages. diff --git a/.changeset/jetbrains-git-changes-data-url.md b/.changeset/jetbrains-git-changes-data-url.md deleted file mode 100644 index 5085f5ee663..00000000000 --- a/.changeset/jetbrains-git-changes-data-url.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Fix @git-changes mentions causing JetBrains chat sessions to fail. diff --git a/.changeset/jetbrains-log-pattern.md b/.changeset/jetbrains-log-pattern.md new file mode 100644 index 00000000000..4131f083d3b --- /dev/null +++ b/.changeset/jetbrains-log-pattern.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Use `kilo.log` as the active JetBrains plugin diagnostic log, rotate old logs to `kilo.log.0` and `kilo.log.1`, and delete legacy `kilo-dev.log*` files when logging starts. diff --git a/.changeset/jetbrains-mention-icons-priority.md b/.changeset/jetbrains-mention-icons-priority.md deleted file mode 100644 index bf968c1855a..00000000000 --- a/.changeset/jetbrains-mention-icons-priority.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Show file type icons and keep predefined mentions first in JetBrains mention completions. diff --git a/.changeset/jetbrains-permission-diff.md b/.changeset/jetbrains-permission-diff.md new file mode 100644 index 00000000000..b7ee01e707b --- /dev/null +++ b/.changeset/jetbrains-permission-diff.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": minor +--- + +Show proposed file changes in permission prompts before approval. diff --git a/.changeset/jetbrains-prompt-completion.md b/.changeset/jetbrains-prompt-completion.md deleted file mode 100644 index 552dfa7ccc2..00000000000 --- a/.changeset/jetbrains-prompt-completion.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": minor ---- - -Add `/` slash commands and `@` file/git-changes mentions to the JetBrains chat prompt with native completion. diff --git a/.changeset/jetbrains-prompt-hint.md b/.changeset/jetbrains-prompt-hint.md deleted file mode 100644 index a69a0e95f75..00000000000 --- a/.changeset/jetbrains-prompt-hint.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Show a concise single-line hint in the JetBrains prompt placeholder. diff --git a/.changeset/jetbrains-prompt-mentions-undo.md b/.changeset/jetbrains-prompt-mentions-undo.md deleted file mode 100644 index ad71522d818..00000000000 --- a/.changeset/jetbrains-prompt-mentions-undo.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Fix prompt undo/redo, clean mid-token mention completion, and show unresolved file mentions in the JetBrains chat prompt. diff --git a/.changeset/jetbrains-send-typed-mentions.md b/.changeset/jetbrains-send-typed-mentions.md deleted file mode 100644 index 3921131f9b8..00000000000 --- a/.changeset/jetbrains-send-typed-mentions.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Attach hand-typed prompt file mentions when sending immediately. diff --git a/.changeset/jetbrains-session-focus-restore.md b/.changeset/jetbrains-session-focus-restore.md deleted file mode 100644 index dee4790f7f6..00000000000 --- a/.changeset/jetbrains-session-focus-restore.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Restore prompt focus after returning from session history in JetBrains. diff --git a/.changeset/jetbrains-slash-aliases.md b/.changeset/jetbrains-slash-aliases.md deleted file mode 100644 index bfc7284a57d..00000000000 --- a/.changeset/jetbrains-slash-aliases.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Support VS Code slash-command aliases in the JetBrains prompt. diff --git a/.changeset/jetbrains-slash-completion-fast-typing.md b/.changeset/jetbrains-slash-completion-fast-typing.md new file mode 100644 index 00000000000..2ea93a591b5 --- /dev/null +++ b/.changeset/jetbrains-slash-completion-fast-typing.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Keep the slash-command completion popup open while typing quickly and reopen it if it closes mid-token, so fast typing filters commands instead of dismissing the list. Refresh the popup when server commands finish loading, and return focus to the prompt after picking a model, agent, or reasoning option from a slash command. diff --git a/.changeset/jetbrains-stable-mention-completion.md b/.changeset/jetbrains-stable-mention-completion.md deleted file mode 100644 index c4578ede044..00000000000 --- a/.changeset/jetbrains-stable-mention-completion.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Keep JetBrains prompt mention suggestions stable while typing fuzzy file matches. diff --git a/.changeset/jetbrains-tool-approval-footer.md b/.changeset/jetbrains-tool-approval-footer.md new file mode 100644 index 00000000000..d0c0fd60155 --- /dev/null +++ b/.changeset/jetbrains-tool-approval-footer.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Show why and how each tool call was allowed. Expanded tool cards (edits, shell commands, and every other tool) now display a shield footer such as "Auto-approved by your global config", including the matched rule or agent and an outside-workspace note. A new "Show approval reason on tool cards" toggle at the bottom of Auto-Approve settings (on by default) controls the footer. diff --git a/.changeset/jetbrains-workflows-settings.md b/.changeset/jetbrains-workflows-settings.md new file mode 100644 index 00000000000..95416b13530 --- /dev/null +++ b/.changeset/jetbrains-workflows-settings.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": minor +--- + +Support opening, editing, and deleting workflows from JetBrains settings. diff --git a/.changeset/macos-network-sandbox.md b/.changeset/macos-network-sandbox.md deleted file mode 100644 index d36fc0c2c7f..00000000000 --- a/.changeset/macos-network-sandbox.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@kilocode/cli": minor -"kilo-code": minor ---- - -Block outbound network access from agent commands and in-process HTTP tools with the optional macOS sandbox, with a Sandboxing setting to allow network access when needed. diff --git a/.changeset/model-selector-provider-prefix.md b/.changeset/model-selector-provider-prefix.md new file mode 100644 index 00000000000..f5f98f39610 --- /dev/null +++ b/.changeset/model-selector-provider-prefix.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Allow model selector searches to match provider prefixes in colon-separated model names. diff --git a/.changeset/persist-index-updates.md b/.changeset/persist-index-updates.md deleted file mode 100644 index a35078a3c5c..00000000000 --- a/.changeset/persist-index-updates.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Preserve unchanged codebase indexes when extension or VS Code updates interrupt an incremental scan. diff --git a/.changeset/prompt-input-minimum-gutter.md b/.changeset/prompt-input-minimum-gutter.md new file mode 100644 index 00000000000..52d221bed36 --- /dev/null +++ b/.changeset/prompt-input-minimum-gutter.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Use a shared responsive minimum gutter for prompt inputs across the VS Code sidebar, Agent Manager, and worktree dialogs. diff --git a/.changeset/provider-usage-center.md b/.changeset/provider-usage-center.md new file mode 100644 index 00000000000..29bde035ef6 --- /dev/null +++ b/.changeset/provider-usage-center.md @@ -0,0 +1,7 @@ +--- +"@kilocode/cli": minor +"@kilocode/kilo-ui": minor +"kilo-code": minor +--- + +View current provider plan usage and quota windows in the CLI and VS Code profile. diff --git a/.changeset/quiet-auto-approved-permissions.md b/.changeset/quiet-auto-approved-permissions.md deleted file mode 100644 index c56014978c4..00000000000 --- a/.changeset/quiet-auto-approved-permissions.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Keep attention sounds silent for permission requests handled by auto-approve. diff --git a/.changeset/quiet-jetbrains-workspace-errors.md b/.changeset/quiet-jetbrains-workspace-errors.md new file mode 100644 index 00000000000..1b8eb82df9b --- /dev/null +++ b/.changeset/quiet-jetbrains-workspace-errors.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Avoid showing JetBrains internal error popups when Kilo workspace data fails to load, and include HTTP status and response details in diagnostics. diff --git a/.changeset/quiet-project-session-switch.md b/.changeset/quiet-project-session-switch.md new file mode 100644 index 00000000000..d830c7cc825 --- /dev/null +++ b/.changeset/quiet-project-session-switch.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Load the correct Agent Manager session history on the first worktree switch between projects. diff --git a/.changeset/rendered-document-inspector.md b/.changeset/rendered-document-inspector.md new file mode 100644 index 00000000000..a625b494b3a --- /dev/null +++ b/.changeset/rendered-document-inspector.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Add an Agent Manager document inspector that previews Markdown and text files, with inline Markdown review comments that can be sent to the agent. diff --git a/.changeset/sandbox-agent-writes.md b/.changeset/sandbox-agent-writes.md deleted file mode 100644 index ea727050768..00000000000 --- a/.changeset/sandbox-agent-writes.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@kilocode/cli": minor -"kilo-code": minor ---- - -Confine agent shell and file-tool writes to project and Kilo state directories with the optional macOS sandbox. diff --git a/.changeset/secure-sandbox-mutations.md b/.changeset/secure-sandbox-mutations.md deleted file mode 100644 index 63eb8fdb087..00000000000 --- a/.changeset/secure-sandbox-mutations.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Prevent sandboxed file tools from escaping project write roots through concurrent symlink replacement on macOS. diff --git a/.changeset/secure-worktree-sandboxes.md b/.changeset/secure-worktree-sandboxes.md deleted file mode 100644 index 72418e02591..00000000000 --- a/.changeset/secure-worktree-sandboxes.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Confine sandboxed worktree sessions to their active worktree instead of allowing writes to sibling or primary checkouts. diff --git a/.changeset/show-routed-step-models.md b/.changeset/show-routed-step-models.md deleted file mode 100644 index 04d06d28a4f..00000000000 --- a/.changeset/show-routed-step-models.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@kilocode/cli": patch -"kilo-code": patch ---- - -Show the concrete model reported for routed Kilo auto-model steps in CLI and VS Code session timelines. diff --git a/.changeset/subagent-session-tabs.md b/.changeset/subagent-session-tabs.md new file mode 100644 index 00000000000..9c595acb4f4 --- /dev/null +++ b/.changeset/subagent-session-tabs.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": minor +--- + +Open sub-agent task sessions in read-only editor tabs from JetBrains task cards. diff --git a/.changeset/tidy-indexing-button.md b/.changeset/tidy-indexing-button.md deleted file mode 100644 index 9b5fe509aaa..00000000000 --- a/.changeset/tidy-indexing-button.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": minor ---- - -Let users hide the codebase indexing button while indexing is off. diff --git a/.changeset/worktree-pty-cleanup.md b/.changeset/worktree-pty-cleanup.md new file mode 100644 index 00000000000..6801652757d --- /dev/null +++ b/.changeset/worktree-pty-cleanup.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Prevent Agent Manager terminal processes from escaping cleanup while a worktree is deleted. diff --git a/.gitattributes b/.gitattributes index a53ef3c352f..a382c23972f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,16 +10,32 @@ packages/kilo-ui/tests/**/*.png filter=lfs diff=lfs merge=lfs -text packages/kilo-vscode/tests/**/*.png filter=lfs diff=lfs merge=lfs -text packages/kilo-docs/public/img/screenshot-tests/**/*.png filter=lfs diff=lfs merge=lfs -text -# Hide non-English localization files by default in GitHub PR diffs. +# Hide most localization files by default in GitHub PR diffs. Show English as the source +# language and German so reviewers can catch untranslated copy-pasted English strings. **/i18n/*.ts linguist-generated=true +**/i18n/**/*.ts linguist-generated=true **/i18n/en.ts linguist-generated=false **/i18n/en*.ts linguist-generated=false +**/i18n/**/en.ts linguist-generated=false +**/i18n/**/en*.ts linguist-generated=false +**/i18n/de.ts linguist-generated=false +**/i18n/de*.ts linguist-generated=false +**/i18n/**/de.ts linguist-generated=false +**/i18n/**/de*.ts linguist-generated=false **/i18n/package-nls-en.ts linguist-generated=false +**/i18n/package-nls-de.ts linguist-generated=false **/i18n/index.ts linguist-generated=false +**/i18n/**/index.ts linguist-generated=false **/i18n/parity.test.ts linguist-generated=false +**/i18n/**/parity.test.ts linguist-generated=false packages/kilo-i18n/src/*.ts linguist-generated=true packages/kilo-i18n/src/en.ts linguist-generated=false +packages/kilo-i18n/src/de.ts linguist-generated=false # Auto-generated CLI reference docs packages/kilo-docs/markdoc/partials/cli-commands-table.md linguist-generated=true packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md linguist-generated=true + +# Upstream generated migration artifacts +packages/core/migration/**/snapshot.json linguist-generated +packages/core/src/database/migration.gen.ts linguist-generated diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3aeef82d62f..c6dfb0ebda4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,3 @@ # web + desktop packages -packages/app/ @adamdotdevin -packages/tauri/ @adamdotdevin -packages/desktop/src-tauri/ @brendonovich -packages/desktop/ @adamdotdevin +packages/app/ @Hona @Brendonovich +packages/desktop/ @Hona @Brendonovich diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 1d4d1916ab9..e3b64495f37 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,4 +2,4 @@ blank_issues_enabled: false contact_links: - name: 💬 Join our Discord url: https://kilo.ai/discord - about: For quick questions or real-time discussion. Note that issues are searchable and help others with the same question. + about: For support, troubleshooting, how-to questions, and real-time discussion. diff --git a/.github/actions/setup-bun/action.yml b/.github/actions/setup-bun/action.yml index 5b44517ec51..421bc0e5caa 100644 --- a/.github/actions/setup-bun/action.yml +++ b/.github/actions/setup-bun/action.yml @@ -8,6 +8,13 @@ inputs: runs: using: "composite" steps: + # node-gyp@latest (invoked via bunx for native install scripts) requires Node >=22; + # some runner images ship an older system Node on PATH + - name: Setup Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "24" + - name: Get baseline download URL id: bun-url shell: bash @@ -33,7 +40,10 @@ runs: shell: bash run: echo "dir=$(bun pm cache)" >> "$GITHUB_OUTPUT" + # Restoring the ~1 GB cache buys nothing on Windows even with Defender exclusions # kilocode_change + # (measured 83-102s Setup Bun with cache vs ~86s fresh install); keep Windows off it. # kilocode_change - name: Restore Bun dependencies + if: runner.os != 'Windows' # kilocode_change id: bun-cache uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: @@ -42,24 +52,59 @@ runs: restore-keys: | ${{ runner.os }}-bun- + # kilocode_change start + - name: Configure node-gyp Node headers + if: runner.os != 'Windows' + id: node-gyp + run: | + node <<'NODE' + const fs = require("node:fs") + const path = require("node:path") + + const exe = path.dirname(fs.realpathSync(process.execPath)) + const dirs = [exe, path.dirname(exe)] + const root = dirs.find((dir) => fs.existsSync(path.join(dir, "include", "node", "node.h"))) + if (!root) { + throw new Error(`Node headers not found under ${dirs.join(", ")}`) + } + + fs.appendFileSync(process.env.GITHUB_OUTPUT, `nodedir=${root}\n`) + NODE + shell: bash + # kilocode_change end + - name: Install setuptools for distutils compatibility run: python3 -m pip install setuptools || pip install setuptools || true shell: bash - name: Install dependencies run: | + # kilocode_change start + if [ "$RUNNER_OS" != "Windows" ]; then + export npm_config_nodedir="${{ steps.node-gyp.outputs.nodedir }}" + fi + # kilocode_change end + # Workaround for patched peer variants # e.g. ./patches/ for standard-openapi - # https://github.com/oven-sh/bun/issues/28147 + # https://github.com/oven-sh/bun/issues/28147 # kilocode_change if [ "$RUNNER_OS" = "Windows" ]; then - bun install --linker hoisted ${{ inputs.install-flags }} + # kilocode_change start + if ! bun install --frozen-lockfile --linker hoisted ${{ inputs.install-flags }}; then + echo "::warning::Bun install failed on Windows; retrying with conservative extraction" + sleep 5 + BUN_FEATURE_FLAG_DISABLE_STREAMING_INSTALL=1 \ + bun install --frozen-lockfile --linker hoisted --network-concurrency 16 ${{ inputs.install-flags }} + fi + # kilocode_change end else - bun install ${{ inputs.install-flags }} + bun install --frozen-lockfile ${{ inputs.install-flags }} # kilocode_change fi shell: bash + # Do not upload a Windows cache that Windows jobs intentionally never restore. # kilocode_change - name: Save Bun dependencies - if: steps.bun-cache.outputs.cache-hit != 'true' && github.event_name != 'pull_request' && github.event_name != 'pull_request_target' + if: runner.os != 'Windows' && steps.bun-cache.outputs.cache-hit != 'true' && github.event_name != 'pull_request' && github.event_name != 'pull_request_target' # kilocode_change uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ${{ steps.cache.outputs.dir }} diff --git a/.github/actions/setup-linux-sandbox/action.yml b/.github/actions/setup-linux-sandbox/action.yml new file mode 100644 index 00000000000..6e3a7804204 --- /dev/null +++ b/.github/actions/setup-linux-sandbox/action.yml @@ -0,0 +1,21 @@ +# kilocode_change - new file +name: "Setup Linux Sandbox" +description: "Build the Linux bubblewrap helper" +runs: + using: "composite" + steps: + - name: Setup Zig + run: | + curl --fail --location --retry 3 \ + https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz \ + --output "$RUNNER_TEMP/zig.tar.xz" + echo "473ec26806133cf4d1918caf1a410f8403a13d979726a9045b421b685031a982 $RUNNER_TEMP/zig.tar.xz" | sha256sum --check --status + tar -xJf "$RUNNER_TEMP/zig.tar.xz" -C "$RUNNER_TEMP" + echo "$RUNNER_TEMP/zig-linux-x86_64-0.14.0" >> "$GITHUB_PATH" + shell: bash + + - name: Build bubblewrap helper + run: | + bun packages/opencode/script/kilocode/bubblewrap.ts --arch x64 --output "$RUNNER_TEMP/bwrap" + echo "KILO_BWRAP_PATH=$RUNNER_TEMP/bwrap" >> "$GITHUB_ENV" + shell: bash diff --git a/.github/docs-sync/collect.mjs b/.github/docs-sync/collect.mjs new file mode 100644 index 00000000000..75ab641039c --- /dev/null +++ b/.github/docs-sync/collect.mjs @@ -0,0 +1,180 @@ +// kilocode_change - new file + +/** + * Collects PRs merged to the source repos since the watermark, applies a + * deterministic pre-filter, and writes docs-sync-out/digest.json for the LLM + * triage pass. + * + * Pre-filter drops (triage never sees these): + * - PRs labeled auto-docs (this bot's own rolling PRs) + * - chore/test/ci/build/docs/style/refactor conventional titles + * - PRs touching only docs/non-product paths + * + * Revert PRs (conventional `revert(...):` and GitHub-native `Revert "..."` titles) + * are intercepted as signals, never added to the digest; affected digest entries + * gain `reverted_by` so triage/edit can skip them. + * + * Bot-authored PRs are kept: release/dependency bots ship user-facing + * changes too, and the label + docs-only guards above prevent loops. + */ + +import fs from "node:fs" +import { api, appendOutput, appendSummary, listPrFiles, searchIssues } from "./lib.mjs" +import { revertTitleKind, parseRevertTargets, applyRevertAnnotations, unannotatedRevertSignals } from "./reverts.mjs" + +const SOURCE_REPOS = ["Kilo-Org/cloud", "Kilo-Org/kilocode"] +const OUT_DIR = "docs-sync-out" +const BODY_LIMIT = 2000 +const SLIM_BODY_LIMIT = 300 +const PATCH_LIMIT = 8000 +const FILE_LIMIT = 30 +// Revert PRs (conventional AND GitHub-native) are intercepted below BEFORE this filter; the "revert" alternative here is unreachable and kept only to minimize diff. +const DROP_TITLE = /^(chore|test|ci|build|docs|style|refactor|revert)(\(.+\))?!?:/i +const DOCS_ONLY_PATH = /^(packages\/kilo-docs\/|\.github\/docs-sync\/|docs-sync-out\/|docs\/|[^/]+\.md$)/ + +function argSince() { + const i = process.argv.indexOf("--since") + const v = i >= 0 ? process.argv[i + 1] : null + if (!v || Number.isNaN(new Date(v).getTime())) { + throw new Error("usage: collect.mjs --since ") + } + return new Date(v) +} + +async function mergedPrs(fullRepo, since) { + const query = `repo:${fullRepo} is:pr is:merged merged:>=${since.toISOString()}` + return searchIssues(query) +} + +const since = argSince() +console.log(`collecting PRs merged since ${since.toISOString()}`) + +const digest = [] +const dropped = { label: 0, title: 0, docs_only: 0, fetch_error: 0, revert: 0 } +const reverts = [] + +for (const fullRepo of SOURCE_REPOS) { + const prs = await mergedPrs(fullRepo, since) + console.log(`${fullRepo}: ${prs.length} merged PRs in window`) + + for (const item of prs) { + const author = item.user?.login ?? "" + if ((item.labels ?? []).some((l) => l.name === "auto-docs")) { + dropped.label++ + continue + } + if (revertTitleKind(item.title ?? "")) { + try { + const pr = await api(`/repos/${fullRepo}/pulls/${item.number}`) + const targets = parseRevertTargets(pr.body ?? "", fullRepo) + reverts.push({ url: pr.html_url, merged_at: pr.merged_at, targets }) + if (targets.length === 0) { + console.warn(`::warning::revert PR ${fullRepo}#${item.number} has no parseable targets`) + } + dropped.revert++ + } catch (err) { + // One dead revert PR must not abort the run; its targets just go unannotated. + console.warn(`::warning::skipping revert ${fullRepo}#${item.number}: ${err.message}`) + dropped.fetch_error++ + } + continue + } + if (DROP_TITLE.test(item.title ?? "")) { + dropped.title++ + continue + } + + const number = item.number + let pr + let files + try { + pr = await api(`/repos/${fullRepo}/pulls/${number}`) + files = await listPrFiles(fullRepo, number) + } catch (err) { + // Isolate per-PR failures: one dead PR must not abort the whole run. + console.warn(`::warning::skipping ${fullRepo}#${number}: ${err.message}`) + dropped.fetch_error++ + continue + } + // listPrFiles caps at 300 files; a truncated list can't support the + // docs-only classification, so keep such PRs and record the true total. + const truncated = files.length >= 300 + if (!truncated && files.length > 0 && files.every((f) => DOCS_ONLY_PATH.test(f.filename))) { + dropped.docs_only++ + continue + } + + let patch = "" + for (const f of files) { + if (!f.patch) continue + const chunk = `--- ${f.filename}\n${f.patch}\n` + if (patch.length + chunk.length > PATCH_LIMIT) { + patch += "\n... (diff truncated) ...\n" + break + } + patch += chunk + } + + digest.push({ + repo: fullRepo, + number, + title: pr.title, + url: pr.html_url, + author, + merged_at: pr.merged_at, + labels: (pr.labels ?? []).map((l) => l.name), + body: (pr.body ?? "").slice(0, BODY_LIMIT), + files: files.slice(0, FILE_LIMIT).map((f) => `${f.status} ${f.filename} (+${f.additions}/-${f.deletions})`), + files_total: pr.changed_files ?? files.length, + patch_excerpt: patch, + }) + } +} + +const applied = applyRevertAnnotations(digest, reverts) +const unannotated = unannotatedRevertSignals(reverts, applied) + +digest.sort((a, b) => new Date(a.merged_at) - new Date(b.merged_at)) + +fs.mkdirSync(OUT_DIR, { recursive: true }) +// Full digest (bodies + patch excerpts) is filtered down to docs-worthy PRs +// for the edit pass; the slim digest keeps the triage pass context small. +fs.writeFileSync(`${OUT_DIR}/digest-full.json`, JSON.stringify(digest, null, 2)) +const slim = digest.map(({ patch_excerpt, body, ...rest }) => ({ + ...rest, + body: body.slice(0, SLIM_BODY_LIMIT), +})) +fs.writeFileSync(`${OUT_DIR}/digest.json`, JSON.stringify(slim, null, 2)) + +console.log(`kept ${digest.length} PRs, dropped:`, dropped) +appendOutput("count", digest.length) +appendOutput("digest", `${OUT_DIR}/digest.json`) + +const summaryLines = [ + "### docs-sync collect", + "", + `- window: since \`${since.toISOString()}\``, + `- kept: **${digest.length}** PRs`, + `- dropped: ${dropped.label} auto-docs, ${dropped.title} title filter, ${dropped.docs_only} docs-only, ${dropped.fetch_error} fetch errors, ${dropped.revert} reverts intercepted`, + "", + ...digest.map((d) => `- [${d.repo}#${d.number}](${d.url}) ${d.title}`), +] +if (applied.length > 0) { + summaryLines.push("", "**revert annotations:**", ...applied.map(([target, reverter]) => `- ${target} — reverted by ${reverter}`)) +} +if (unannotated.missed.length > 0 || unannotated.unparsed.length > 0) { + summaryLines.push("", "**revert targets with no in-window annotation:**") + for (const m of unannotated.missed) { + summaryLines.push(`- ${m.url} — unannotated targets: ${m.targets.join(", ")}`) + } + for (const url of unannotated.unparsed) { + summaryLines.push(`- ${url} (no parseable targets)`) + } +} +if (unannotated.chains.length > 0) { + summaryLines.push("", "**revert chains (not annotated):**") + for (const c of unannotated.chains) { + summaryLines.push(`- ${c.url}${c.targets.length > 0 ? ` (targets: ${c.targets.join(", ")})` : ""}`) + } +} +appendSummary(summaryLines.join("\n")) diff --git a/.github/docs-sync/edit-prompt.md b/.github/docs-sync/edit-prompt.md new file mode 100644 index 00000000000..0a3b7a98e1d --- /dev/null +++ b/.github/docs-sync/edit-prompt.md @@ -0,0 +1,28 @@ +You are the Kilo Code documentation bot. You update the public product documentation in `packages/kilo-docs` (a Markdoc/Next.js site served at kilo.ai/docs) so it reflects recently merged PRs. You are handling one batch of PRs; the batch files and your output file are named at the end of these instructions. + +Before writing anything: + +1. Read `packages/kilo-docs/AGENTS.md` and `packages/kilo-docs/STYLE_GUIDE.md` and follow them exactly: Markdoc custom tags, the `/docs` prefix in image paths, navigation files under `lib/nav/`, redirect rules, and the generated-screenshot policy. +2. Read the attached batch files: the full-details file (PR title, body, file list, `patch_excerpt` diffs) and the triage file (docs-worthiness verdicts, target sections, priorities). +3. Verify facts against the current source tree — for Kilo-Org/kilocode PRs. This checkout reflects current kilocode main: before documenting a command, flag, setting, default, or behavior from a kilocode PR, confirm it exists in the current source. Existence alone is not enough: defaults, whether an option is required or optional, and on-by-default behavior must also match the current tree — a symbol that still exists as opt-in does not justify documenting default-on behavior. When the merged diff and the current tree disagree, the current tree wins — the change may have been reverted or superseded; skip it and record why. For Kilo-Org/cloud PRs the source is not in this checkout: rely on the PR diff and body, and on the `reverted_by` field below. Reading any file in the checkout for verification is expected; the hard rule against touching anything outside `packages/kilo-docs/` applies to writing only. + +For each PR in the batch, in priority order, first decide whether it needs documentation at all — skipping is a first-class outcome. For each one that does: + +- Find the most relevant existing docs page(s) and make minimal, precise updates in the style of the surrounding content. +- Create a new page only when no existing page fits; then add it to the matching nav file in `packages/kilo-docs/lib/nav/`. +- Document only behavior that is actually present in the merged diff and still present in the product now (step 3 above: when they disagree, the current state wins). If the PR body or diff shows the feature is behind a flag or otherwise not user-visible yet, skip it and record why. +- If a PR turns out not to need documentation, skip it and record why. Trust evidence over the triage verdict. +- A batch PR with a `reverted_by` field was reverted by that PR; skip it unless there is clear evidence the change is present now — for kilocode PRs verify it in the current source tree (re-land); for cloud PRs require explicit re-land evidence in the entry itself. +- Skipping is a normal outcome: a batch where every PR is skipped is a valid result. Never write docs just to have something to show. + +Hard rules: + +- Only create or modify files under `packages/kilo-docs/`. Never touch code, tests, config, images, or anything outside that directory. +- Never remove or rename pages. Never document unreleased behavior. Never copy internal PR discussion into the docs; write user-facing documentation. +- Do not run git commands and do not commit anything; automation handles git. +- Keep the change small and precise. Do not rewrite sections that are already accurate. +- Never create, modify, or delete packages/kilo-docs/LEARNINGS.md. Automation owns that file. + +When finished, write the summary JSON file named in the batch specifics below: a JSON array with exactly one entry per batch PR, consumed by automation (this file is never committed). Use `action` values like `updated `, `created `, or `skipped`. Example: + +[{"pr": 123, "url": "https://github.com/Kilo-Org/kilocode/pull/123", "action": "updated pages/code-with-ai/platforms/cli.md", "reason": "documented --variant flag"}, {"pr": 124, "url": "https://github.com/Kilo-Org/kilocode/pull/124", "action": "skipped", "reason": "feature behind unreleased flag"}] diff --git a/.github/docs-sync/edit.mjs b/.github/docs-sync/edit.mjs new file mode 100644 index 00000000000..15b89734edc --- /dev/null +++ b/.github/docs-sync/edit.mjs @@ -0,0 +1,209 @@ +// kilocode_change - new file + +/** + * Runs the LLM edit pass over docs-sync-out/worthy.json in batches. + * + * Batching bounds each `kilo run` context (a replay window can yield dozens + * of docs-worthy PRs with large diffs). Each batch gets its own CLI session + * and writes its own summary file; results are merged into + * docs-sync-out/edit-summary.json. A batch that fails or is deferred by the + * wall-clock budget is recorded as action "pending" so the watermark holds + * back and the next run re-collects those PRs. + * + * Env: EDIT_MODEL (provider/model), KILO_API_KEY + KILO_ORG_ID (set by workflow; read natively by the kilo provider). + * Budgets: EDIT_BUDGET_MINUTES (default 50), EDIT_BATCH_TIMEOUT_MINUTES (default 15). + * Test hook: DOCS_SYNC_BACKOFF_MS replaces every retry wait when set. + */ + +import fs from "node:fs" +import path from "node:path" +import { fileURLToPath } from "node:url" +import { backoffMsForAttempt, deadline, remainingMs, runKilo, sleepSync } from "./lib.mjs" +import { readLearningsBlock } from "./learn.mjs" + +const BATCH_SIZE = 5 +const ATTEMPTS = 3 +const OUT_DIR = "docs-sync-out" +export const SUMMARY_FILE = ".docs-sync-summary.json" + +const HERE = path.dirname(fileURLToPath(import.meta.url)) +const basePrompt = fs.readFileSync(path.join(HERE, "edit-prompt.md"), "utf8") + readLearningsBlock("edit") +const model = process.env.EDIT_MODEL +if (!model) throw new Error("EDIT_MODEL is required") + +const EDIT_BUDGET_MINUTES = Number(process.env.EDIT_BUDGET_MINUTES) || 50 +const EDIT_BATCH_TIMEOUT_MINUTES = Number(process.env.EDIT_BATCH_TIMEOUT_MINUTES) || 15 +const BATCH_TIMEOUT_MS = EDIT_BATCH_TIMEOUT_MINUTES * 60 * 1000 + +const worthy = JSON.parse(fs.readFileSync(`${OUT_DIR}/worthy.json`, "utf8")) +const triage = JSON.parse(fs.readFileSync(`${OUT_DIR}/triage.json`, "utf8")) +const priority = new Map(triage.map((e) => [e.url, e])) +const ordered = [...worthy].sort((a, b) => { + const rank = { high: 0, medium: 1, low: 2 } + return (rank[priority.get(a.url)?.priority] ?? 1) - (rank[priority.get(b.url)?.priority] ?? 1) +}) + +/** @type {Map} url → pending cause for failed/deferred batches */ +const pendingCauses = new Map() + +function formatCause(result) { + const bits = [] + if (result.timedOut) bits.push("timed out") + if (result.exitCode !== null && result.exitCode !== undefined) bits.push(`exit ${result.exitCode}`) + if (result.stderrTail) bits.push(result.stderrTail.replaceAll("\n", " ").slice(0, 200)) + return bits.join("; ") || "no diagnostic" +} + +function editBatch(batch, index, budgetDeadline) { + const batchFile = `${OUT_DIR}/edit-batch-${index}.json` + const triageFile = `${OUT_DIR}/edit-batch-triage-${index}.json` + const summaryFile = `${OUT_DIR}/edit-summary-${index}.json` + fs.writeFileSync(batchFile, JSON.stringify(batch, null, 2)) + fs.writeFileSync(triageFile, JSON.stringify(batch.map((d) => priority.get(d.url)).filter(Boolean), null, 2)) + + const prompt = `${basePrompt} + +Batch specifics for this run: the PRs to handle are in the attached ${batchFile} (full details) and ${triageFile} (triage verdicts). Handle ONLY the PRs in these batch files. When finished, write your per-PR results in the summary JSON format described above to the file \`${summaryFile}\` (path relative to the repository root).` + + let lastCause = "edit pass failed" + for (let attempt = 1; attempt <= ATTEMPTS; attempt++) { + const left = remainingMs(budgetDeadline) + if (left < BATCH_TIMEOUT_MS) { + lastCause = `edit budget exhausted before batch ${index} attempt ${attempt} (${Math.ceil(left / 1000)}s left, need ${EDIT_BATCH_TIMEOUT_MINUTES}m)` + console.warn( + `batch ${index}: stopping retries — remaining budget cannot fit another ${EDIT_BATCH_TIMEOUT_MINUTES}m attempt`, + ) + break + } + + // Headless `kilo run` auto-rejects every permission ask; without --auto the + // agent cannot run shell commands. SECURITY: --auto grants unrestricted bash + // to an agent steered by external PR content. Hardening deferred: a scoped + // permission.bash map via KILO_CONFIG_CONTENT should replace --auto once the + // required shell patterns are stable (see PR #12605 review thread). + const result = runKilo({ + args: [ + "run", + "--auto", + prompt, + "-m", + model, + "--variant", + "high", + "--dir", + process.cwd(), + "-f", + batchFile, + "-f", + triageFile, + ], + timeoutMs: Math.min(BATCH_TIMEOUT_MS, left), + streamStdout: true, + label: `edit batch ${index} attempt ${attempt}`, + }) + + if (fs.existsSync(summaryFile)) return true + // Tolerate the agent dropping the docs-sync-out/ prefix. + const alt = path.basename(summaryFile) + if (fs.existsSync(alt)) { + fs.renameSync(alt, summaryFile) + return true + } + + // Exit 0 is not success: missing summary is a failure logged WITH the + // captured stderrTail and exit code on every attempt. + const cause = formatCause(result) + lastCause = `edit batch ${index}: ${cause}` + console.warn( + `batch ${index} attempt ${attempt}: summary file ${summaryFile} not produced` + + ` (exit ${result.exitCode}${result.timedOut ? ", timed out" : ""})` + + (result.stderrTail ? `\nstderr tail:\n${result.stderrTail}` : "\nstderr tail: (empty)"), + ) + + if (attempt < ATTEMPTS) { + const wait = backoffMsForAttempt(attempt) + // Skip the wait when the remaining budget cannot fit another attempt. + const afterWait = remainingMs(budgetDeadline) - wait + if (wait > 0 && afterWait >= BATCH_TIMEOUT_MS) { + console.warn(`batch ${index}: backing off ${wait / 1000}s before attempt ${attempt + 1}`) + sleepSync(wait) + } else if (wait > 0) { + console.warn(`batch ${index}: skipping backoff — remaining budget cannot fit attempt ${attempt + 1} after wait`) + } + } + } + + console.warn(`::warning::edit batch ${index} failed after up to ${ATTEMPTS} attempts; ${batch.length} PRs pending`) + for (const d of batch) pendingCauses.set(d.url, lastCause) + return false +} + +const batches = [] +for (let i = 0; i < ordered.length; i += BATCH_SIZE) { + batches.push(ordered.slice(i, i + BATCH_SIZE)) +} +console.log(`editing docs for ${ordered.length} PRs in ${batches.length} batches of up to ${BATCH_SIZE}`) + +const budgetDeadline = deadline(EDIT_BUDGET_MINUTES) +let deferredFrom = -1 + +for (let i = 0; i < batches.length; i++) { + const left = remainingMs(budgetDeadline) + if (left < BATCH_TIMEOUT_MS) { + deferredFrom = i + const deferredPrs = batches.slice(i).reduce((n, b) => n + b.length, 0) + console.warn( + `stopping edit pass before batch ${i}: remaining budget (${Math.ceil(left / 1000)}s) cannot fit a ${EDIT_BATCH_TIMEOUT_MINUTES}m batch; deferring ${deferredPrs} PRs`, + ) + const cause = `edit budget exhausted before batch ${i} (${Math.ceil(left / 1000)}s left)` + for (let j = i; j < batches.length; j++) { + for (const d of batches[j]) pendingCauses.set(d.url, cause) + } + break + } + editBatch(batches[i], i, budgetDeadline) +} + +if (deferredFrom >= 0) { + console.warn( + `edit pass deferred ${batches.slice(deferredFrom).reduce((n, b) => n + b.length, 0)} PRs due to wall-clock budget`, + ) +} + +// Merge batch summaries. Coverage: every worthy PR gets an entry so the PR +// body accounts for it; failed/deferred batches show up as pending (not skipped). +const merged = [] +const seen = new Set() +for (let i = 0; i < batches.length; i++) { + const file = `${OUT_DIR}/edit-summary-${i}.json` + let entries = [] + try { + entries = JSON.parse(fs.readFileSync(file, "utf8")) + } catch { + continue + } + for (const e of entries) { + const url = String(e?.url ?? "") + if (!url.startsWith("http") || seen.has(url)) continue + seen.add(url) + merged.push({ + pr: Number(e.pr) || 0, + url, + action: String(e.action ?? "skipped"), + reason: String(e.reason ?? ""), + }) + } +} +for (const d of ordered) { + if (seen.has(d.url)) continue + const cause = pendingCauses.get(d.url) || "edit pass failed or timed out for this PR" + merged.push({ pr: d.number, url: d.url, action: "pending", reason: cause }) +} + +// upsert-pr.mjs consumes the merged summary from the repo root; the file is +// removed there before committing so it never lands in the docs PR. +fs.writeFileSync(SUMMARY_FILE, JSON.stringify(merged, null, 2)) +const changed = merged.filter((e) => e.action !== "skipped" && e.action !== "pending").length +const skipped = merged.filter((e) => e.action === "skipped").length +const pending = merged.filter((e) => e.action === "pending").length +console.log(`edit pass complete: ${changed} changed, ${skipped} skipped, ${pending} pending`) diff --git a/.github/docs-sync/extract-json.mjs b/.github/docs-sync/extract-json.mjs new file mode 100644 index 00000000000..c8cdf04ff12 --- /dev/null +++ b/.github/docs-sync/extract-json.mjs @@ -0,0 +1,76 @@ +// kilocode_change - new file + +/** + * Extracts and validates the triage JSON array from raw LLM stdout. + * Usage: extract-json.mjs + * Exit 0 on success, 1 on any failure. Also exports parseTriageEntries for + * the chunked triage runner. + */ + +import fs from "node:fs" +import { pathToFileURL } from "node:url" + +/** Returns validated triage entries, or null when extraction fails. */ +export function parseTriageEntries(raw) { + // `kilo run` prints the assistant message twice (streaming render + final + // summary), so stdout can hold the same array back-to-back. Try each "[" + // from the right and return the first slice that parses — i.e. the last + // (most recent) valid array in the output. + const end = raw.lastIndexOf("]") + if (end < 0) return null + + const starts = [] + for (let i = 0; i <= end; i++) { + if (raw[i] === "[") starts.push(i) + } + + for (let s = starts.length - 1; s >= 0; s--) { + let parsed + try { + parsed = JSON.parse(raw.slice(starts[s], end + 1)) + } catch { + continue + } + if (!Array.isArray(parsed)) continue + const entries = validate(parsed) + if (entries) return entries + } + return null +} + +function validate(parsed) { + const entries = [] + for (const e of parsed) { + const pr = Number(e?.pr) + const url = String(e?.url ?? "") + if (!Number.isInteger(pr) || !url.startsWith("http")) continue + entries.push({ + pr, + url, + docs_worthy: e.docs_worthy === true, + reason: String(e.reason ?? ""), + target_sections: Array.isArray(e.target_sections) ? e.target_sections.map(String) : [], + priority: ["high", "medium", "low"].includes(e.priority) ? e.priority : "medium", + }) + } + return entries.length > 0 ? entries : null +} + +function main() { + const [, , inputPath, outputPath] = process.argv + if (!inputPath || !outputPath) { + console.error("usage: extract-json.mjs ") + process.exit(1) + } + const entries = parseTriageEntries(fs.readFileSync(inputPath, "utf8")) + if (!entries) { + console.error("no valid triage JSON array found in input") + process.exit(1) + } + fs.writeFileSync(outputPath, JSON.stringify(entries, null, 2)) + console.log(`extracted ${entries.length} triage entries (${entries.filter((e) => e.docs_worthy).length} docs-worthy)`) +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + main() +} diff --git a/.github/docs-sync/filter-worthy.mjs b/.github/docs-sync/filter-worthy.mjs new file mode 100644 index 00000000000..ea99f3d2589 --- /dev/null +++ b/.github/docs-sync/filter-worthy.mjs @@ -0,0 +1,24 @@ +// kilocode_change - new file + +/** + * Filters the full digest down to PRs the triage pass marked docs-worthy. + * Usage: filter-worthy.mjs + * The edit pass consumes the output so its context stays small. + */ + +import fs from "node:fs" + +const [, , digestPath, triagePath, outputPath] = process.argv +if (!digestPath || !triagePath || !outputPath) { + console.error("usage: filter-worthy.mjs ") + process.exit(1) +} + +const digest = JSON.parse(fs.readFileSync(digestPath, "utf8")) +const triage = JSON.parse(fs.readFileSync(triagePath, "utf8")) + +const worthy = new Set(triage.filter((e) => e.docs_worthy).map((e) => e.url)) +const out = digest.filter((d) => worthy.has(d.url)) + +fs.writeFileSync(outputPath, JSON.stringify(out, null, 2)) +console.log(`${out.length} of ${digest.length} digest entries are docs-worthy`) diff --git a/.github/docs-sync/learn.mjs b/.github/docs-sync/learn.mjs new file mode 100644 index 00000000000..6baa59124e2 --- /dev/null +++ b/.github/docs-sync/learn.mjs @@ -0,0 +1,937 @@ +// kilocode_change - new file + +/** + * Learns general rules of thumb from maintainer corrections to the docs-sync + * bot's rolling pull request, and writes them into packages/kilo-docs/LEARNINGS.md + * so the triage and edit passes follow them on every subsequent run. + * + * Two modes: + * node learn.mjs — extraction: fetch corrections, call the model, validate + * node learn.mjs --apply — apply: write learnings.json into LEARNINGS.md + * + * Env: TRIAGE_MODEL (provider/model, reused), GH_TOKEN (or GITHUB_TOKEN). + * Budget: LEARNINGS_BUDGET_MINUTES (default 10). + * Test hook: DOCS_SYNC_FIXTURE. When set to a fixture JSON path, skips every + * GitHub API call and writes any marker PATCH to .patched instead of + * the network. The workflow never sets it — only selftests do. + * + * Test hook: DOCS_SYNC_BACKOFF_MS replaces wait between extraction retries, same as + * lib.mjs:138 documents for triage.mjs and edit.mjs. + * + * Patch suppression: DRY_RUN=true or LEARNINGS_NO_PATCH=1 suppress the marker PATCH. + */ + +import { execFileSync } from "node:child_process" +import fs from "node:fs" +import path from "node:path" +import { fileURLToPath, pathToFileURL } from "node:url" + +const LEARNINGS_FILE = "packages/kilo-docs/LEARNINGS.md" +const OUT_DIR = "docs-sync-out" +const ATTEMPTS = 2 +const LEARNINGS_BUDGET_MINUTES = Number(process.env.LEARNINGS_BUDGET_MINUTES) || 10 +const EXTRACTION_TIMEOUT_MS = LEARNINGS_BUDGET_MINUTES * 60 * 1000 +const COMMENT_BODY_CAP = 5000 + +const HERE = path.dirname(fileURLToPath(import.meta.url)) + +const LINE_RE = + /^- (?.+?) $/ + +const LEARNED_THROUGH_RE = // + +// Agent-generated strings land in the PR body next to machine-read markers. +// Identical to clean() at upsert-pr.mjs:37. +function clean(value) { + return String(value ?? "") + .replaceAll("", "") +} + +function warn(msg) { + console.warn(`::warning::${msg}`) +} + +function log(msg) { + console.log(msg) +} + +// --- pure exports --- + +/** + * Parse the LEARNINGS.md file text into an entry array. + * Drops lines inside the markers that do not match the format. + */ +export function parseLearnings(text) { + const m = String(text ?? "").match( + /([\s\S]*?)/, + ) + if (!m) return [] + const entries = [] + for (const line of m[1].split("\n")) { + const trimmed = line.trim() + if (!trimmed) continue + const parsed = trimmed.match(LINE_RE) + if (!parsed) { + warn(`LEARNINGS.md: dropping unparseable line: ${trimmed.slice(0, 80)}`) + continue + } + entries.push({ + id: parsed.groups.id, + rule: clean(parsed.groups.rule).replaceAll("\n", " "), + scope: parsed.groups.scope, + source: parsed.groups.source, + date: parsed.groups.date, + }) + } + return entries +} + +/** Render the full LEARNINGS.md file text from an entry array. Deterministic order. */ +export function renderLearnings(entries) { + const list = [...entries].sort((a, b) => { + if (a.date !== b.date) return a.date < b.date ? -1 : 1 + return a.id < b.id ? -1 : a.id > b.id ? 1 : 0 + }) + const lines = list.map( + (e) => + `- ${clean(e.rule).replaceAll("\n", " ")} `, + ) + return [ + "# docs-sync learnings", + "", + "Rules the docs-sync bot learned from maintainer corrections to its rolling pull request.", + "The bot reads this file at the start of every run and follows every rule below.", + "", + "To unlearn a rule, delete its line and commit. The next run reads this file from the", + "branch, so the rule is gone from its input, and the deletion itself is a correction the", + "extraction step is instructed not to undo.", + "", + "", + ...lines, + "", + "", + ].join("\n") +} + +/** Parse the learned-through watermark from a PR body. Returns { commit, comment } with nulls for absent/none. */ +export function parseLearnedThrough(body) { + const m = String(body ?? "").match(LEARNED_THROUGH_RE) + if (!m) return { commit: null, comment: null } + const commit = m[1] === "none" ? null : m[1] + const comment = m[2] === "none" ? null : m[2] + return { commit, comment } +} + +/** Render a single learned-through marker line. */ +export function renderLearnedThrough({ commit, comment }) { + const c = commit ?? "none" + const m = comment ?? "none" + return `` +} + +/** Replace or append the learned-through marker in a PR body. Pure — no API call. */ +export function patchMarkerIntoBody(body, marker) { + const b = String(body ?? "") + if (LEARNED_THROUGH_RE.test(b)) { + return b.replace(LEARNED_THROUGH_RE, marker) + } + return b + "\n" + marker + "\n" +} + +/** + * Extract { add, remove } from raw model stdout. + * Mirrors parseTriageEntries at extract-json.mjs:14-38, adapted for an object. + * `kilo run` prints the assistant message twice; the last copy wins. + * Walk "{" positions from right to left; return the first that parses to an object + * holding an array `add` or an array `remove`. + */ +export function parseDelta(raw) { + const r = String(raw ?? "") + const end = r.lastIndexOf("}") + if (end < 0) return null + + const starts = [] + for (let i = 0; i <= end; i++) { + if (r[i] === "{") starts.push(i) + } + + for (let s = starts.length - 1; s >= 0; s--) { + let parsed + try { + parsed = JSON.parse(r.slice(starts[s], end + 1)) + } catch { + continue + } + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) continue + if (Array.isArray(parsed.add) || Array.isArray(parsed.remove)) { + return { + add: Array.isArray(parsed.add) ? parsed.add : [], + remove: Array.isArray(parsed.remove) ? parsed.remove : [], + } + } + } + return null +} + +/** Cap a review comment body so a single long comment cannot dominate extraction input. */ +function capBody(body) { + const b = String(body ?? "") + if (b.length <= COMMENT_BODY_CAP) return b + return b.slice(0, COMMENT_BODY_CAP) + " [truncated]" +} + +/** Normalize rule text for duplicate comparison: lowercase, strip punctuation and whitespace runs. */ +function norm(text) { + return String(text ?? "") + .toLowerCase() + .replace(/[^\w\s]/g, "") + .replace(/\s+/g, " ") + .trim() +} + +/** + * Validate a delta against the existing entries and constraints. + * Returns { add, remove, rejected }. Never throws. + */ +export function validateDelta(delta, { existing, candidateSources, deletedInWindow }) { + const add = Array.isArray(delta.add) ? delta.add : [] + const remove = Array.isArray(delta.remove) ? delta.remove : [] + const ex = Array.isArray(existing) ? existing : [] + const candidates = Array.isArray(candidateSources) ? candidateSources : [] + const deleted = Array.isArray(deletedInWindow) ? deletedInWindow : [] + + const rejected = [] + const valid = [] + const toRemove = [] + const existingIds = new Set(ex.map((e) => e.id)) + // One model response can repeat an id or a rule. Both would render two lines for + // one id, so an accepted addition also blocks the next one. + const acceptedIds = new Set() + const acceptedRules = new Set() + + // Process remove first so toRemove is populated before the add loop checks + // for id collisions with entries listed in remove (criterion 8). + for (const id of remove) { + if (!existingIds.has(id)) { + rejected.push({ entry: { id, remove: id }, reason: `remove target ${id} not in existing entries` }) + } else { + toRemove.push(id) + } + } + + for (const a of add) { + let reason = null + + // Reject null, undefined, and non-object entries before any property access. + if (a === null || a === undefined || typeof a !== "object" || Array.isArray(a)) { + rejected.push({ entry: a, reason: "add entry is null, undefined, or not a plain object" }) + continue + } + + if (!a.rule || String(a.rule).length < 10 || String(a.rule).length > 300) { + reason = "rule text absent, shorter than 10 characters, or longer than 300" + } else if (!["triage", "edit", "both"].includes(a.scope)) { + reason = `invalid scope: ${a.scope}` + } else if (!/^commit:[0-9a-f]{7,40}$/.test(a.source) && !/^comment:\d+$/.test(a.source)) { + reason = `invalid source format: ${a.source}` + } else if (!candidates.includes(a.source)) { + reason = `source ${a.source} not in candidate sources` + } else if (!/^[a-z0-9][a-z0-9-]{2,48}$/.test(a.id)) { + reason = `invalid id format: ${a.id}` + } else if (existingIds.has(a.id) && !toRemove.includes(a.id)) { + reason = `id ${a.id} collides with an existing entry not listed in remove` + } else if (acceptedIds.has(a.id)) { + reason = `id ${a.id} collides with an earlier addition in this delta` + } else if (!/^\d{4}-\d{2}-\d{2}$/.test(a.date)) { + reason = `invalid date format: ${a.date}` + } else { + // Check that date is a real calendar date. + const d = new Date(a.date + "T00:00:00Z") + if (Number.isNaN(d.getTime()) || d.toISOString().slice(0, 10) !== a.date) { + reason = `invalid calendar date: ${a.date}` + } + } + + if (reason) { + rejected.push({ entry: a, reason }) + continue + } + + const n = norm(a.rule) + + // Duplicate of an existing entry not being removed. + if (ex.some((e) => norm(e.rule) === n && !remove.includes(e.id))) { + reason = `rule text is a duplicate of an existing entry not listed in remove` + rejected.push({ entry: a, reason }) + continue + } + + // Duplicate of an earlier addition in the same response. + if (acceptedRules.has(n)) { + reason = `rule text is a duplicate of an earlier addition in this delta` + rejected.push({ entry: a, reason }) + continue + } + + // Names a PR, URL, person, or docs page. The URL clause keeps docs-check-links.yml green. + if (String(a.rule).match(/#\d{2,}|https?:\/\/|@[A-Za-z0-9-]|packages\/kilo-docs|\.md\b/)) { + reason = "rule names a PR, URL, person, or docs page" + rejected.push({ entry: a, reason }) + continue + } + + // Duplicate of a rule deleted in this window. + if (deleted.some((d) => norm(d) === n)) { + reason = "rule text matches a line a maintainer deleted in this window" + rejected.push({ entry: a, reason }) + continue + } + + acceptedIds.add(a.id) + acceptedRules.add(n) + valid.push({ + id: a.id, + rule: clean(String(a.rule)).replaceAll("\n", " "), + scope: a.scope, + source: a.source, + date: a.date, + }) + } + + return { add: valid, remove: toRemove, rejected } +} + +/** Apply a validated delta to an existing entry array. Drops removed ids, appends adds. */ +export function applyDelta(existing, delta) { + const ex = Array.isArray(existing) ? existing : [] + const remove = new Set(Array.isArray(delta.remove) ? delta.remove : []) + const add = Array.isArray(delta.add) ? delta.add : [] + return [...ex.filter((e) => !remove.has(e.id)), ...add] +} + +/** Trust a review comment whose author_association is OWNER, MEMBER, or COLLABORATOR and is not a bot. */ +export function isTrustedComment(comment) { + if (!comment) return false + const login = String(comment.user?.login ?? "") + if (login.endsWith("[bot]")) return false + return ["OWNER", "MEMBER", "COLLABORATOR"].includes(comment.author_association) +} + +/** Render the prompt block for a given scope. Returns "" when no entry matches. */ +export function promptBlock(entries, scope) { + const matches = (Array.isArray(entries) ? entries : []).filter((e) => e.scope === scope || e.scope === "both") + if (matches.length === 0) return "" + return [ + "## Learnings from maintainer corrections", + "", + "Follow every rule below. Each was extracted from a correction a maintainer made to an", + "earlier run of this bot. A rule here outranks a general instruction above when they conflict.", + "", + ...matches.map((e) => `- ${e.rule}`), + ].join("\n") +} + +/** Read a prompt block artifact from docs-sync-out. Returns the content or "" when absent. */ +export function readLearningsBlock(scope) { + const file = `${OUT_DIR}/learnings-${scope}.md` + try { + return fs.readFileSync(file, "utf8") + } catch { + return "" + } +} + +// --- helpers for main --- + +function git(args) { + return execFileSync("git", args, { stdio: ["ignore", "pipe", "inherit"] }) + .toString() + .trim() +} + +// --- main --- + +async function main() { + // Step 0: ensure docs-sync-out exists. collect.mjs:139 is the only other unconditional + // mkdirSync of this directory, and it runs after the learn step. Without this line the + // empty-candidate path throws ENOENT on its first write, continue-on-error swallows it, + // and the feature silently never works. + fs.mkdirSync(OUT_DIR, { recursive: true }) + + if (process.argv.includes("--apply")) { + await apply() + return + } + + await extract() +} + +// --- apply mode --- + +async function apply() { + const learningsPath = `${OUT_DIR}/learnings.json` + if (!fs.existsSync(learningsPath)) { + log("learnings.json absent — extraction was skipped or failed; nothing to apply") + return + } + const entries = JSON.parse(fs.readFileSync(learningsPath, "utf8")) + const file = renderLearnings(entries) + fs.writeFileSync(LEARNINGS_FILE, file) + log(`wrote ${LEARNINGS_FILE} with ${entries.length} entries`) +} + +// --- extraction mode --- + +async function extract() { + // Step 0: seed the prompt artifacts from the checked-out file before any fallible + // work. Every later step can throw, the workflow step is continue-on-error, and + // triage and edit read only these two files. Without the seed one failed API call + // silently drops every learned rule for the whole run. Later steps replace them + // with the rolling-branch copy and then with the validated delta. + writePromptArtifacts(parseLearnings(readFileOrEmpty(LEARNINGS_FILE))) + + // Load fixture when DOCS_SYNC_FIXTURE is set. + const fixturePath = process.env.DOCS_SYNC_FIXTURE + let fixture = null + let patchFile = null + if (fixturePath) { + fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8")) + patchFile = fixturePath + ".patched" + } + + const { api, repo, searchIssues, appendOutput, appendSummary, backoffMsForAttempt, runKilo, sleepSync } = + await import("./lib.mjs") + + let prData + let prBody = "" + let prNumber = "" + let branch = "" + + if (fixture) { + // Fixture mode: skip all API calls. + prData = fixture.pr + prBody = prData.body ?? "" + prNumber = String(prData.number ?? 1) + branch = prData.head?.ref ?? "docs/auto-sync" + } else { + // Step 1: resolve the rolling PR. Use prepare-branch.mjs's selection rule so both + // target the same branch. searchIssues takes prs[0] with no author filter (like + // prepare-branch.mjs:69). But trust the body marker only when authored by + // github-actions[bot] (like watermark.mjs:35). The two rules differ on purpose: + // the branch must match what prepare-branch.mjs will check out, but a body is + // editable so its marker needs the author filter. + const r = repo() + const prs = await searchIssues(`repo:${r} is:pr is:open label:auto-docs sort:created-desc`, { maxPages: 1 }) + if (prs.length === 0) { + log("no open rolling pull request — nothing to learn from") + + // Read existing learnings from main for empty-state artifacts. + let existing = [] + try { + const existingText = git(["show", `origin/main:${LEARNINGS_FILE}`]) + existing = parseLearnings(existingText) + } catch { + existing = [] + } + log(`no-PR existing entries from main: ${existing.length}`) + writeEmptyStateArtifacts(existing) + appendOutput("count", String(existing.length)) + appendSummary("### docs-sync learnings\n\nNo open auto-docs pull request; extraction skipped.") + return + } + prData = await api(`/repos/${r}/pulls/${prs[0].number}`) + prBody = prData.body ?? "" + prNumber = String(prData.number) + branch = prData.head?.ref ?? "docs/auto-sync" + } + + // Step 2: read existing entries. + let existing = [] + let existingText = "" + if (fixture) { + existingText = readFileOrEmpty(LEARNINGS_FILE) + existing = parseLearnings(existingText) + } else { + try { + existingText = git(["show", `origin/${branch}:${LEARNINGS_FILE}`]) + } catch { + // branch copy absent — fall back to main, then empty. + // Required for the first live run: the rolling branch predates the seeded file. + try { + existingText = git(["show", `origin/main:${LEARNINGS_FILE}`]) + } catch { + existingText = "" + } + } + existing = parseLearnings(existingText) + } + log(`existing entries: ${existing.length}`) + + // Replace the seed with the rolling-branch copy. Every step below can throw, and + // these two files are all triage and edit read. + writePromptArtifacts(existing) + + // Step 3: parse marker. Trust only when authored by github-actions[bot] (like watermark.mjs:35). + let commitWm = null + let commentWm = null + const trusted = prData.user?.login === "github-actions[bot]" + if (trusted) { + ;({ commit: commitWm, comment: commentWm } = parseLearnedThrough(prBody)) + } else { + log("PR author is not github-actions[bot]; ignoring body marker") + } + log(`watermark: commit=${commitWm ?? "none"} comment=${commentWm ?? "none"}`) + + // Step 4: fetch and tip SHA. + let tipSha + if (fixture) { + tipSha = git(["rev-parse", "HEAD"]) + } else { + git(["fetch", "origin", "main", branch]) + tipSha = git(["rev-parse", `origin/${branch}`]) + } + + // Step 5: candidate commits. + let rangeArgs = [`origin/main..origin/${branch}`] + if (fixture) { + // In fixture mode, work from the local repo state. + try { + git(["rev-parse", "--verify", branch]) + rangeArgs = [`origin/main..${branch}`] + } catch { + rangeArgs = [`origin/main..HEAD`] + } + } + + if (commitWm) { + let wmExists = false + try { + git(["cat-file", "-e", `${commitWm}^{commit}`]) + wmExists = true + } catch { + wmExists = false + } + if (wmExists) { + rangeArgs.push(`^${commitWm}`) + } + // A missing watermark commit (force-push, rebase) drops the exclusion. + // The duplicate-rule-text rejection in validateDelta blocks the re-added duplicate. + } + + const logOut = git(["log", "--no-merges", "--format=%H|%ae|%cI|%s", ...rangeArgs]) + const rawCommits = logOut ? logOut.split("\n").filter(Boolean) : [] + + const botEmail = "41898282+github-actions[bot]@users.noreply.github.com" + const candidates = [] + const candidateSources = [] + const deletedInWindow = [] + + for (const line of rawCommits) { + const [sha, email, dateIso] = line.split("|") + // Drop commits authored by the sync job itself (criterion 5). + if (email === botEmail) continue + // Everything reachable from main is already excluded by the range (criterion 6). + + // Get the full file list. + let files = [] + try { + const out = git(["show", "--name-only", "--format=", sha]) + files = out + ? out + .split("\n") + .filter(Boolean) + .filter((f) => f) + : [] + } catch { + continue + } + + // Get the docs-scoped diff and message. + let message = "" + let docDiff = "" + try { + message = git(["show", "--format=%B", "--no-patch", sha]).trim() + docDiff = git(["show", "--format=", sha, "--", "packages/kilo-docs"]) + // Cap diff sizes. + if (docDiff.length > 20000) docDiff = docDiff.slice(0, 20000) + "\n[truncated]" + } catch { + // skip on error + } + + // Drop commits whose docs-scoped diff is empty. + if (!docDiff.trim()) continue + + // Collect deleted rule lines from LEARNINGS.md. + for (const dl of docDiff.split("\n")) { + if (!dl.startsWith("-")) continue + const stripped = dl.slice(1).trim() + const parsed = stripped.match(LINE_RE) + if (parsed) { + deletedInWindow.push(clean(parsed.groups.rule).replaceAll("\n", " ")) + } + } + + // Cap total diff data. + const totalDiff = candidates.reduce((n, c) => n + (c.diff ? c.diff.length : 0), 0) + if (totalDiff > 120000) { + log(`diff cap reached at commit ${sha.slice(0, 7)}; truncating`) + candidates.push({ + source: `commit:${sha.slice(0, 7)}`, + iso: dateIso, + date: dateIso.slice(0, 10), + message, + files, + diff: "[truncated]", + }) + candidateSources.push(`commit:${sha.slice(0, 7)}`) + break + } + + candidates.push({ + source: `commit:${sha.slice(0, 7)}`, + iso: dateIso, + date: dateIso.slice(0, 10), + message, + files, + diff: docDiff, + }) + candidateSources.push(`commit:${sha.slice(0, 7)}`) + } + + // Step 6: candidate comments. + let allComments = [] + let maxCommentAt = "none" + + if (fixture && fixture.comments) { + allComments = fixture.comments + } else if (prNumber) { + const pages = [] + for (let page = 1; page <= 5; page++) { + const batch = await api(`/repos/${repo()}/pulls/${prNumber}/comments?per_page=100&page=${page}`) + pages.push(...batch) + if (batch.length < 100) break + } + allComments = pages + } + + if (allComments.length > 0) { + let max = "" + for (const c of allComments) { + if (c.created_at && c.created_at > max) max = c.created_at + } + maxCommentAt = max || "none" + } + + // Filter trusted comments. + const trustedComments = allComments.filter((c) => { + if (!isTrustedComment(c)) return false + if (commentWm && c.created_at <= commentWm) return false + return true + }) + + // Step 7: correlate comments to commits. + // A comment is a commit's trigger when c.path is in that commit's full file list + // and c.created_at < commit date. The earliest such commit claims it. + // Compare parsed timestamps so different timezone offsets do not skew the ordering. + for (const c of trustedComments) { + let best = null + const cTime = Date.parse(c.created_at) + for (const cc of candidates) { + if (!Array.isArray(cc.files) || !cc.files.includes(c.path)) continue + const ccTime = Date.parse(cc.iso) + if (cTime < ccTime) { + if (!best || ccTime < Date.parse(best.iso)) { + best = cc + } + } + } + if (best) { + best.comment = { + author_association: c.author_association, + path: c.path, + body: capBody(c.body), + } + } else { + candidates.push({ + source: `comment:${c.id}`, + date: (c.created_at ?? "").slice(0, 10), + path: c.path, + body: capBody(c.body), + author_association: c.author_association, + }) + candidateSources.push(`comment:${c.id}`) + } + } + + // Step 8: no candidates → empty delta route. + const hasCandidates = candidates.length > 0 + + if (!hasCandidates) { + log("no candidate corrections; advancing marker with no model call") + writeEmptyStateArtifacts(existing) + appendOutput("count", String(existing.length)) + appendSummary( + `### docs-sync learnings\n\nNo new candidate corrections. Entries: ${existing.length}. Marker route: empty (no candidates).`, + ) + + const marker = renderLearnedThrough({ commit: tipSha, comment: maxCommentAt }) + await patchOrLogMarker({ prBody, prNumber, marker, fixture, patchFile }) + return + } + + // Step 9: write learnings input. + const input = { + existing: existing.map((e) => ({ id: e.id, rule: e.rule, scope: e.scope, source: e.source, date: e.date })), + deleted_in_window: deletedInWindow, + corrections: candidates, + } + const inputFile = `${OUT_DIR}/learnings-input.json` + fs.writeFileSync(inputFile, JSON.stringify(input, null, 2)) + log(`wrote ${inputFile} with ${candidates.length} candidates`) + + // Step 10: call the model. + // Deliberately no --auto. Every input is in the attached file and the output goes to + // stdout, so the agent needs no tool. Omitting --auto makes "the extraction step never + // writes outside LEARNINGS.md" structurally true instead of prompt-deep. triage.mjs:76 + // and edit.mjs:86 carry the opposite comment; do not copy them without updating the reason. + const prompt = fs.readFileSync(path.join(HERE, "learnings-prompt.md"), "utf8") + const model = process.env.TRIAGE_MODEL + if (!model) throw new Error("TRIAGE_MODEL is required") + + const budgetDeadline = Date.now() + EXTRACTION_TIMEOUT_MS + + let raw = null + let lastCause = "extraction failed" + + for (let attempt = 1; attempt <= ATTEMPTS; attempt++) { + const left = Math.max(0, budgetDeadline - Date.now()) + if (left <= 0) { + log("budget exhausted before extraction attempt") + break + } + + const result = runKilo({ + args: ["run", prompt, "-m", model, "--dir", process.cwd(), "-f", inputFile], + timeoutMs: Math.min(EXTRACTION_TIMEOUT_MS, left), + streamStdout: false, + label: "learnings extraction", + }) + + if (result.stdout) { + fs.writeFileSync(`${OUT_DIR}/learnings-raw.txt`, result.stdout) + raw = result.stdout + const delta = parseDelta(raw) + if (delta) break + lastCause = `parseDelta returned null (attempt ${attempt})` + } else { + lastCause = result.timedOut ? "timed out" : `exit ${result.exitCode}` + } + + if (attempt < ATTEMPTS) { + const wait = backoffMsForAttempt(1) // 60s, same as the sibling convention + if (wait > 0) { + log(`backing off ${wait / 1000}s before attempt ${attempt + 1}`) + sleepSync(wait) + } + } + } + + // Step 11: parse and validate. + const delta = raw ? parseDelta(raw) : null + + if (!delta) { + // parseDelta null after every try — retryable unhappy. + warn(`extraction failed: ${lastCause}. Leaving learnings untouched.`) + writeEmptyStateArtifacts(existing) + appendOutput("count", String(existing.length)) + appendSummary( + `### docs-sync learnings\n\nExtraction failed: ${lastCause}. Entries unchanged: ${existing.length}. No marker advance.`, + ) + return + } + + const validated = validateDelta(delta, { existing, candidateSources, deletedInWindow }) + + if (validated.rejected.length > 0) { + for (const r of validated.rejected) { + warn(`rejected: ${r.reason}` + (r.entry?.id ? ` (id=${r.entry.id})` : "")) + } + } + + const nonEmpty = validated.add.length > 0 || validated.remove.length > 0 + + // Step 12: route by outcome (G5 table, exact). + if (nonEmpty) { + // Non-empty validated delta. + const newEntries = applyDelta(existing, { add: validated.add, remove: validated.remove }) + fs.writeFileSync(`${OUT_DIR}/learnings.json`, JSON.stringify(newEntries, null, 2)) + const marker = renderLearnedThrough({ commit: tipSha, comment: maxCommentAt }) + const suppressed = process.env.DRY_RUN === "true" || process.env.LEARNINGS_NO_PATCH === "1" + if (!suppressed) appendOutput("learned_through", marker) + if (suppressed) log(`learned-through output suppressed: ${marker}`) + + const added = validated.add.length + const removed = validated.remove.length + const rejected = validated.rejected.length + log(`delta: +${added} -${removed} (${rejected} rejected)`) + appendSummary( + `### docs-sync learnings\n\n- added: ${added}\n- removed: ${removed}\n- rejected: ${rejected}\n- candidates: ${candidates.length}\n- marker route: upsert (non-empty delta)\n`, + ) + + writePromptArtifacts(newEntries) + appendOutput("count", String(newEntries.length)) + + // Marker rides through LEARNED_THROUGH into upsert-pr.mjs. No direct PATCH. + } else { + // Empty validated delta (nothing added, nothing removed, including every-add-rejected). + log("empty validated delta; advancing marker directly") + fs.writeFileSync(`${OUT_DIR}/learnings.json`, JSON.stringify(existing, null, 2)) + writePromptArtifacts(existing) + appendOutput("count", String(existing.length)) + + const marker = renderLearnedThrough({ commit: tipSha, comment: maxCommentAt }) + await patchOrLogMarker({ prBody, prNumber, marker, fixture, patchFile }) + + const rejected = validated.rejected.length + appendSummary( + `### docs-sync learnings\n\n- added: 0\n- removed: 0\n- rejected: ${rejected}\n- candidates: ${candidates.length}\n- marker route: direct PATCH (empty delta)\n`, + ) + } +} + +// --- shared helpers --- + +function writeEmptyStateArtifacts(entries) { + fs.writeFileSync(`${OUT_DIR}/learnings.json`, JSON.stringify(entries, null, 2)) + writePromptArtifacts(entries) +} + +// A later call must be able to shrink a seeded block back to nothing, so an empty +// block removes the file instead of leaving the earlier content in place. +function writePromptArtifacts(entries) { + writeOrRemove(`${OUT_DIR}/learnings-triage.md`, promptBlock(entries, "triage")) + writeOrRemove(`${OUT_DIR}/learnings-edit.md`, promptBlock(entries, "edit")) +} + +function writeOrRemove(file, text) { + if (text) fs.writeFileSync(file, text) + else fs.rmSync(file, { force: true }) +} + +function readFileOrEmpty(file) { + try { + return fs.readFileSync(file, "utf8") + } catch { + return "" + } +} + +async function patchOrLogMarker({ prBody, prNumber, marker, fixture, patchFile }) { + const suppressed = process.env.DRY_RUN === "true" || process.env.LEARNINGS_NO_PATCH === "1" + + if (suppressed) { + log( + `marker PATCH suppressed (DRY_RUN=${process.env.DRY_RUN}, LEARNINGS_NO_PATCH=${process.env.LEARNINGS_NO_PATCH})`, + ) + log(`would have written marker: ${marker}`) + return + } + + if (fixture) { + // Write to the fixture patch file instead of the network. + fs.writeFileSync(patchFile, marker) + log(`wrote marker to ${patchFile}`) + return + } + + // Live PATCH: body-only, one line changed. The job already holds pull-requests: write. + // Re-read the body first. The body in hand was fetched before the extraction call, so + // patching that copy would drop any edit made in the minutes since. GitHub has no + // conditional update for a pull request body, so a short fetch-to-PATCH race remains. + const { api, repo } = await import("./lib.mjs") + let latestBody = prBody + try { + const fresh = await api(`/repos/${repo()}/pulls/${prNumber}`) + latestBody = fresh.body ?? "" + } catch (err) { + warn(`could not re-read PR #${prNumber} before the marker PATCH: ${err.message}. Using the earlier body.`) + } + const newBody = patchMarkerIntoBody(latestBody, marker) + await api(`/repos/${repo()}/pulls/${prNumber}`, { + method: "PATCH", + body: { body: newBody }, + }) + log(`PATCHed learned-through marker on PR #${prNumber}`) +} + +// --- entry point --- + +const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href + +// --- self-test harness (run: node .github/docs-sync/learn.mjs --self-test) --- +if (isMain && process.argv.includes("--self-test")) { + const failures = [] + const check = (label, fn) => { + try { + const ok = fn() + if (!ok) failures.push(label) + } catch (e) { + failures.push(label + " THREW: " + e.message) + } + } + + check("null in add does not throw", () => { + const r = validateDelta({ add: [null], remove: [] }, { existing: [], candidateSources: [], deletedInWindow: [] }) + return r.add.length === 0 && r.rejected.length === 1 && r.rejected[0].reason.includes("not a plain object") + }) + + check("undefined in add does not throw", () => { + const r = validateDelta( + { add: [undefined], remove: [] }, + { existing: [], candidateSources: [], deletedInWindow: [] }, + ) + return r.add.length === 0 && r.rejected.length === 1 && r.rejected[0].reason.includes("not a plain object") + }) + + check("mixed valid and null retains valid", () => { + const r = validateDelta( + { + add: [ + { + id: "valid-a", + rule: "Do not document experimental features", + scope: "both", + source: "commit:bbbbbbb", + date: "2026-08-03", + }, + null, + { + id: "valid-b", + rule: "Keep release notes concise", + scope: "edit", + source: "commit:bbbbbbb", + date: "2026-08-03", + }, + ], + remove: [], + }, + { existing: [], candidateSources: ["commit:bbbbbbb"], deletedInWindow: [] }, + ) + return r.add.length === 2 && r.rejected.length === 1 + }) + + if (failures.length) { + console.error("SELF-TEST FAILURES:", failures) + process.exit(1) + } + console.log("SELF-TEST PASSED (" + 3 + " checks)") + process.exit(0) +} + +if (isMain) { + main().catch((err) => { + console.error(err) + process.exit(1) + }) +} diff --git a/.github/docs-sync/learnings-prompt.md b/.github/docs-sync/learnings-prompt.md new file mode 100644 index 00000000000..cde193b8d4f --- /dev/null +++ b/.github/docs-sync/learnings-prompt.md @@ -0,0 +1,84 @@ +You are the extraction pass of an automated documentation pipeline for Kilo Code. Your only job: extract general rules of thumb from maintainer corrections to the docs-sync bot's rolling pull request. A correction is a commit or review comment a maintainer made to fix something the bot got wrong, and a learning is the general principle behind it that the bot should follow from now on. + +The attached `learnings-input.json` file contains: + +- `existing`: rules the bot already knows, each with `id`, `rule`, `scope`, `source`, and `date`. +- `deleted_in_window`: rule texts (not ids) a maintainer deleted from the learnings file in this extraction window. A maintainer deleted these on purpose — do not re-add them. +- `corrections`: the maintainer corrections to learn from. Each entry has a `source` (commit or comment id), `date`, and the relevant context. Commit entries have `message`, `files`, and `diff`. Comment entries have `path` and `body`. Some commits also carry an attached inline review `comment` that triggered them. + +Before writing anything: + +1. Read every correction in `corrections` and every rule in `existing`. +2. For each correction, decide whether it implies a general rule of thumb the bot should follow. Not every correction does — returning no new rules is a valid and expected answer. +3. When a correction implies a rule, write it as one imperative sentence stating the general principle, not what the specific correction did. + +Response format: a strict JSON object with no prose, no markdown fences, no comments: + +```json +{ + "add": [ + { + "id": "kebab-case-slug", + "rule": "One imperative sentence.", + "scope": "triage|edit|both", + "source": "commit:|comment:", + "date": "" + } + ], + "remove": [] +} +``` + +- `id`: a short kebab-case slug unique across this response. +- `rule`: one general imperative sentence. Never name a pull request, a number, a URL, a docs page, a file path, or a person. State the rule the correction implies, not what the correction changed. +- `scope`: `triage` when the rule changes which pull requests deserve documentation; `edit` when it changes how a page is written; `both` when it changes both. +- `source`: copied verbatim from the correction's `source` field. Never invent one. +- `date`: the correction's date, copied verbatim. + +The `remove` array lists `id` values of existing entries to drop. Remove an id only when a new rule contradicts or supersedes it. + +Hard rules: + +- The list of `add` entries may be empty. Returning `{"add": [], "remove": []}` is a valid and expected answer when no correction implies a general rule. +- Never re-add a rule listed in `deleted_in_window`, and never add a reworded near-duplicate of one. A maintainer deleted it. +- When a new rule is a near-duplicate of an existing one, merge them into one `add` and list the old id in `remove`. +- When a new rule contradicts an existing rule, `add` the new one and `remove` the contradicted id. +- Every `add` entry must have a `source` that appears in the input's `corrections` list. Never invent a source. +- Do not read files and do not run commands. Every input is already attached. + +Example. Input: + +```json +{ + "existing": [], + "deleted_in_window": [], + "corrections": [ + { + "source": "commit:9dd2c07", + "date": "2026-08-03", + "message": "docs: remove experimental features page", + "files": ["packages/kilo-docs/pages/code-with-ai/experimental-features.md"], + "diff": "- removed the entire experimental features page\n- the page documented features behind unreleased flags" + } + ] +} +``` + +Expected output: + +```json +{ + "add": [ + { + "id": "no-experimental-features", + "rule": "Do not document features that are behind unreleased flags.", + "scope": "both", + "source": "commit:9dd2c07", + "date": "2026-08-03" + } + ], + "remove": [] +} +``` + +The rule is `both` because documenting unreleased features is wrong at triage time (the feature is not docs-worthy yet) and at edit time (the page should not exist). diff --git a/.github/docs-sync/lib.mjs b/.github/docs-sync/lib.mjs new file mode 100644 index 00000000000..f63155adf66 --- /dev/null +++ b/.github/docs-sync/lib.mjs @@ -0,0 +1,268 @@ +// kilocode_change - new file + +/** + * Shared helpers for the docs-sync bot scripts. Dependency-free (Node 20+ + * global fetch) so the workflow does not rely on runner images shipping the + * gh CLI. + */ + +import { spawnSync } from "node:child_process" +import fs from "node:fs" + +// Test hook: DOCS_SYNC_API_BASE points the API at a local stub server. The workflow +// never sets it — only selftests do. +const API = process.env.DOCS_SYNC_API_BASE || "https://api.github.com" +const MAX_RETRIES = 3 + +export function token() { + const t = process.env.GH_TOKEN || process.env.GITHUB_TOKEN + if (!t) throw new Error("GH_TOKEN (or GITHUB_TOKEN) is required") + return t +} + +export function repo() { + const r = process.env.GITHUB_REPOSITORY + if (!r) throw new Error("GITHUB_REPOSITORY is required") + return r +} + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) + +export async function api(path, { method = "GET", body } = {}) { + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + let res + try { + res = await fetch(`${API}${path}`, { + method, + headers: { + authorization: `Bearer ${token()}`, + accept: "application/vnd.github+json", + "x-github-api-version": "2022-11-28", + "user-agent": "kilo-docs-sync-bot", + }, + body: body === undefined ? undefined : JSON.stringify(body), + }) + } catch (err) { + if (attempt < MAX_RETRIES) { + console.warn(`network error (${err.message}), retrying in ${5 * attempt}s`) + await sleep(5000 * attempt) + continue + } + throw err + } + + if (res.status === 403) { + const text = await res.text() + if (text.includes("rate limit") && attempt < MAX_RETRIES) { + const retryAfter = Number(res.headers.get("retry-after")) || 30 + console.warn(`rate limited, retrying in ${retryAfter}s`) + await sleep(retryAfter * 1000) + continue + } + const err = new Error(`${method} ${path} -> 403: ${text}`) + err.status = 403 + throw err + } + + if (res.status >= 500 && attempt < MAX_RETRIES) { + console.warn(`${method} ${path} -> ${res.status}, retrying in ${5 * attempt}s`) + await sleep(5000 * attempt) + continue + } + + if (!res.ok) { + const text = await res.text() + const err = new Error(`${method} ${path} -> ${res.status}: ${text}`) + err.status = res.status + throw err + } + + if (res.status === 204) return null + return res.json() + } + throw new Error(`${method} ${path}: exhausted retries`) +} + +/** Paginated search/issues. Caps at `maxPages` * 100 results. */ +export async function searchIssues(query, { maxPages = 5 } = {}) { + const items = [] + for (let page = 1; page <= maxPages; page++) { + const data = await api(`/search/issues?q=${encodeURIComponent(query)}&per_page=100&page=${page}`) + items.push(...(data.items ?? [])) + if ((data.items ?? []).length < 100) break + } + return items +} + +export async function listPrFiles(fullRepo, number, { maxPages = 3 } = {}) { + const files = [] + for (let page = 1; page <= maxPages; page++) { + const batch = await api(`/repos/${fullRepo}/pulls/${number}/files?per_page=100&page=${page}`) + files.push(...batch) + if (batch.length < 100) break + } + return files +} + +export function appendOutput(name, value) { + const out = process.env.GITHUB_OUTPUT + if (out) fs.appendFileSync(out, `${name}=${value}\n`) + console.log(`output ${name}=${value}`) +} + +export function appendSummary(markdown) { + const summary = process.env.GITHUB_STEP_SUMMARY + if (summary) fs.appendFileSync(summary, markdown + "\n") +} + +/** + * Absolute deadline timestamp (ms since epoch) for a wall-clock budget. + * Used by triage/edit to stop before the job timeout rather than silently + * truncating. + */ +export function deadline(minutes) { + return Date.now() + Number(minutes) * 60 * 1000 +} + +/** Remaining milliseconds until a deadline; never negative. */ +export function remainingMs(deadlineMs) { + return Math.max(0, Number(deadlineMs) - Date.now()) +} + +/** + * Backoff schedule between kilo-run attempts. Production waits 60s then 300s + * (observed outage lasted ~11 min; batch 8 recovered on attempt 2). When + * DOCS_SYNC_BACKOFF_MS is set it replaces EVERY wait (`0` disables waiting); + * the workflow never sets it — only selftests do. + */ +export function backoffMsForAttempt(attempt) { + // attempt is 1-based; wait happens after attempt N before attempt N+1. + const override = process.env.DOCS_SYNC_BACKOFF_MS + if (override !== undefined && override !== "") { + const n = Number(override) + return Number.isFinite(n) && n >= 0 ? n : 0 + } + // After attempt 1 → 60s; after attempt 2 → 300s; nothing after the last. + if (attempt === 1) return 60_000 + if (attempt === 2) return 300_000 + return 0 +} + +/** + * Blocking sleep used between kilo-run retries. Prefer this over async sleep + * so edit/triage stay synchronous around spawnSync. + */ +export function sleepSync(ms) { + const n = Number(ms) + if (!Number.isFinite(n) || n <= 0) return + const end = Date.now() + n + // Atomics.wait is the portable Node sync sleep (no busy loop). + const sab = new SharedArrayBuffer(4) + const view = new Int32Array(sab) + while (Date.now() < end) { + const left = end - Date.now() + if (left <= 0) break + Atomics.wait(view, 0, 0, Math.min(left, 2_147_483_647)) + } +} + +const STDERR_TAIL_LINES = 20 +const STDERR_TAIL_CHARS = 4_000 + +// CSI sequences (colour, cursor moves, erases). kilo renders its TUI to stderr, +// so an unstripped tail lands in the rolling PR's pending table as +// "^[[0m→ ^[[0mRead packages/..." and the cause is unreadable. Stripped before +// the line/char slice so escapes do not eat the budget. The persisted +// docs-sync-out/kilo-stderr-*.log stays raw — that is the debugging record. +// eslint-disable-next-line no-control-regex +const ANSI_CSI = /\u001b\[[0-9;?]*[ -/]*[@-~]/g + +function tailText(text, { lines = STDERR_TAIL_LINES, chars = STDERR_TAIL_CHARS } = {}) { + const s = String(text ?? "") + .replace(ANSI_CSI, "") + .trim() + if (!s) return "" + const lastLines = s.split("\n").slice(-lines).join("\n") + return lastLines.length > chars ? lastLines.slice(-chars) : lastLines +} + +/** + * Artifact files are raw: GitHub masks secret values in log streams only, and the runner + * env holds long-lived secrets (KILO_API_KEY), so exact values of secret-looking env vars + * are redacted before stdout/stderr is persisted or printed. + * Matching is exact-substring and case-sensitive on values — JSON-escaped, base64'd, or + * line-wrapped renderings and values shorter than 8 chars survive (same limitation as + * GitHub's own log masking); this is defense-in-depth, not a guarantee the logs are clean. + */ +export function redactEnvSecrets(text) { + let out = String(text ?? "") + // Also match CREDENTIAL/PASSWORD/ORG_ID/_PAT (e.g. KILO_ORG_ID, GH_PAT) beyond KEY|TOKEN|SECRET. + const nameRe = /KEY|TOKEN|SECRET|CREDENTIAL|PASSWORD|ORG_ID|_PAT$/i + const candidates = [] + for (const [name, value] of Object.entries(process.env)) { + if (!nameRe.test(name)) continue + if (typeof value !== "string" || value.length < 8) continue + candidates.push(value) + } + // Longer values first so a shorter secret that is a prefix of a longer one cannot leave a remainder. + candidates.sort((a, b) => b.length - a.length) + for (const value of candidates) { + if (!out.includes(value)) continue + out = out.split(value).join("***") + } + return out +} + +/** Max bytes of child stderr persisted to docs-sync-out/ (full buffer, not the console tail). */ +const STDERR_LOG_MAX_CHARS = 8 * 1024 * 1024 + +/** + * Run `kilo` via spawnSync so stderr is always recoverable — including when + * the child exits 0 after writing a diagnostic (execFileSync cannot return + * piped stderr on exit 0; that path lost every diagnostic on run 30122603016). + * + * streamStdout:true → inherit fd 1 (edit live log); false → capture stdout + * (triage parses it). stderr is always buffered. + * + * Always writes the full captured stderr to + * docs-sync-out/kilo-stderr-.log (unconditional — success and + * failure). The console return value still uses the short tailText. + */ +export function runKilo({ args, timeoutMs, streamStdout = false, label = "kilo" }) { + const result = spawnSync("kilo", args, { + encoding: "utf8", + maxBuffer: 32 * 1024 * 1024, + timeout: timeoutMs, + stdio: ["ignore", streamStdout ? "inherit" : "pipe", "pipe"], + }) + + const timedOut = Boolean(result.error && result.error.code === "ETIMEDOUT") + const exitCode = + typeof result.status === "number" ? result.status : timedOut ? null : result.status === null ? null : result.status + const stderrRaw = String(result.stderr ?? "") + const stderrSafe = redactEnvSecrets(stderrRaw) + const stderrTail = tailText(stderrSafe) + const stdoutSafe = streamStdout ? "" : redactEnvSecrets(String(result.stdout ?? "")) + // ok is "process finished without OS-level failure". Callers still treat a + // missing summary / unparseable output as failure even when ok is true — + // exit 0 is not success for the docs-sync bot. + const ok = !result.error && result.status === 0 + + // Persist full stderr on every call (not gated on ok/exitCode/summary). Cap is + // generous (megabytes) so long batch dumps keep auto-rejecting lines; console + // still uses the short tail above. + try { + fs.mkdirSync("docs-sync-out", { recursive: true }) + const safe = label.replace(/[^A-Za-z0-9._-]/g, "-") + const body = stderrSafe.length > STDERR_LOG_MAX_CHARS ? stderrSafe.slice(-STDERR_LOG_MAX_CHARS) : stderrSafe + fs.writeFileSync(`docs-sync-out/kilo-stderr-${safe}.log`, body) + } catch (err) { + console.warn(`${label}: failed to write kilo-stderr log: ${err.message}`) + } + + if (result.error && !timedOut) { + console.warn(`${label}: spawn error: ${result.error.message}`) + } + + return { ok, stdout: stdoutSafe, stderrTail, exitCode, timedOut } +} diff --git a/.github/docs-sync/prepare-branch.mjs b/.github/docs-sync/prepare-branch.mjs new file mode 100644 index 00000000000..2710832ccf3 --- /dev/null +++ b/.github/docs-sync/prepare-branch.mjs @@ -0,0 +1,105 @@ +// kilocode_change - new file + +/** + * Prepares the rolling docs-sync branch before the edit pass: + * - an open auto-docs PR exists -> check out its head branch and merge + * origin/main (preserves any human commits on the branch) + * - otherwise -> fresh branch from origin/main (bot force-pushes later) + * + * Outputs: branch, mode (update|fresh|conflict), pr_number (empty when fresh). + */ + +import { execFileSync } from "node:child_process" +import { pathToFileURL } from "node:url" +import { api, appendOutput, repo, searchIssues } from "./lib.mjs" + +export const DEFAULT_BRANCH = "docs/auto-sync" + +const defaultGit = (args) => execFileSync("git", args, { stdio: ["ignore", "pipe", "inherit"] }).toString().trim() + +/** + * Merge origin/main into the current branch. On a genuine conflict, abort the + * merge, switch to a dated fallback branch from origin/main, and return + * mode=conflict so human commits on the rolling branch stay untouched. Any + * other merge failure (missing identity, corrupt ref, fetch issues) is + * rethrown so the job fails loudly. + */ +export function mergeOrFallback({ branch, git = defaultGit }) { + try { + git(["merge", "origin/main", "--no-edit"]) + return { branch, mode: "update" } + } catch (err) { + // Conflict ⇔ unmerged index entries (or MERGE_HEAD still present). + // Identity failures and similar abort before a merge is started, so + // merge --abort would itself fail — those must rethrow. + let unmerged = "" + try { + unmerged = git(["ls-files", "--unmerged"]) + } catch { + // ls-files itself failing is not a conflict signal + } + let mergeInProgress = false + try { + git(["rev-parse", "-q", "--verify", "MERGE_HEAD"]) + mergeInProgress = true + } catch { + mergeInProgress = false + } + const isConflict = unmerged.length > 0 || mergeInProgress + if (!isConflict) throw err + + console.warn(`merge of origin/main into ${branch} conflicted.`) + console.warn( + "Leaving the conflicted branch untouched so human commits are preserved; continuing on a fresh dated branch.", + ) + git(["merge", "--abort"]) + const fallback = `${DEFAULT_BRANCH}-${new Date().toISOString().slice(0, 10)}` + try { + git(["fetch", "origin", `+refs/heads/${fallback}:refs/remotes/origin/${fallback}`]) + } catch { + console.log(`dated branch ${fallback} does not exist on origin yet; will create it on push`) + } + git(["checkout", "-B", fallback, "origin/main"]) + return { branch: fallback, mode: "conflict" } + } +} + +async function main() { + const git = defaultGit + const prs = await searchIssues(`repo:${repo()} is:pr is:open label:auto-docs sort:created-desc`, { maxPages: 1 }) + + let mode = "fresh" + let prNumber = "" + let branch = DEFAULT_BRANCH + + if (prs.length > 0) { + const pr = await api(`/repos/${repo()}/pulls/${prs[0].number}`) + branch = pr.head?.ref ?? DEFAULT_BRANCH + prNumber = String(pr.number) + git(["fetch", "origin", "main", branch]) + git(["checkout", branch]) + ;({ branch, mode } = mergeOrFallback({ branch, git })) + } else { + // Keep the remote-tracking ref current so the later --force-with-lease + // push (stale branch left over from a merged/closed PR) is safe. + try { + git(["fetch", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`]) + } catch { + console.log(`branch ${branch} does not exist on origin yet; will create it on push`) + } + git(["checkout", "-B", branch, "origin/main"]) + } + + appendOutput("branch", branch) + appendOutput("mode", mode) + appendOutput("pr_number", prNumber) + console.log(`branch ${branch} ready (mode=${mode}, pr=${prNumber || "none"})`) +} + +const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href +if (isMain) { + main().catch((err) => { + console.error(err) + process.exit(1) + }) +} diff --git a/.github/docs-sync/redact-stream.mjs b/.github/docs-sync/redact-stream.mjs new file mode 100644 index 00000000000..341aea34ad7 --- /dev/null +++ b/.github/docs-sync/redact-stream.mjs @@ -0,0 +1,25 @@ +// kilocode_change - new file + +/** + * Line-wise stdin→stdout filter that redacts secret-looking env values. + * Used in the docs-sync workflow so kilo stdout piped to edit-log.txt is safe. + * Env values contain no newlines, so line-wise processing never splits a value. + */ + +import { redactEnvSecrets } from "./lib.mjs" + +let carry = "" + +process.stdin.setEncoding("utf8") +process.stdin.on("data", (chunk) => { + carry += chunk + let idx + while ((idx = carry.indexOf("\n")) !== -1) { + const line = carry.slice(0, idx + 1) + carry = carry.slice(idx + 1) + process.stdout.write(redactEnvSecrets(line)) + } +}) +process.stdin.on("end", () => { + if (carry.length > 0) process.stdout.write(redactEnvSecrets(carry)) +}) diff --git a/.github/docs-sync/reverts.mjs b/.github/docs-sync/reverts.mjs new file mode 100644 index 00000000000..5702fa37437 --- /dev/null +++ b/.github/docs-sync/reverts.mjs @@ -0,0 +1,153 @@ +// kilocode_change - new file + +/** + * Pure helpers for intercepting revert PRs during docs-sync collect. + * No I/O, no imports — offline-testable title/body/annotation logic only. + */ + +/** Detect revert PR titles. Returns "conventional" | "github-native" | null. */ +export function revertTitleKind(title) { + const t = String(title ?? "") + if (/^revert(\(.+\))?!?:/i.test(t)) return "conventional" + if (/^revert\s+["']/i.test(t)) return "github-native" + return null +} + +/** + * Parse revert targets from a PR body. `defaultRepo` ("Kilo-Org/kilocode") resolves bare `#N`. + * Returns [{ repo, number, url }] with url = `https://github.com/${repo}/pull/${number}`. + * Handles the conventional trailer form: a line starting with `Reverts` (case-insensitive), + * e.g. `Reverts #12249 and #12481.`, `Reverts Kilo-Org/cloud#42.`, comma-separated lists, + * several such lines in one body, and bulleted/quoted single-line trailers + * (`- Reverts #12249`, `> Reverts #12249`). + * NOT handled (documented limitation): `This reverts commit .` (no PR number), + * mid-sentence forms (`This reverts #5.`), narrative mentions (`Revert the fix in #99999` + * is prose, not a trailer — must NOT produce a target), `Revertsomething #5` (prose glued + * to the word — `\b` word boundary makes it inert), and multi-line lists + * (`Reverts:\n- #1\n- #2`). + */ +export function parseRevertTargets(body, defaultRepo) { + const text = String(body ?? "") + const repoDefault = String(defaultRepo ?? "") + const seen = new Set() + const out = [] + const lineRe = /^[ \t>*-]*reverts\b[ \t:]*([^\n]*)/gim + let lineMatch + while ((lineMatch = lineRe.exec(text)) !== null) { + const capture = lineMatch[1] ?? "" + const refRe = /(?:([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+))?#(\d+)/g + let refMatch + while ((refMatch = refRe.exec(capture)) !== null) { + const repo = refMatch[1] || repoDefault + const number = Number(refMatch[2]) + if (!repo || !Number.isInteger(number)) continue + const url = `https://github.com/${repo}/pull/${number}` + if (seen.has(url)) continue + seen.add(url) + out.push({ repo, number, url }) + } + } + return out +} + +/** + * revertSignals: [{ url, merged_at, targets: [{ repo, number, url }] }] + * Returns Map — the annotation for each reverted PR. + * Map keys are lowercased target urls; values keep signal.url as authored. + * Revert-of-revert: a signal whose own url is itself a target of another signal is + * dropped entirely (a re-land: net effect zero — one set lookup, no chain walking). + */ +export function computeRevertAnnotations(revertSignals) { + const signals = Array.isArray(revertSignals) ? revertSignals : [] + const revertedUrls = new Set() + for (const signal of signals) { + for (const t of signal?.targets ?? []) { + if (t?.url) revertedUrls.add(t.url.toLowerCase()) + } + } + const annotations = new Map() + for (const signal of signals) { + if (!signal?.url || revertedUrls.has(signal.url.toLowerCase())) continue + for (const t of signal.targets ?? []) { + if (!t?.url) continue + annotations.set(t.url.toLowerCase(), { url: signal.url, merged_at: signal.merged_at }) + } + } + return annotations +} + +/** + * Applies computeRevertAnnotations to digest entries in place: an entry whose url + * was reverted gains `reverted_by: { url, merged_at }` (the reverter). + * Lookup is case-insensitive; applied pairs still report entry.url (canonical). + * Returns applied [targetUrl, reverterUrl] pairs (digest entries only) for reporting. + */ +export function applyRevertAnnotations(digest, revertSignals) { + const annotations = computeRevertAnnotations(revertSignals) + const applied = [] + for (const entry of digest ?? []) { + if (!entry?.url) continue + const ann = annotations.get(entry.url.toLowerCase()) + if (!ann) continue + entry.reverted_by = { url: ann.url, merged_at: ann.merged_at } + applied.push([entry.url, ann.url]) + } + return applied +} + +/** + * Reports intercepted reverts whose targets received no digest annotation, for the + * step summary. `signals` is the same shape as computeRevertAnnotations takes; + * `appliedPairs` is applyRevertAnnotations' return ([targetUrl, reverterUrl]). + * Returns { missed, unparsed, chains }: + * - missed: [{ url, targets: [targetUrl, ...] }] — one entry per signal with at least + * one unannotated plain target, listing exactly the targets that missed (a partially-covered + * revert still surfaces its uncovered targets). Plain = not itself an intercepted revert. + * - unparsed: [url, ...] — urls of signals with zero targets (they already warn at + * intercept time). + * - chains: [{ url, targets: [targetUrl, ...] }] — signals involved in a revert chain, + * reported for human visibility only; the pipeline never resolves a chain's net effect. + * A signal lands here when EITHER its own url is a target of another signal (cancelled + * by computeRevertAnnotations — lists ALL its targets), OR at least one of its targets + * is itself an intercepted revert (re-land chain link — lists ONLY those chain-link + * targets; plain targets still flow through missed). + * Cancelled signals skip missed/unparsed. Chain-linked (non-cancelled) signals may also + * appear in missed for their unannotated plain targets. Chain-link targets never appear + * in missed. All url comparisons are case-insensitive. + */ +export function unannotatedRevertSignals(signals, appliedPairs) { + const list = Array.isArray(signals) ? signals : [] + const annotated = new Set((appliedPairs ?? []).map(([target]) => String(target ?? "").toLowerCase())) + const signalUrls = new Set(list.map((s) => String(s?.url ?? "").toLowerCase())) + const cancelledUrls = new Set() + for (const s of list) { + for (const t of s?.targets ?? []) { + if (t?.url) cancelledUrls.add(t.url.toLowerCase()) + } + } + const missed = [] + const unparsed = [] + const chains = [] + for (const s of list) { + const url = s?.url + if (!url) continue + const targets = (s.targets ?? []).map((t) => t?.url).filter(Boolean) + const cancelled = cancelledUrls.has(url.toLowerCase()) + const chainTargets = targets.filter((u) => signalUrls.has(u.toLowerCase())) + if (cancelled) { + chains.push({ url, targets }) + continue + } + if (chainTargets.length > 0) { + chains.push({ url, targets: chainTargets }) + } + if (targets.length === 0) { + unparsed.push(url) + continue + } + const plainTargets = targets.filter((u) => !signalUrls.has(u.toLowerCase())) + const missing = plainTargets.filter((u) => !annotated.has(u.toLowerCase())) + if (missing.length > 0) missed.push({ url, targets: missing }) + } + return { missed, unparsed, chains } +} diff --git a/.github/docs-sync/selftest.mjs b/.github/docs-sync/selftest.mjs new file mode 100644 index 00000000000..8341b0ef9ad --- /dev/null +++ b/.github/docs-sync/selftest.mjs @@ -0,0 +1,3499 @@ +// kilocode_change - new file + +/** + * Offline self-check for the docs-sync failure paths (S4). + * Plain node:assert, no network, no LLM, no new dependency. + * Run: node .github/docs-sync/selftest.mjs + */ + +import assert from "node:assert/strict" +import { execFileSync, spawn, spawnSync } from "node:child_process" +import fs from "node:fs" +import os from "node:os" +import path from "node:path" +import { fileURLToPath } from "node:url" + +import { sleepSync } from "./lib.mjs" +import { mergeOrFallback, DEFAULT_BRANCH } from "./prepare-branch.mjs" +import { applyCap } from "./watermark.mjs" +import { + computeUncovered, + computeProcessedThrough, + routeRows, + dropLegacySkipped, + noDiffReport, + LEARNINGS_FILE, + nonContentFiles, + resolveLearnedThrough, + renderBody, + extractSectionRows, +} from "./upsert-pr.mjs" +import { + revertTitleKind, + parseRevertTargets, + computeRevertAnnotations, + applyRevertAnnotations, + unannotatedRevertSignals, +} from "./reverts.mjs" +import { + parseLearnings, + renderLearnings, + parseLearnedThrough, + patchMarkerIntoBody, + parseDelta, + validateDelta, + applyDelta, + isTrustedComment, + promptBlock, +} from "./learn.mjs" + +const HERE = path.dirname(fileURLToPath(import.meta.url)) +const EDIT_SCRIPT = path.join(HERE, "edit.mjs") +const TRIAGE_SCRIPT = path.join(HERE, "triage.mjs") +const COLLECT_SCRIPT = path.join(HERE, "collect.mjs") +const LEARN_SCRIPT = path.join(HERE, "learn.mjs") + +const temps = [] + +function mktemp(prefix) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)) + temps.push(dir) + return dir +} + +function cleanup() { + for (const dir of temps.splice(0)) { + try { + fs.rmSync(dir, { recursive: true, force: true }) + } catch { + // best-effort + } + } +} + +function writeExecutable(filePath, body) { + fs.writeFileSync(filePath, body, { mode: 0o755 }) +} + +function makeStubKiloDir({ mode, callLog, stderrText = "event stream disconnected" }) { + const dir = mktemp("docs-sync-kilo-") + const kiloPath = path.join(dir, "kilo") + // mode: "stderr-exit0" | "record" | "partial-triage" | "mixed-triage" | "write-edit-summary" + const script = `#!/usr/bin/env node +const fs = require("node:fs"); +const path = require("node:path"); +const mode = ${JSON.stringify(mode)}; +const callLog = ${JSON.stringify(callLog ?? "")}; +const stderrText = ${JSON.stringify(stderrText)}; +if (callLog) { + fs.appendFileSync(callLog, JSON.stringify({ argv: process.argv.slice(2), cwd: process.cwd() }) + "\\n"); +} +if (mode === "stderr-exit0") { + process.stderr.write(stderrText + "\\n"); + process.exit(0); +} +if (mode === "record") { + process.stderr.write("recorded\\n"); + process.exit(0); +} +// Parse -f chunk/batch file from args for triage stubs +const args = process.argv.slice(2); +const fIdx = args.indexOf("-f"); +const fileArg = fIdx >= 0 ? args[fIdx + 1] : null; +let chunk = []; +if (fileArg && fs.existsSync(fileArg)) { + try { chunk = JSON.parse(fs.readFileSync(fileArg, "utf8")); } catch { chunk = []; } +} +if (mode === "write-edit-summary") { + // Success path: write the batch summary so edit.mjs returns true, while still + // emitting stderr so selftest can assert runKilo persisted it unconditionally. + process.stderr.write(stderrText + "\\n"); + const m = fileArg && String(fileArg).match(/edit-batch-(\\d+)\\.json/); + const index = m ? m[1] : "0"; + const summary = chunk.map((d) => ({ + pr: d.number, + url: d.url, + action: "skipped", + reason: "selftest stub", + })); + fs.mkdirSync("docs-sync-out", { recursive: true }); + fs.writeFileSync("docs-sync-out/edit-summary-" + index + ".json", JSON.stringify(summary)); + process.exit(0); +} +if (mode === "partial-triage") { + // Classify only a proper subset (first URL) of the chunk. + const owned = chunk.slice(0, Math.max(0, chunk.length - 1)); + const entries = owned.map((d) => ({ + pr: d.number, + url: d.url, + docs_worthy: false, + reason: "genuine not worthy", + target_sections: [], + priority: "medium", + })); + if (entries.length === 0 && chunk.length > 0) { + // single-PR chunk: still leave one missing by emitting empty-ish foreign-only + process.stdout.write("[]\\n"); + } else { + process.stdout.write(JSON.stringify(entries) + "\\n"); + } + process.exit(0); +} +if (mode === "mixed-triage") { + // Half docs_worthy true, half fail (no output for second half — but we return + // only some entries so backfill marks the rest pending). Actually: return + // docs_worthy:true for first half of chunk URLs so worthy > 0. + const half = Math.ceil(chunk.length / 2); + const entries = chunk.slice(0, half).map((d) => ({ + pr: d.number, + url: d.url, + docs_worthy: true, + reason: "needs docs", + target_sections: ["overview"], + priority: "high", + })); + process.stdout.write(JSON.stringify(entries) + "\\n"); + process.exit(0); +} +if (mode === "triage-embed-env-secret") { + // Valid triage JSON with a secret env value embedded in a string field + // (stdout is persisted to triage-raw-*.txt; must be redacted at capture). + const secret = process.env.KILO_API_KEY || "missing-secret"; + const entries = chunk.map((d) => ({ + pr: d.number, + url: d.url, + docs_worthy: true, + reason: "needs docs; diagnostic=" + secret, + target_sections: ["overview"], + priority: "high", + })); + process.stdout.write(JSON.stringify(entries) + "\\n"); + process.exit(0); +} +if (mode === "extraction-delta") { + const deltaFile = "docs-sync-out/extraction-delta.json"; + if (fs.existsSync(deltaFile)) { + const delta = JSON.parse(fs.readFileSync(deltaFile, "utf8")); + process.stdout.write(JSON.stringify(delta) + "\\n"); + } else { + process.stdout.write('{"add":[],"remove":[]}' + "\\n"); + } + process.exit(0); +} +process.stderr.write("unknown stub mode\\n"); +process.exit(1); +` + writeExecutable(kiloPath, script) + return dir +} + +function gitIn(cwd, args, env = {}) { + return execFileSync("git", args, { + cwd, + env: { ...process.env, ...env }, + stdio: ["ignore", "pipe", "pipe"], + encoding: "utf8", + }) + .toString() + .trim() +} + +function makeGitRunner(cwd, env = {}) { + return (args) => gitIn(cwd, args, env) +} + +function initRepoWithIdentity(dir) { + gitIn(dir, ["init", "-b", "main"]) + gitIn(dir, ["config", "user.name", "docs-sync-selftest"]) + gitIn(dir, ["config", "user.email", "docs-sync-selftest@example.com"]) + gitIn(dir, ["config", "commit.gpgsign", "false"]) +} + +// --------------------------------------------------------------------------- +// Case 1 — Defect A: mergeOrFallback +// --------------------------------------------------------------------------- +function case1_mergeOrFallback() { + console.log("case 1: Defect A (mergeOrFallback)") + + // 1a — identity configured + clean merge → mode=update + { + const dir = mktemp("docs-sync-merge-clean-") + initRepoWithIdentity(dir) + fs.writeFileSync(path.join(dir, "a.txt"), "base\n") + gitIn(dir, ["add", "a.txt"]) + gitIn(dir, ["commit", "-m", "base"]) + gitIn(dir, ["checkout", "-b", DEFAULT_BRANCH]) + fs.writeFileSync(path.join(dir, "b.txt"), "on branch\n") + gitIn(dir, ["add", "b.txt"]) + gitIn(dir, ["commit", "-m", "branch commit"]) + // Advance main without conflict + gitIn(dir, ["checkout", "main"]) + fs.writeFileSync(path.join(dir, "c.txt"), "on main\n") + gitIn(dir, ["add", "c.txt"]) + gitIn(dir, ["commit", "-m", "main advance"]) + gitIn(dir, ["update-ref", "refs/remotes/origin/main", "main"]) + gitIn(dir, ["checkout", DEFAULT_BRANCH]) + + const result = mergeOrFallback({ branch: DEFAULT_BRANCH, git: makeGitRunner(dir) }) + assert.equal(result.mode, "update") + assert.equal(result.branch, DEFAULT_BRANCH) + // merge brought c.txt in + assert.ok(fs.existsSync(path.join(dir, "c.txt"))) + } + + // 1b — genuine conflict → mode=conflict, abort succeeds, original branch untouched + { + const dir = mktemp("docs-sync-merge-conflict-") + initRepoWithIdentity(dir) + fs.writeFileSync(path.join(dir, "conflict.txt"), "base\n") + gitIn(dir, ["add", "conflict.txt"]) + gitIn(dir, ["commit", "-m", "base"]) + const baseSha = gitIn(dir, ["rev-parse", "HEAD"]) + + gitIn(dir, ["checkout", "-b", DEFAULT_BRANCH]) + fs.writeFileSync(path.join(dir, "conflict.txt"), "branch side\n") + gitIn(dir, ["add", "conflict.txt"]) + gitIn(dir, ["commit", "-m", "branch edit"]) + const branchShaBefore = gitIn(dir, ["rev-parse", "HEAD"]) + + gitIn(dir, ["checkout", "main"]) + fs.writeFileSync(path.join(dir, "conflict.txt"), "main side\n") + gitIn(dir, ["add", "conflict.txt"]) + gitIn(dir, ["commit", "-m", "main edit"]) + gitIn(dir, ["update-ref", "refs/remotes/origin/main", "main"]) + gitIn(dir, ["checkout", DEFAULT_BRANCH]) + + const result = mergeOrFallback({ branch: DEFAULT_BRANCH, git: makeGitRunner(dir) }) + assert.equal(result.mode, "conflict") + assert.ok(result.branch.startsWith(`${DEFAULT_BRANCH}-`)) + // Original rolling branch tip unchanged + const branchShaAfter = gitIn(dir, ["rev-parse", DEFAULT_BRANCH]) + assert.equal(branchShaAfter, branchShaBefore) + // No merge in progress + let mergeHead = true + try { + gitIn(dir, ["rev-parse", "-q", "--verify", "MERGE_HEAD"]) + } catch { + mergeHead = false + } + assert.equal(mergeHead, false) + void baseSha + } + + // 1c — identity-less / non-conflict merge failure → throws (does not fake conflict) + { + const dir = mktemp("docs-sync-merge-noid-") + initRepoWithIdentity(dir) + fs.writeFileSync(path.join(dir, "a.txt"), "base\n") + gitIn(dir, ["add", "a.txt"]) + gitIn(dir, ["commit", "-m", "base"]) + gitIn(dir, ["checkout", "-b", DEFAULT_BRANCH]) + fs.writeFileSync(path.join(dir, "b.txt"), "branch\n") + gitIn(dir, ["add", "b.txt"]) + gitIn(dir, ["commit", "-m", "branch"]) + gitIn(dir, ["checkout", "main"]) + fs.writeFileSync(path.join(dir, "c.txt"), "main\n") + gitIn(dir, ["add", "c.txt"]) + gitIn(dir, ["commit", "-m", "main"]) + gitIn(dir, ["update-ref", "refs/remotes/origin/main", "main"]) + gitIn(dir, ["checkout", DEFAULT_BRANCH]) + + // Strip identity so merge cannot create a commit + gitIn(dir, ["config", "--unset", "user.name"]) + gitIn(dir, ["config", "--unset", "user.email"]) + + const env = { + GIT_CONFIG_GLOBAL: "/dev/null", + GIT_CONFIG_SYSTEM: "/dev/null", + GIT_CONFIG_NOSYSTEM: "1", + } + const git = (args) => + execFileSync("git", ["-c", "user.useConfigOnly=true", ...args], { + cwd: dir, + env: { ...process.env, ...env }, + stdio: ["ignore", "pipe", "pipe"], + encoding: "utf8", + }) + .toString() + .trim() + + assert.throws( + () => mergeOrFallback({ branch: DEFAULT_BRANCH, git }), + (err) => { + // Must throw the original merge error, not a merge --abort failure + const msg = String(err?.stderr ?? err?.message ?? err) + assert.ok(!/no merge to abort/i.test(msg), `should not reach merge --abort: ${msg}`) + return true + }, + ) + } +} + +// --------------------------------------------------------------------------- +// Helpers to run edit.mjs / triage.mjs as child processes +// --------------------------------------------------------------------------- +function setupEditCwd(worthy, triage) { + const cwd = mktemp("docs-sync-edit-") + fs.mkdirSync(path.join(cwd, "docs-sync-out"), { recursive: true }) + fs.writeFileSync(path.join(cwd, "docs-sync-out", "worthy.json"), JSON.stringify(worthy, null, 2)) + fs.writeFileSync(path.join(cwd, "docs-sync-out", "triage.json"), JSON.stringify(triage, null, 2)) + return cwd +} + +function setupTriageCwd(digest) { + const cwd = mktemp("docs-sync-triage-") + fs.mkdirSync(path.join(cwd, "docs-sync-out"), { recursive: true }) + fs.writeFileSync(path.join(cwd, "docs-sync-out", "digest.json"), JSON.stringify(digest, null, 2)) + return cwd +} + +function runNodeScript(scriptPath, { cwd, env = {}, kiloDir, args = [] }) { + const pathEnv = [kiloDir, process.env.PATH].filter(Boolean).join(path.delimiter) + const result = spawnSync(process.execPath, [scriptPath, ...args], { + cwd, + env: { + ...process.env, + ...env, + PATH: pathEnv, + DOCS_SYNC_BACKOFF_MS: env.DOCS_SYNC_BACKOFF_MS ?? "0", + }, + encoding: "utf8", + timeout: 60_000, + }) + return { + status: result.status, + stdout: String(result.stdout ?? ""), + stderr: String(result.stderr ?? ""), + output: `${result.stdout ?? ""}${result.stderr ?? ""}`, + error: result.error, + } +} + +function samplePr(n, { merged_at, repo = "Kilo-Org/cloud" } = {}) { + return { + repo, + number: n, + title: `feat: sample ${n}`, + url: `https://github.com/${repo}/pull/${n}`, + author: "dev", + merged_at: merged_at ?? "2026-07-20T12:00:00.000Z", + labels: [], + body: "body", + files: [], + files_total: 1, + patch_excerpt: "", + } +} + +// --------------------------------------------------------------------------- +// Case 2 — Defect B: edit.mjs with stub kilo (exit 0 + stderr) +// --------------------------------------------------------------------------- +function case2_defectB() { + console.log("case 2: Defect B (edit.mjs stderr-on-exit-0)") + + const prs = [1, 2, 3, 4, 5].map((n) => samplePr(n)) + const worthy = prs + const triage = prs.map((p) => ({ + pr: p.number, + url: p.url, + docs_worthy: true, + reason: "needs docs", + target_sections: ["overview"], + priority: "high", + })) + const cwd = setupEditCwd(worthy, triage) + const stderrText = "event stream disconnected DIAG-CASE2" + const kiloDir = makeStubKiloDir({ mode: "stderr-exit0", stderrText }) + + const started = Date.now() + const result = runNodeScript(EDIT_SCRIPT, { + cwd, + kiloDir, + env: { + EDIT_MODEL: "test/model", + DOCS_SYNC_BACKOFF_MS: "0", + // Enough budget for 3 attempts × tiny timeout + EDIT_BUDGET_MINUTES: "5", + EDIT_BATCH_TIMEOUT_MINUTES: "1", + }, + }) + const elapsed = Date.now() - started + + assert.equal(result.status, 0, `edit.mjs exit: ${result.output}`) + // Backoff collapsed — 3 attempts without 60s+300s waits + assert.ok(elapsed < 15_000, `backoff should collapse with DOCS_SYNC_BACKOFF_MS=0; elapsed=${elapsed}ms`) + + assert.match(result.output, /stderr tail:/) + assert.match(result.output, /DIAG-CASE2|event stream disconnected/) + assert.match(result.output, /attempt 1/) + assert.match(result.output, /attempt 2/) + // 3 attempts + assert.match(result.output, /attempt 3|failed after up to 3 attempts/) + + const summary = JSON.parse(fs.readFileSync(path.join(cwd, ".docs-sync-summary.json"), "utf8")) + assert.equal(summary.length, 5) + for (const e of summary) { + assert.equal(e.action, "pending", `expected pending, got ${JSON.stringify(e)}`) + } + + const uncovered = computeUncovered({ worthy, summary, triage }) + assert.equal(uncovered.length, 5) + for (const u of uncovered) { + assert.ok(u.reason, "uncovered reason present") + } +} + +// --------------------------------------------------------------------------- +// Case 2b — AC4a: every docs-sync kilo run argv carries --auto +// --------------------------------------------------------------------------- +/** Slice `args: [` … matching `]` from source (newlines allowed inside). */ +function extractArgsArraySlice(source) { + const start = source.indexOf("args: [") + assert.ok(start >= 0, "args: [ not found in source") + let i = start + "args: ".length + assert.equal(source[i], "[") + let depth = 0 + for (; i < source.length; i++) { + const ch = source[i] + if (ch === "[") depth++ + else if (ch === "]") { + depth-- + if (depth === 0) return source.slice(start, i + 1) + } + } + throw new assert.AssertionError({ message: "unclosed args: [ array in source" }) +} + +/** Label → kilo-stderr filename rule (must match lib.mjs runKilo). */ +function kiloStderrLogName(label) { + return `kilo-stderr-${String(label).replace(/[^A-Za-z0-9._-]/g, "-")}.log` +} + +function case2b_autoFlag() { + console.log("case 2b: AC4a (--auto on every docs-sync kilo run)") + + // (i) region-scoped static check on triage.mjs / edit.mjs argv arrays + for (const name of ["triage.mjs", "edit.mjs"]) { + const src = fs.readFileSync(path.join(HERE, name), "utf8") + const slice = extractArgsArraySlice(src) + assert.ok(slice.includes('"--auto"'), `${name} args array must contain "--auto"; got:\n${slice}`) + } + + // (ii) Fix verify failures step: join the run: | block and require --auto on kilo run + { + const yml = fs.readFileSync(path.join(HERE, "..", "workflows", "docs-sync.yml"), "utf8") + const stepIdx = yml.indexOf("Fix verify failures") + assert.ok(stepIdx >= 0, "Fix verify failures step missing") + const afterStep = yml.slice(stepIdx) + const runIdx = afterStep.indexOf("run: |") + assert.ok(runIdx >= 0, "run: | missing after Fix verify failures") + const blockStart = stepIdx + runIdx + "run: |".length + const rest = yml.slice(blockStart) + // Block ends at next unindented step key or EOF — collect indented lines + const lines = [] + for (const line of rest.split("\n")) { + if (line === "") { + lines.push(line) + continue + } + // stop at next top-level list item under steps (two-space + "- ") + if (/^ {0,6}- name:/.test(line) || (/^\S/.test(line) && lines.length > 0)) break + lines.push(line) + } + // Join continuation backslashes then collapse whitespace for the kilo run line + const joined = lines + .map((l) => l.replace(/^\s+/, "")) + .join("\n") + .replace(/\\\n/g, " ") + .replace(/\s+/g, " ") + assert.match(joined, /kilo run\b/, `expected kilo run in Fix verify block:\n${joined}`) + const kiloCmd = joined.match(/kilo run\b[^|]*/)?.[0] ?? "" + assert.ok( + /\s--auto\b/.test(kiloCmd) || /kilo run\s+--auto\b/.test(kiloCmd), + `Fix verify kilo run must contain --auto; got: ${kiloCmd}`, + ) + + // The step runs under `set -o pipefail` + the default `bash -e`, so an + // unguarded kilo pipeline aborts the block before verify2.log is written + // once the CLI exits nonzero on a mid-stream error. The rebuild must decide + // this step's outcome, not the agent's exit code. + // Window is the end of the kilo pipeline → the rebuild, so a comment + // elsewhere in the block cannot satisfy the guard assertion. + const teeIdx = joined.indexOf("tee -a docs-sync-out/edit-log.txt") + assert.ok(teeIdx >= 0, `expected the kilo pipeline to tee edit-log.txt:\n${joined}`) + const kiloPipeline = joined.slice(teeIdx, joined.indexOf("bun run", teeIdx)) + assert.match( + kiloPipeline, + /\|\|\s*(echo|true)\b/, + `Fix verify kilo pipeline must be guarded (|| echo/true) so bash -e cannot skip the rebuild; got: ${kiloPipeline}`, + ) + assert.match(joined, /verify2\.log/, "Fix verify block must still write verify2.log") + } + + // (iii) authoritative: real stub invocations with callLog — every argv has --auto + { + const prs = [1, 2, 3, 4, 5].map((n) => samplePr(n)) + const worthy = prs + const triage = prs.map((p) => ({ + pr: p.number, + url: p.url, + docs_worthy: true, + reason: "needs docs", + target_sections: ["overview"], + priority: "high", + })) + const cwd = setupEditCwd(worthy, triage) + const callLog = path.join(cwd, "kilo-calls.log") + const stderrText = "event stream disconnected DIAG-AUTO" + const kiloDir = makeStubKiloDir({ mode: "stderr-exit0", stderrText, callLog }) + + const result = runNodeScript(EDIT_SCRIPT, { + cwd, + kiloDir, + env: { + EDIT_MODEL: "test/model", + DOCS_SYNC_BACKOFF_MS: "0", + EDIT_BUDGET_MINUTES: "5", + EDIT_BATCH_TIMEOUT_MINUTES: "1", + }, + }) + assert.equal(result.status, 0, `edit.mjs exit: ${result.output}`) + + assert.ok(fs.existsSync(callLog), "callLog must be written (stub was invoked)") + const lines = fs.readFileSync(callLog, "utf8").trim().split("\n").filter(Boolean) + assert.ok(lines.length > 0, "callCount > 0 required (vacuous empty log forbidden)") + for (const line of lines) { + const { argv } = JSON.parse(line) + assert.ok( + Array.isArray(argv) && argv.includes("--auto"), + `every kilo argv must include --auto; got ${JSON.stringify(argv)}`, + ) + } + } +} + +// --------------------------------------------------------------------------- +// Case 2c — full child stderr always written (success and failure paths) +// --------------------------------------------------------------------------- +function case2c_stderrLogAlways() { + console.log("case 2c: unconditional kilo-stderr-*.log") + + const prs = [1, 2, 3, 4, 5].map((n) => samplePr(n)) + const worthy = prs + const triage = prs.map((p) => ({ + pr: p.number, + url: p.url, + docs_worthy: true, + reason: "needs docs", + target_sections: ["overview"], + priority: "high", + })) + + // Failure path: stub exits 0 without summary (same mode as case 2) + { + const cwd = setupEditCwd(worthy, triage) + const stderrText = "FAILPATH-STDERR-MARKER" + const kiloDir = makeStubKiloDir({ mode: "stderr-exit0", stderrText }) + const result = runNodeScript(EDIT_SCRIPT, { + cwd, + kiloDir, + env: { + EDIT_MODEL: "test/model", + DOCS_SYNC_BACKOFF_MS: "0", + EDIT_BUDGET_MINUTES: "5", + EDIT_BATCH_TIMEOUT_MINUTES: "1", + }, + }) + assert.equal(result.status, 0, result.output) + const logName = kiloStderrLogName("edit batch 0 attempt 1") + const logPath = path.join(cwd, "docs-sync-out", logName) + assert.equal(logName, "kilo-stderr-edit-batch-0-attempt-1.log") + assert.ok(fs.existsSync(logPath), `expected ${logPath} on failure path`) + assert.match(fs.readFileSync(logPath, "utf8"), /FAILPATH-STDERR-MARKER/) + } + + // Success path: stub writes summary (today's path that discarded stderr) + { + const cwd = setupEditCwd(worthy, triage) + const stderrText = "SUCCESSPATH-STDERR-MARKER" + const kiloDir = makeStubKiloDir({ mode: "write-edit-summary", stderrText }) + const result = runNodeScript(EDIT_SCRIPT, { + cwd, + kiloDir, + env: { + EDIT_MODEL: "test/model", + DOCS_SYNC_BACKOFF_MS: "0", + EDIT_BUDGET_MINUTES: "5", + EDIT_BATCH_TIMEOUT_MINUTES: "1", + }, + }) + assert.equal(result.status, 0, result.output) + assert.ok( + fs.existsSync(path.join(cwd, "docs-sync-out", "edit-summary-0.json")), + "stub must write summary (success path)", + ) + const logName = kiloStderrLogName("edit batch 0 attempt 1") + const logPath = path.join(cwd, "docs-sync-out", logName) + assert.ok(fs.existsSync(logPath), `expected ${logPath} on success path`) + assert.match(fs.readFileSync(logPath, "utf8"), /SUCCESSPATH-STDERR-MARKER/) + } +} + +// --------------------------------------------------------------------------- +// Case 2d — redact secret env values from captured kilo stderr (artifact-safe) +// --------------------------------------------------------------------------- +function case2d_redactEnvSecrets() { + console.log("case 2d: redact env secrets from kilo stderr capture") + + const prs = [1, 2, 3, 4, 5].map((n) => samplePr(n)) + const worthy = prs + const triage = prs.map((p) => ({ + pr: p.number, + url: p.url, + docs_worthy: true, + reason: "needs docs", + target_sections: ["overview"], + priority: "high", + })) + const cwd = setupEditCwd(worthy, triage) + const secret = "selftest-secret-value-12345" + const stderrText = `leak before ${secret} after` + const kiloDir = makeStubKiloDir({ mode: "stderr-exit0", stderrText }) + + const result = runNodeScript(EDIT_SCRIPT, { + cwd, + kiloDir, + env: { + EDIT_MODEL: "test/model", + DOCS_SYNC_BACKOFF_MS: "0", + EDIT_BUDGET_MINUTES: "5", + EDIT_BATCH_TIMEOUT_MINUTES: "1", + KILO_API_KEY: secret, + }, + }) + assert.equal(result.status, 0, `edit.mjs exit: ${result.output}`) + + const logName = kiloStderrLogName("edit batch 0 attempt 1") + const logPath = path.join(cwd, "docs-sync-out", logName) + assert.ok(fs.existsSync(logPath), `expected ${logPath}`) + const logBody = fs.readFileSync(logPath, "utf8") + assert.ok(!logBody.includes(secret), `persisted stderr must not contain secret; got: ${logBody}`) + assert.ok(logBody.includes("leak before *** after"), `persisted stderr must redact to exact line; got: ${logBody}`) + + // Console stderr-tail region must also be redacted (not only the artifact file). + const tailIdx = result.output.indexOf("stderr tail:") + assert.ok(tailIdx >= 0, `expected stderr tail: in output; got: ${result.output}`) + const tailRegion = result.output.slice(tailIdx) + assert.ok(!tailRegion.includes(secret), `console stderr tail must not contain secret; got: ${tailRegion}`) +} + +// --------------------------------------------------------------------------- +// Case 2e — longer secret first when a shorter env value is a prefix +// --------------------------------------------------------------------------- +function case2e_prefixSecretOrdering() { + console.log("case 2e: prefix-secret ordering (longer value redacted first)") + + const prs = [1, 2, 3, 4, 5].map((n) => samplePr(n)) + const worthy = prs + const triage = prs.map((p) => ({ + pr: p.number, + url: p.url, + docs_worthy: true, + reason: "needs docs", + target_sections: ["overview"], + priority: "high", + })) + const cwd = setupEditCwd(worthy, triage) + const shortSecret = "abcdefgh" + const longSecret = "abcdefghIJKL-tail" + const stderrText = `leak: ${longSecret} end` + const kiloDir = makeStubKiloDir({ mode: "stderr-exit0", stderrText }) + + const result = runNodeScript(EDIT_SCRIPT, { + cwd, + kiloDir, + env: { + EDIT_MODEL: "test/model", + DOCS_SYNC_BACKOFF_MS: "0", + EDIT_BUDGET_MINUTES: "5", + EDIT_BATCH_TIMEOUT_MINUTES: "1", + A_KEY: shortSecret, + B_TOKEN: longSecret, + }, + }) + assert.equal(result.status, 0, `edit.mjs exit: ${result.output}`) + + const logName = kiloStderrLogName("edit batch 0 attempt 1") + const logPath = path.join(cwd, "docs-sync-out", logName) + assert.ok(fs.existsSync(logPath), `expected ${logPath}`) + const logBody = fs.readFileSync(logPath, "utf8") + assert.ok(!logBody.includes("IJKL-tail"), `must not leak prefix remainder; got: ${logBody}`) + assert.ok(logBody.includes("leak: *** end"), `expected full long secret redacted; got: ${logBody}`) +} + +// --------------------------------------------------------------------------- +// Case 2f — redact secret values from captured kilo stdout (triage-raw artifact) +// --------------------------------------------------------------------------- +function case2f_redactStdout() { + console.log("case 2f: redact env secrets from kilo stdout (triage-raw)") + + const digest = [samplePr(501), samplePr(502)] + const cwd = setupTriageCwd(digest) + const secret = "selftest-stdout-secret-99999" + const kiloDir = makeStubKiloDir({ mode: "triage-embed-env-secret" }) + const summaryFile = path.join(cwd, "step-summary.md") + fs.writeFileSync(summaryFile, "") + + const result = runNodeScript(TRIAGE_SCRIPT, { + cwd, + kiloDir, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_BACKOFF_MS: "0", + TRIAGE_BUDGET_MINUTES: "30", + GITHUB_STEP_SUMMARY: summaryFile, + KILO_API_KEY: secret, + }, + }) + assert.equal(result.status, 0, `triage.mjs exit: ${result.output}`) + + const rawFiles = fs.readdirSync(path.join(cwd, "docs-sync-out")).filter((f) => f.startsWith("triage-raw-")) + assert.ok(rawFiles.length > 0, "expected triage-raw-*.txt artifact") + for (const f of rawFiles) { + const body = fs.readFileSync(path.join(cwd, "docs-sync-out", f), "utf8") + assert.ok(!body.includes(secret), `triage-raw must not contain secret; ${f}: ${body}`) + } + + const triage = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "triage.json"), "utf8")) + assert.ok(triage.length >= 1, "triage must still parse after redaction") + assert.ok( + triage.some((e) => e.docs_worthy === true || e.pending === true || e.docs_worthy === false), + "triage entries must be structured", + ) +} + +// --------------------------------------------------------------------------- +// Case 2g — redact-stream.mjs line-wise filter (including partial last line) +// --------------------------------------------------------------------------- +function case2g_redactStream() { + console.log("case 2g: redact-stream.mjs stdin filter") + + const secret = "stream-secret-value-xyz" + const filterPath = path.join(HERE, "redact-stream.mjs") + assert.ok(fs.existsSync(filterPath), `expected ${filterPath}`) + + const input = `leak ${secret} after\npartial-${secret}` + const result = spawnSync(process.execPath, [filterPath], { + env: { ...process.env, KILO_API_KEY: secret }, + input, + encoding: "utf8", + timeout: 10_000, + }) + assert.equal(result.status, 0, `redact-stream exit: ${result.stderr || result.error}`) + assert.equal(result.stdout, "leak *** after\npartial-***") +} + +// --------------------------------------------------------------------------- +// Case 2h — pending causes reach the rolling PR free of ANSI escapes +// --------------------------------------------------------------------------- +function case2h_pendingCauseIsReadable() { + console.log("case 2h: pending cause has no ANSI escapes") + + const prs = [1, 2, 3, 4, 5].map((n) => samplePr(n)) + const worthy = prs + const triage = prs.map((p) => ({ + pr: p.number, + url: p.url, + docs_worthy: true, + reason: "needs docs", + target_sections: ["overview"], + priority: "high", + })) + const cwd = setupEditCwd(worthy, triage) + // Verbatim shape of a real kilo TUI stderr line (see PR #12521's pending table). + const ESC = "\u001b" + const stderrText = `${ESC}[0m→ ${ESC}[0mRead packages/kilo-docs/AGENTS.md${ESC}[2K${ESC}[1G done` + const kiloDir = makeStubKiloDir({ mode: "stderr-exit0", stderrText }) + + const result = runNodeScript(EDIT_SCRIPT, { + cwd, + kiloDir, + env: { + EDIT_MODEL: "test/model", + DOCS_SYNC_BACKOFF_MS: "0", + EDIT_BUDGET_MINUTES: "5", + EDIT_BATCH_TIMEOUT_MINUTES: "1", + }, + }) + assert.equal(result.status, 0, `edit.mjs exit: ${result.output}`) + + const summary = JSON.parse(fs.readFileSync(path.join(cwd, ".docs-sync-summary.json"), "utf8")) + assert.equal(summary.length, 5) + for (const e of summary) { + assert.equal(e.action, "pending", `expected pending, got ${JSON.stringify(e)}`) + assert.ok(!e.reason.includes(ESC), `pending reason must not contain ANSI escapes: ${JSON.stringify(e.reason)}`) + // Non-vacuous: the diagnostic text itself must survive the strip. + assert.match(e.reason, /Read packages\/kilo-docs\/AGENTS\.md/) + } + + // The raw artifact log keeps the escapes — it is the debugging record. + const rawLog = fs.readFileSync(path.join(cwd, "docs-sync-out", "kilo-stderr-edit-batch-0-attempt-1.log"), "utf8") + assert.ok(rawLog.includes(ESC), "persisted stderr log must stay raw") +} + +// --------------------------------------------------------------------------- +// Case 2i — wall-clock budgets can actually fit work +// --------------------------------------------------------------------------- +/** + * The pre-unit gates in triage.mjs/edit.mjs refuse to start a chunk/batch unless + * a whole per-unit timeout remains, so a budget below that timeout silently runs + * ZERO units and defers every PR. Run 30306629290 hit the weaker form of this: + * 8 of 11 chunks and 4 of 11 batches ran, the rest deferred untried. Assert the + * workflow sets both budgets and that each fits at least two units. + */ +function case2i_budgetsFitWork() { + console.log("case 2i: triage/edit budgets fit at least two units") + + const yml = fs.readFileSync(path.join(HERE, "..", "workflows", "docs-sync.yml"), "utf8") + const readEnvNumber = (key) => { + const m = yml.match(new RegExp(`^\\s*${key}:\\s*"?(\\d+)"?\\s*$`, "m")) + assert.ok(m, `${key} must be set in docs-sync.yml (default is too small to drain a backlog)`) + return Number(m[1]) + } + + // Per-unit timeouts are script constants, not workflow env; read them from source. + const triageSrc = fs.readFileSync(path.join(HERE, "triage.mjs"), "utf8") + const chunkMin = Number(triageSrc.match(/CHUNK_TIMEOUT_MS = (\d+) \* 60 \* 1000/)?.[1]) + assert.ok(Number.isFinite(chunkMin), "could not read CHUNK_TIMEOUT_MS from triage.mjs") + + const editSrc = fs.readFileSync(path.join(HERE, "edit.mjs"), "utf8") + const batchMin = Number(editSrc.match(/EDIT_BATCH_TIMEOUT_MINUTES\) \|\| (\d+)/)?.[1]) + assert.ok(Number.isFinite(batchMin), "could not read EDIT_BATCH_TIMEOUT_MINUTES default from edit.mjs") + + const triageBudget = readEnvNumber("TRIAGE_BUDGET_MINUTES") + const editBudget = readEnvNumber("EDIT_BUDGET_MINUTES") + assert.ok( + triageBudget >= 2 * chunkMin, + `TRIAGE_BUDGET_MINUTES=${triageBudget} must be >= 2x chunk timeout (${chunkMin}m)`, + ) + assert.ok(editBudget >= 2 * batchMin, `EDIT_BUDGET_MINUTES=${editBudget} must be >= 2x batch timeout (${batchMin}m)`) + + // The job timeout must outlast both budgets plus the non-LLM steps. + const jobTimeout = Number(yml.match(/^\s*timeout-minutes:\s*(\d+)\s*$/m)?.[1]) + assert.ok(Number.isFinite(jobTimeout), "could not read job timeout-minutes") + assert.ok( + jobTimeout > triageBudget + editBudget, + `job timeout-minutes=${jobTimeout} must exceed triage+edit budgets (${triageBudget}+${editBudget})`, + ) +} + +// --------------------------------------------------------------------------- +// Case 3 — watermark invariant +// --------------------------------------------------------------------------- +function case3_watermark() { + console.log("case 3: watermark invariant") + + const now = "2026-07-27T12:00:00.000Z" + const nowMs = Date.parse(now) + + const prA = samplePr(10, { merged_at: "2026-07-20T10:00:00.000Z" }) + const prB = samplePr(11, { merged_at: "2026-07-22T15:30:00.000Z" }) + const prC = samplePr(12, { merged_at: "2026-07-25T08:00:00.000Z" }) + const digest = [prA, prB, prC] + + // all covered → processed-through === now + { + const worthy = [prA, prB] + const summary = [ + { pr: 10, url: prA.url, action: "updated packages/kilo-docs/pages/x.md", reason: "" }, + { pr: 11, url: prB.url, action: "skipped", reason: "already documented" }, + ] + const triage = [ + { pr: 10, url: prA.url, docs_worthy: true, pending: false, reason: "ok" }, + { pr: 11, url: prB.url, docs_worthy: true, pending: false, reason: "ok" }, + ] + const uncovered = computeUncovered({ worthy, summary, triage }) + assert.equal(uncovered.length, 0) + const through = computeProcessedThrough({ uncovered, digest, now }) + assert.equal(through, now) + } + + // one uncovered → merged_at − 1 ms, strictly < now + { + const worthy = [prA, prB] + const summary = [ + { pr: 10, url: prA.url, action: "updated x", reason: "" }, + { pr: 11, url: prB.url, action: "pending", reason: "edit batch 0: exit 0" }, + ] + const uncovered = computeUncovered({ worthy, summary, triage: [] }) + assert.equal(uncovered.length, 1) + assert.equal(uncovered[0].url, prB.url) + const through = computeProcessedThrough({ uncovered, digest, now }) + const expected = new Date(Date.parse(prB.merged_at) - 1).toISOString() + assert.equal(through, expected) + assert.ok(Date.parse(through) < nowMs) + } + + // several uncovered → earliest merge time wins + { + const worthy = [prA, prB, prC] + const summary = [ + { pr: 10, url: prA.url, action: "pending", reason: "fail" }, + { pr: 12, url: prC.url, action: "pending", reason: "fail" }, + ] + // prB missing from summary entirely + const uncovered = computeUncovered({ worthy, summary, triage: [] }) + assert.ok(uncovered.length >= 2) + const through = computeProcessedThrough({ uncovered, digest, now }) + // earliest among A, B, C that are uncovered — A is earliest + const times = uncovered + .map((u) => digest.find((d) => d.url === u.url)?.merged_at) + .filter(Boolean) + .map((t) => Date.parse(t)) + const earliest = Math.min(...times) + assert.equal(through, new Date(earliest - 1).toISOString()) + } + + // summary missing/truncated while worthy non-empty → every worthy URL held back + { + const worthy = [prA, prB] + const uncovered = computeUncovered({ worthy, summary: [], triage: [] }) + assert.equal(uncovered.length, 2) + const through = computeProcessedThrough({ uncovered, digest, now }) + assert.equal(through, new Date(Date.parse(prA.merged_at) - 1).toISOString()) + } + + // noDiffReport three arms + { + const uncovered = [{ url: prA.url, reason: "edit batch failed" }] + const arm1 = noDiffReport({ uncovered, sinceOverride: true }) + assert.ok(arm1.summary.includes(prA.url)) + assert.ok(arm1.warning, "override + uncovered → warning present") + + const arm2 = noDiffReport({ uncovered: [], sinceOverride: true }) + assert.equal(arm2.warning, null, "override + empty uncovered → warning absent") + + const arm3 = noDiffReport({ uncovered, sinceOverride: false }) + assert.equal(arm3.warning, null, "scheduled + uncovered → warning absent") + } + + // triage pending:true backfill rows land in uncovered (consumption) + { + const triage = [ + { + pr: 99, + url: "https://github.com/Kilo-Org/cloud/pull/99", + docs_worthy: false, + pending: true, + reason: "not classified by triage", + }, + ] + const uncovered = computeUncovered({ worthy: [], summary: [], triage }) + assert.equal(uncovered.length, 1) + assert.equal(uncovered[0].url, triage[0].url) + const through = computeProcessedThrough({ + uncovered, + digest: [{ url: triage[0].url, merged_at: "2026-07-21T00:00:00.000Z" }], + now, + }) + assert.equal(through, new Date(Date.parse("2026-07-21T00:00:00.000Z") - 1).toISOString()) + } + + // fallback field (post-plan repair) + { + const uncovered = [{ url: "https://github.com/Kilo-Org/cloud/pull/50", reason: "missing" }] + const fallback = "2026-07-17T00:00:00.000Z" + // unresolved merged_at + parseable fallback → hold at fallback, warn + const prevWarn = console.warn + const warnings = [] + console.warn = (...a) => warnings.push(a.join(" ")) + try { + const through = computeProcessedThrough({ uncovered, digest: [], now, fallback }) + assert.equal(through, new Date(fallback).toISOString()) + assert.ok(Date.parse(through) < nowMs) + assert.ok(warnings.some((w) => w.includes("::warning::"))) + } finally { + console.warn = prevWarn + } + + // unresolved + unparseable/missing fallback → throws + assert.throws(() => computeProcessedThrough({ uncovered, digest: [], now }), /fallback|SINCE|refusing/i) + assert.throws( + () => computeProcessedThrough({ uncovered, digest: [], now, fallback: "not-a-date" }), + /fallback|SINCE|refusing/i, + ) + + // resolved merged_at ignores fallback + const throughResolved = computeProcessedThrough({ + uncovered: [{ url: prA.url, reason: "x" }], + digest: [prA], + now, + fallback: "2020-01-01T00:00:00.000Z", + }) + assert.equal(throughResolved, new Date(Date.parse(prA.merged_at) - 1).toISOString()) + + // empty uncovered ignores fallback + const throughEmpty = computeProcessedThrough({ + uncovered: [], + digest: [], + now, + fallback: "2020-01-01T00:00:00.000Z", + }) + assert.equal(throughEmpty, now) + } +} + +// --------------------------------------------------------------------------- +// Case 4 — routing and round trip +// --------------------------------------------------------------------------- +function case4_routing() { + console.log("case 4: routing and round trip") + + const summary = [ + { pr: 1, url: "https://github.com/Kilo-Org/cloud/pull/1", action: "updated pages/a.md", reason: "" }, + { pr: 2, url: "https://github.com/Kilo-Org/cloud/pull/2", action: "skipped", reason: "already documented" }, + { pr: 3, url: "https://github.com/Kilo-Org/cloud/pull/3", action: "pending", reason: "edit batch 1: exit 0" }, + ] + const triage = [ + { + pr: 4, + url: "https://github.com/Kilo-Org/cloud/pull/4", + docs_worthy: false, + pending: false, + reason: "chore only", + }, + { + pr: 5, + url: "https://github.com/Kilo-Org/cloud/pull/5", + docs_worthy: false, + pending: true, + reason: "triage failed to classify this PR", + }, + ] + const worthy = [ + { number: 1, url: summary[0].url }, + { number: 2, url: summary[1].url }, + { number: 3, url: summary[2].url }, + ] + const uncovered = computeUncovered({ worthy, summary, triage }) + const { changesRows, pendingRows, skippedRows } = routeRows({ summary, triage, uncovered }) + + // pending appears in neither Changes nor Considered + const changesText = changesRows.join("\n") + const skippedText = skippedRows.join("\n") + assert.ok(changesText.includes("pull/1"), "success in Changes") + assert.ok(!changesText.includes("pull/3"), "pending must not be in Changes") + assert.ok(!changesText.includes("pull/5"), "triage-pending must not be in Changes") + assert.ok(skippedText.includes("pull/2"), "genuine skipped in Considered") + assert.ok(skippedText.includes("pull/4"), "genuine not-worthy in Considered") + assert.ok(!skippedText.includes("pull/3"), "pending must not be in Considered") + assert.ok(!skippedText.includes("pull/5"), "triage-pending must not be in Considered") + assert.ok(pendingRows.some((r) => r.includes("pull/3"))) + assert.ok(pendingRows.some((r) => r.includes("pull/5"))) + + // round-trip renderBody → extractSectionRows + const through = "2026-07-20T09:59:59.999Z" + const learnedMarker = "" + const body = renderBody({ + date: "2026-07-27", + since: "2026-07-17T00:00:00.000Z", + through, + learnedThrough: learnedMarker, + changesRows, + pendingRows, + skippedRows, + verified: true, + draftReasons: [], + note: "", + }) + assert.ok(body.includes(``)) + assert.ok(body.includes(learnedMarker), "learned-through marker must appear in rendered body") + const extChanges = extractSectionRows(body, "changes") + const extPending = extractSectionRows(body, "pending") + const extSkipped = extractSectionRows(body, "skipped") + assert.deepEqual(extChanges, changesRows) + assert.deepEqual(extPending, pendingRows) + assert.deepEqual(extSkipped, skippedRows) + + // clean() prevents marker forgery in agent-generated row strings + { + const forgedRows = routeRows({ + summary: [ + { + pr: 9, + url: "https://github.com/Kilo-Org/cloud/pull/9", + action: "skipped", + reason: "x injection", + }, + ], + triage: [], + uncovered: [], + }) + assert.ok(!forgedRows.skippedRows[0].includes(""), "clean() must strip --> from reasons") + const forgedBody = renderBody({ + date: "2026-07-27", + since: "s", + through: "t", + learnedThrough: "", + changesRows: [], + pendingRows: [], + skippedRows: forgedRows.skippedRows, + verified: true, + draftReasons: [], + note: "", + }) + // Exactly one real section end marker — the forged sequences were stripped + assert.equal((forgedBody.match(//g) || []).length, 1) + const extracted = extractSectionRows(forgedBody, "skipped") + assert.equal(extracted.length, 1) + assert.ok(extracted[0].includes("injection")) + } + + const legacyRows = [ + "| [Kilo-Org/cloud#1](https://github.com/Kilo-Org/cloud/pull/1) | edit pass failed or timed out for this PR |", + "| [Kilo-Org/cloud#2](https://github.com/Kilo-Org/cloud/pull/2) | triage failed to classify this PR |", + "| [Kilo-Org/cloud#3](https://github.com/Kilo-Org/cloud/pull/3) | not classified by triage |", + "| [Kilo-Org/cloud#4](https://github.com/Kilo-Org/cloud/pull/4) | already covered by existing docs |", + ] + const kept = dropLegacySkipped(legacyRows) + assert.equal(kept.length, 1) + assert.ok(kept[0].includes("pull/4")) + assert.ok(!kept.some((r) => r.includes("edit pass failed"))) + assert.ok(!kept.some((r) => r.includes("triage failed to classify"))) + assert.ok(!kept.some((r) => r.includes("not classified by triage"))) +} + +// --------------------------------------------------------------------------- +// Case 5 — re-collection window +// --------------------------------------------------------------------------- +function case5_recollection() { + console.log("case 5: re-collection closes the loop") + + const collectSrc = fs.readFileSync(COLLECT_SCRIPT, "utf8") + // Query template must use merged:>= + assert.ok( + /merged:>=\$\{since\.toISOString\(\)\}/.test(collectSrc) || /merged:>=/.test(collectSrc), + "collect.mjs must search merged:>=since", + ) + assert.match(collectSrc, /merged:>=/) + + const mergedAt = "2026-07-22T15:30:00.000Z" + const uncovered = [{ url: "https://github.com/Kilo-Org/cloud/pull/11", reason: "pending" }] + const digest = [{ url: uncovered[0].url, merged_at: mergedAt }] + const now = "2026-07-27T12:00:00.000Z" + const since = computeProcessedThrough({ uncovered, digest, now }) + // held-back since is strictly before the uncovered PR's merged_at + assert.ok(Date.parse(since) < Date.parse(mergedAt), `since ${since} must be < merged_at ${mergedAt}`) + // And the query window merged:>=since therefore includes that PR + assert.ok(Date.parse(mergedAt) >= Date.parse(since)) +} + +// --------------------------------------------------------------------------- +// Case 6 — budgets +// --------------------------------------------------------------------------- +function case6_budgets() { + console.log("case 6: budgets") + + // --- edit budget --- + { + // 12 PRs = 3 batches of 5; budget too small for even one batch unit + const prs = Array.from({ length: 12 }, (_, i) => samplePr(100 + i)) + const worthy = prs + const triage = prs.map((p) => ({ + pr: p.number, + url: p.url, + docs_worthy: true, + reason: "needs docs", + target_sections: [], + priority: "medium", + })) + const cwd = setupEditCwd(worthy, triage) + const callLog = path.join(cwd, "kilo-calls.log") + const kiloDir = makeStubKiloDir({ mode: "record", callLog }) + + // EDIT_BUDGET_MINUTES must be positive (0 falls through to default 50). + // BATCH_TIMEOUT default would be 15m; set both tiny so left < BATCH_TIMEOUT immediately. + const result = runNodeScript(EDIT_SCRIPT, { + cwd, + kiloDir, + env: { + EDIT_MODEL: "test/model", + DOCS_SYNC_BACKOFF_MS: "0", + EDIT_BUDGET_MINUTES: "0.0001", + EDIT_BATCH_TIMEOUT_MINUTES: "15", + }, + }) + assert.equal(result.status, 0, result.output) + assert.match(result.output, /deferring \d+ PRs/) + assert.match(result.output, /deferred \d+ PRs due to wall-clock budget/) + + const calls = fs.existsSync(callLog) ? fs.readFileSync(callLog, "utf8").trim() : "" + const callCount = calls ? calls.split("\n").filter(Boolean).length : 0 + assert.equal(callCount, 0, `kilo must not be invoked for deferred edit batches; got ${callCount}`) + + const summary = JSON.parse(fs.readFileSync(path.join(cwd, ".docs-sync-summary.json"), "utf8")) + assert.ok(summary.every((e) => e.action === "pending")) + const uncovered = computeUncovered({ worthy, summary, triage }) + assert.equal(uncovered.length, 12) + assert.ok(summary.every((e) => e.action !== "skipped")) + } + + // --- triage budget --- + { + // CHUNK_SIZE=25; 30 PRs = 2 chunks; budget too small for a 10m chunk + const digest = Array.from({ length: 30 }, (_, i) => samplePr(200 + i)) + const cwd = setupTriageCwd(digest) + const callLog = path.join(cwd, "kilo-calls.log") + const kiloDir = makeStubKiloDir({ mode: "record", callLog }) + const summaryFile = path.join(cwd, "step-summary.md") + fs.writeFileSync(summaryFile, "") + + const result = runNodeScript(TRIAGE_SCRIPT, { + cwd, + kiloDir, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_BACKOFF_MS: "0", + TRIAGE_BUDGET_MINUTES: "0.0001", + GITHUB_STEP_SUMMARY: summaryFile, + }, + }) + assert.equal(result.status, 0, result.output) + assert.match(result.output, /deferring \d+ PRs/) + + const calls = fs.existsSync(callLog) ? fs.readFileSync(callLog, "utf8").trim() : "" + const callCount = calls ? calls.split("\n").filter(Boolean).length : 0 + assert.equal(callCount, 0, `kilo must not be invoked for deferred triage chunks; got ${callCount}`) + + const triage = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "triage.json"), "utf8")) + assert.equal(triage.length, 30) + assert.ok(triage.every((e) => e.pending === true)) + assert.ok(triage.every((e) => e.docs_worthy === false)) + const uncovered = computeUncovered({ worthy: [], summary: [], triage }) + assert.equal(uncovered.length, 30) + } +} + +// --------------------------------------------------------------------------- +// Case 7 — applyCap both arms +// --------------------------------------------------------------------------- +function case7_cap() { + console.log("case 7: applyCap") + + const now = new Date("2026-07-27T12:00:00.000Z") + const old = new Date("2026-06-01T00:00:00.000Z") + + const prevLog = console.log + const prevWarn = console.warn + const logs = [] + const warnings = [] + console.log = (...a) => logs.push(a.join(" ")) + console.warn = (...a) => warnings.push(a.join(" ")) + try { + // explicit:false + older than 14 days → clamped AND reported + const a = applyCap(old, now, { explicit: false }) + assert.equal(a.clamped, true) + assert.ok(a.since.getTime() > old.getTime()) + const cap = new Date(now.getTime() - 14 * 24 * 3600 * 1000) + assert.equal(a.since.toISOString(), cap.toISOString()) + assert.ok(warnings.some((w) => w.includes("::warning::") && w.includes("clamped"))) + + // explicit:true + older than 14 days → unchanged, skip reported + logs.length = 0 + warnings.length = 0 + const b = applyCap(old, now, { explicit: true }) + assert.equal(b.clamped, false) + assert.equal(b.since.toISOString(), old.toISOString()) + assert.ok(logs.some((l) => /cap skipped|INPUT_SINCE/i.test(l))) + } finally { + console.log = prevLog + console.warn = prevWarn + } +} + +// --------------------------------------------------------------------------- +// Case 8 — triage.mjs outputs +// --------------------------------------------------------------------------- +function case8_triage() { + console.log("case 8: triage pass outputs") + + // 8a Run A: SINCE_OVERRIDE=true + everything pending → warning present + { + const digest = [samplePr(301), samplePr(302), samplePr(303)] + const cwd = setupTriageCwd(digest) + const kiloDir = makeStubKiloDir({ mode: "stderr-exit0", stderrText: "stream end before idle" }) + const summaryFile = path.join(cwd, "step-summary.md") + fs.writeFileSync(summaryFile, "") + + const result = runNodeScript(TRIAGE_SCRIPT, { + cwd, + kiloDir, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_BACKOFF_MS: "0", + TRIAGE_BUDGET_MINUTES: "30", + SINCE_OVERRIDE: "true", + GITHUB_STEP_SUMMARY: summaryFile, + }, + }) + assert.equal(result.status, 0, result.output) + const triage = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "triage.json"), "utf8")) + assert.equal(triage.length, 3) + assert.ok(triage.every((e) => e.pending === true)) + const summary = fs.readFileSync(summaryFile, "utf8") + assert.match(summary, /triage pending/) + for (const d of digest) { + assert.ok(summary.includes(d.url), `summary lists ${d.url}`) + } + assert.match(result.output, /::warning::.*since-override/) + } + + // 8a Run B: SINCE_OVERRIDE unset → warning absent + { + const digest = [samplePr(311), samplePr(312)] + const cwd = setupTriageCwd(digest) + const kiloDir = makeStubKiloDir({ mode: "stderr-exit0", stderrText: "stream end" }) + const summaryFile = path.join(cwd, "step-summary.md") + fs.writeFileSync(summaryFile, "") + + const result = runNodeScript(TRIAGE_SCRIPT, { + cwd, + kiloDir, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_BACKOFF_MS: "0", + TRIAGE_BUDGET_MINUTES: "30", + GITHUB_STEP_SUMMARY: summaryFile, + }, + }) + assert.equal(result.status, 0, result.output) + const triage = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "triage.json"), "utf8")) + assert.ok(triage.every((e) => e.pending === true)) + assert.ok(fs.readFileSync(summaryFile, "utf8").includes("triage pending")) + assert.ok(!/::warning::.*since-override/.test(result.output), "override warning must be absent when unset") + } + + // 8a Run C: SINCE_OVERRIDE=true with MIXED stub (worthy > 0) → warning ABSENT + { + const digest = [samplePr(321), samplePr(322), samplePr(323), samplePr(324)] + const cwd = setupTriageCwd(digest) + const kiloDir = makeStubKiloDir({ mode: "mixed-triage" }) + const summaryFile = path.join(cwd, "step-summary.md") + fs.writeFileSync(summaryFile, "") + + const result = runNodeScript(TRIAGE_SCRIPT, { + cwd, + kiloDir, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_BACKOFF_MS: "0", + TRIAGE_BUDGET_MINUTES: "30", + SINCE_OVERRIDE: "true", + GITHUB_STEP_SUMMARY: summaryFile, + }, + }) + assert.equal(result.status, 0, result.output) + const triage = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "triage.json"), "utf8")) + const worthy = triage.filter((e) => e.docs_worthy === true).length + const pending = triage.filter((e) => e.pending === true).length + assert.ok(worthy > 0, "mixed stub must produce worthy > 0") + assert.ok(pending > 0, "mixed stub must leave some pending") + assert.ok( + !/::warning::.*since-override/.test(result.output), + "override warning must be ABSENT when worthy > 0 (Upsert will run)", + ) + } + + // 8b — partial classification → missing URLs pending:true + computeUncovered + { + // One chunk of 4 PRs; stub classifies first 3 only + const digest = [samplePr(401), samplePr(402), samplePr(403), samplePr(404)] + const cwd = setupTriageCwd(digest) + const kiloDir = makeStubKiloDir({ mode: "partial-triage" }) + const summaryFile = path.join(cwd, "step-summary.md") + fs.writeFileSync(summaryFile, "") + + const result = runNodeScript(TRIAGE_SCRIPT, { + cwd, + kiloDir, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_BACKOFF_MS: "0", + TRIAGE_BUDGET_MINUTES: "30", + GITHUB_STEP_SUMMARY: summaryFile, + }, + }) + assert.equal(result.status, 0, result.output) + const triage = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "triage.json"), "utf8")) + assert.equal(triage.length, 4) + const missing = triage.filter((e) => e.reason === "not classified by triage") + assert.ok(missing.length >= 1, "backfill must mark unclassified URLs") + assert.ok(missing.every((e) => e.pending === true)) + const uncovered = computeUncovered({ worthy: [], summary: [], triage }) + for (const m of missing) { + assert.ok( + uncovered.some((u) => u.url === m.url), + `${m.url} must appear in computeUncovered`, + ) + } + } +} + +// --------------------------------------------------------------------------- +// Case 9 — revert interception (title/body/annotations + source-order guards) +// --------------------------------------------------------------------------- +function case9_reverts() { + console.log("case 9: revert interception") + + // --- revertTitleKind --- + assert.equal(revertTitleKind("revert(cli): restore opt-in stream idle timeouts"), "conventional") + assert.equal(revertTitleKind('Revert "feat(cli): default stream watchdog"'), "github-native") + assert.equal(revertTitleKind("REVERT: all of it"), "conventional") + assert.equal(revertTitleKind("feat(cli): add x"), null) + assert.equal(revertTitleKind("docs: update y"), null) + assert.equal(revertTitleKind("Reverted behavior docs"), null) + + // --- parseRevertTargets --- + const defaultRepo = "Kilo-Org/kilocode" + const body12497 = `The default stream inactivity watchdog introduced by #12249 aborts requests based only on the absence of normalized AI SDK events. That signal cannot distinguish a dead provider stream from long prompt processing, reasoning, buffering, or transport behavior, and the follow-up in #12481 reduces false positives without resolving that ambiguity. + +Revert both changes and restore the previous opt-in contract: Kilo does not impose a stream idle timeout unless the provider configuration explicitly sets \`chunkTimeout\`. Explicit provider timeouts continue to use the existing AI SDK and SSE timeout paths. This removes the global heuristic while the underlying stalled-stream source and the required transport-level observability are investigated. + +This deliberately restores the possibility that an unconfigured provider stream can remain open indefinitely. A default watchdog should be reintroduced only with evidence that its liveness signal and threshold do not terminate healthy responses. + +Reverts #12249 and #12481. +` + { + const targets = parseRevertTargets(body12497, defaultRepo) + assert.equal(targets.length, 2) + assert.deepEqual( + targets.map((t) => ({ repo: t.repo, number: t.number, url: t.url })), + [ + { + repo: "Kilo-Org/kilocode", + number: 12249, + url: "https://github.com/Kilo-Org/kilocode/pull/12249", + }, + { + repo: "Kilo-Org/kilocode", + number: 12481, + url: "https://github.com/Kilo-Org/kilocode/pull/12481", + }, + ], + ) + } + + { + const narrative = "Revert the mistaken fix in #99999 because it broke streams.\n\nReverts #12249." + const targets = parseRevertTargets(narrative, defaultRepo) + assert.equal(targets.length, 1) + assert.equal(targets[0].number, 12249) + assert.ok(!targets.some((t) => t.number === 99999)) + } + + { + const targets = parseRevertTargets("Reverts Kilo-Org/cloud#42.", defaultRepo) + assert.equal(targets.length, 1) + assert.equal(targets[0].repo, "Kilo-Org/cloud") + assert.equal(targets[0].number, 42) + assert.equal(targets[0].url, "https://github.com/Kilo-Org/cloud/pull/42") + } + + { + const targets = parseRevertTargets("Reverts #1, #2.", defaultRepo) + assert.equal(targets.length, 2) + assert.deepEqual( + targets.map((t) => t.number), + [1, 2], + ) + } + + assert.deepEqual(parseRevertTargets("This reverts commit deadbeefcafe.", defaultRepo), []) + assert.deepEqual(parseRevertTargets("This reverts #5.", defaultRepo), []) + assert.deepEqual(parseRevertTargets("No revert trailer here at all.", defaultRepo), []) + + // bulleted / quoted single-line trailers + { + const targets = parseRevertTargets("- Reverts #7.", defaultRepo) + assert.equal(targets.length, 1) + assert.equal(targets[0].number, 7) + } + { + const targets = parseRevertTargets("> Reverts #8.", defaultRepo) + assert.equal(targets.length, 1) + assert.equal(targets[0].number, 8) + } + // word-boundary: prose glued to "Reverts" is not a trailer + assert.deepEqual(parseRevertTargets("Revertsomething #5", defaultRepo), []) + + // --- computeRevertAnnotations --- + // Map keys are lowercased; lookups must use .toLowerCase() + const fUrl = "https://github.com/Kilo-Org/kilocode/pull/100" + const r1Url = "https://github.com/Kilo-Org/kilocode/pull/200" + const r2Url = "https://github.com/Kilo-Org/kilocode/pull/300" + const f2Url = "https://github.com/Kilo-Org/kilocode/pull/101" + const mergedAt = "2026-07-20T12:00:00.000Z" + const mergedAt2 = "2026-07-21T12:00:00.000Z" + + { + const annotations = computeRevertAnnotations([ + { + url: r1Url, + merged_at: mergedAt, + targets: [{ repo: "Kilo-Org/kilocode", number: 100, url: fUrl }], + }, + ]) + assert.equal(annotations.size, 1) + assert.deepEqual(annotations.get(fUrl.toLowerCase()), { url: r1Url, merged_at: mergedAt }) + } + + { + // two-target signal (#12497-shaped) + const annotations = computeRevertAnnotations([ + { + url: r1Url, + merged_at: mergedAt, + targets: [ + { repo: "Kilo-Org/kilocode", number: 100, url: fUrl }, + { repo: "Kilo-Org/kilocode", number: 101, url: f2Url }, + ], + }, + ]) + assert.equal(annotations.size, 2) + assert.deepEqual(annotations.get(fUrl.toLowerCase()), { url: r1Url, merged_at: mergedAt }) + assert.deepEqual(annotations.get(f2Url.toLowerCase()), { url: r1Url, merged_at: mergedAt }) + } + + { + // revert-of-revert: R1 reverts F, R2 reverts R1 → F not annotated; R1 gets R2 + const annotations = computeRevertAnnotations([ + { + url: r1Url, + merged_at: mergedAt, + targets: [{ repo: "Kilo-Org/kilocode", number: 100, url: fUrl }], + }, + { + url: r2Url, + merged_at: mergedAt2, + targets: [{ repo: "Kilo-Org/kilocode", number: 200, url: r1Url }], + }, + ]) + assert.equal(annotations.has(fUrl.toLowerCase()), false) + assert.deepEqual(annotations.get(r1Url.toLowerCase()), { url: r2Url, merged_at: mergedAt2 }) + } + + { + const annotations = computeRevertAnnotations([{ url: r1Url, merged_at: mergedAt, targets: [] }]) + assert.equal(annotations.size, 0) + } + + // --- applyRevertAnnotations --- + { + const digest = [ + { url: fUrl, title: "feat F" }, + { url: "https://github.com/Kilo-Org/kilocode/pull/999", title: "untouched" }, + ] + const applied = applyRevertAnnotations(digest, [ + { + url: r1Url, + merged_at: mergedAt, + targets: [{ repo: "Kilo-Org/kilocode", number: 100, url: fUrl }], + }, + ]) + assert.deepEqual(digest[0].reverted_by, { url: r1Url, merged_at: mergedAt }) + assert.equal(digest[1].reverted_by, undefined) + assert.deepEqual(applied, [[fUrl, r1Url]]) + } + + { + // end-to-end revert-of-revert: F must not gain reverted_by + const digest = [{ url: fUrl, title: "feat F" }] + const applied = applyRevertAnnotations(digest, [ + { + url: r1Url, + merged_at: mergedAt, + targets: [{ repo: "Kilo-Org/kilocode", number: 100, url: fUrl }], + }, + { + url: r2Url, + merged_at: mergedAt2, + targets: [{ repo: "Kilo-Org/kilocode", number: 200, url: r1Url }], + }, + ]) + assert.equal(digest[0].reverted_by, undefined) + assert.deepEqual(applied, []) + } + + { + // case-insensitive url matching: lowercase signal target vs canonical digest entry + const canonical = "https://github.com/Kilo-Org/kilocode/pull/12249" + const lowerTarget = "https://github.com/kilo-org/kilocode/pull/12249" + const reverter = "https://github.com/Kilo-Org/kilocode/pull/12497" + const digest = [{ url: canonical, title: "feat stream" }] + const applied = applyRevertAnnotations(digest, [ + { + url: reverter, + merged_at: mergedAt, + targets: [{ repo: "kilo-org/kilocode", number: 12249, url: lowerTarget }], + }, + ]) + assert.deepEqual(digest[0].reverted_by, { url: reverter, merged_at: mergedAt }) + assert.deepEqual(applied, [[canonical, reverter]]) + } + + // --- unannotatedRevertSignals --- + { + // partial coverage: F in-digest, G pre-window → only G missed; chains empty + const sUrl = "https://github.com/Kilo-Org/kilocode/pull/500" + const gUrl = "https://github.com/Kilo-Org/kilocode/pull/102" + const signals = [ + { + url: sUrl, + merged_at: mergedAt, + targets: [ + { repo: "Kilo-Org/kilocode", number: 100, url: fUrl }, + { repo: "Kilo-Org/kilocode", number: 102, url: gUrl }, + ], + }, + ] + const result = unannotatedRevertSignals(signals, [[fUrl, sUrl]]) + assert.deepEqual(result, { missed: [{ url: sUrl, targets: [gUrl] }], unparsed: [], chains: [] }) + } + + { + // fully covered signal (no chain) → all three buckets empty + const signals = [ + { + url: r1Url, + merged_at: mergedAt, + targets: [ + { repo: "Kilo-Org/kilocode", number: 100, url: fUrl }, + { repo: "Kilo-Org/kilocode", number: 101, url: f2Url }, + ], + }, + ] + const result = unannotatedRevertSignals(signals, [ + [fUrl, r1Url], + [f2Url, r1Url], + ]) + assert.deepEqual(result, { missed: [], unparsed: [], chains: [] }) + } + + { + // depth-2 chain (#4709/#4759): R1→F, R2→R1 — both in chains; F visible as R1 target + const signals = [ + { + url: r1Url, + merged_at: mergedAt, + targets: [{ repo: "Kilo-Org/kilocode", number: 100, url: fUrl }], + }, + { + url: r2Url, + merged_at: mergedAt2, + targets: [{ repo: "Kilo-Org/kilocode", number: 200, url: r1Url }], + }, + ] + const result = unannotatedRevertSignals(signals, []) + assert.deepEqual(result, { + missed: [], + unparsed: [], + chains: [ + { url: r1Url, targets: [fUrl] }, + { url: r2Url, targets: [r1Url] }, + ], + }) + } + + { + // depth-3 chain: R1→F, R2→R1, R3→R2 — all three in chains; missed/unparsed empty + const r3Url = "https://github.com/Kilo-Org/kilocode/pull/301" + const signals = [ + { + url: r1Url, + merged_at: mergedAt, + targets: [{ repo: "Kilo-Org/kilocode", number: 100, url: fUrl }], + }, + { + url: r2Url, + merged_at: mergedAt2, + targets: [{ repo: "Kilo-Org/kilocode", number: 200, url: r1Url }], + }, + { + url: r3Url, + merged_at: "2026-04-03T00:00:00Z", + targets: [{ repo: "Kilo-Org/kilocode", number: 201, url: r2Url }], + }, + ] + const result = unannotatedRevertSignals(signals, []) + assert.deepEqual(result, { + missed: [], + unparsed: [], + chains: [ + { url: r1Url, targets: [fUrl] }, + { url: r2Url, targets: [r1Url] }, + { url: r3Url, targets: [r2Url] }, + ], + }) + } + + { + // mixed signal: M targets [R1, A, B]; A annotated, B missed; chains lists only R1 + const mUrl = "https://github.com/Kilo-Org/kilocode/pull/800" + const aUrl = "https://github.com/Kilo-Org/kilocode/pull/801" + const bUrl = "https://github.com/Kilo-Org/kilocode/pull/802" + const signals = [ + { + url: r1Url, + merged_at: mergedAt, + targets: [{ repo: "Kilo-Org/kilocode", number: 100, url: fUrl }], + }, + { + url: mUrl, + merged_at: mergedAt2, + targets: [ + { repo: "Kilo-Org/kilocode", number: 200, url: r1Url }, + { repo: "Kilo-Org/kilocode", number: 801, url: aUrl }, + { repo: "Kilo-Org/kilocode", number: 802, url: bUrl }, + ], + }, + ] + const result = unannotatedRevertSignals(signals, [[aUrl, mUrl]]) + assert.deepEqual(result, { + missed: [{ url: mUrl, targets: [bUrl] }], + unparsed: [], + chains: [ + { url: r1Url, targets: [fUrl] }, + { url: mUrl, targets: [r1Url] }, + ], + }) + } + + { + // zero-target signal → unparsed, not missed; chains empty + const emptyUrl = "https://github.com/Kilo-Org/kilocode/pull/400" + const result = unannotatedRevertSignals([{ url: emptyUrl, merged_at: mergedAt, targets: [] }], []) + assert.deepEqual(result, { missed: [], unparsed: [emptyUrl], chains: [] }) + } + + { + // case-insensitivity: annotated set matches target urls differing only by case + const signalUrl = "https://github.com/Kilo-Org/kilocode/pull/600" + const targetMixed = "https://github.com/Kilo-Org/kilocode/pull/700" + const targetLower = "https://github.com/kilo-org/kilocode/pull/700" + const signals = [ + { + url: signalUrl, + merged_at: mergedAt, + targets: [{ repo: "Kilo-Org/kilocode", number: 700, url: targetMixed }], + }, + ] + const result = unannotatedRevertSignals(signals, [[targetLower, signalUrl]]) + assert.deepEqual(result, { missed: [], unparsed: [], chains: [] }) + } + + { + // live-window lock: #12497-shaped signal with both targets covered + const s12497 = "https://github.com/Kilo-Org/kilocode/pull/12497" + const t12249 = "https://github.com/Kilo-Org/kilocode/pull/12249" + const t12481 = "https://github.com/Kilo-Org/kilocode/pull/12481" + const signals = [ + { + url: s12497, + merged_at: mergedAt, + targets: [ + { repo: "Kilo-Org/kilocode", number: 12249, url: t12249 }, + { repo: "Kilo-Org/kilocode", number: 12481, url: t12481 }, + ], + }, + ] + const result = unannotatedRevertSignals(signals, [ + [t12249, s12497], + [t12481, s12497], + ]) + assert.deepEqual(result, { missed: [], unparsed: [], chains: [] }) + } + + // --- source-order guards (case-5 style) --- + { + const collectSrc = fs.readFileSync(COLLECT_SCRIPT, "utf8") + assert.ok(collectSrc.includes("./reverts.mjs"), "collect.mjs must import ./reverts.mjs") + const kindIdx = collectSrc.indexOf("revertTitleKind(item.title") + const dropIdx = collectSrc.indexOf("DROP_TITLE.test") + assert.ok(kindIdx >= 0, "revertTitleKind(item.title call missing") + assert.ok(dropIdx >= 0, "DROP_TITLE.test missing") + assert.ok(kindIdx < dropIdx, "revert interception must run before DROP_TITLE") + + const applyIdx = collectSrc.indexOf("applyRevertAnnotations(digest") + const fullIdx = collectSrc.indexOf("digest-full.json") + assert.ok(applyIdx >= 0, "applyRevertAnnotations(digest call missing") + assert.ok(fullIdx >= 0, "digest-full.json write missing") + assert.ok(applyIdx < fullIdx, "annotations must be applied before digests are written") + + assert.ok( + collectSrc.includes('if (revertTitleKind(item.title ?? "")) {'), + 'collect must use exact intercept line if (revertTitleKind(item.title ?? "")) {', + ) + } + + // --- prompt-text guards --- + { + const triagePrompt = fs.readFileSync(path.join(HERE, "triage-prompt.md"), "utf8") + assert.ok(triagePrompt.includes("reverted_by"), "triage-prompt must mention reverted_by") + assert.ok(triagePrompt.includes("stream-liveness"), "triage-prompt must mention stream-liveness") + assert.ok( + triagePrompt.includes("reverted by https://github.com/Kilo-Org/kilocode/pull/12497"), + "triage-prompt must contain exact cite example", + ) + + const editPrompt = fs.readFileSync(path.join(HERE, "edit-prompt.md"), "utf8") + assert.ok(editPrompt.includes("reverted_by"), "edit-prompt must mention reverted_by") + assert.ok(editPrompt.includes("current source tree"), "edit-prompt must mention current source tree") + assert.ok(editPrompt.includes("Skipping is a normal outcome"), "edit-prompt must mention skipping outcome") + assert.ok(editPrompt.includes("Existence alone is not enough"), "edit-prompt must mention existence guard") + } +} + +// --------------------------------------------------------------------------- +// Case 10 — learnings extraction, validation, injection, upsert safety +// --------------------------------------------------------------------------- +function case10_learnings() { + console.log("case 10: learnings") + + // --- helpers for extraction runs --- + function writeFixture(cwd, data) { + const f = path.join(cwd, "fixture.json") + fs.writeFileSync(f, JSON.stringify(data, null, 2)) + return f + } + + function writeExtractionDelta(cwd, delta) { + fs.mkdirSync(path.join(cwd, "docs-sync-out"), { recursive: true }) + fs.writeFileSync(path.join(cwd, "docs-sync-out", "extraction-delta.json"), JSON.stringify(delta, null, 2)) + } + + // Prepare a git repo for learn.mjs tests: set origin refs, create docs-sync-out. + function setupLearnRepo(dir) { + fs.mkdirSync(path.join(dir, "docs-sync-out"), { recursive: true }) + gitIn(dir, ["update-ref", "refs/remotes/origin/main", "main"]) + gitIn(dir, ["update-ref", "refs/remotes/origin/docs/auto-sync", "docs/auto-sync"]) + return dir + } + + const githubBotEmail = "41898282+github-actions[bot]@users.noreply.github.com" + const kiloconnectBotEmail = "240665456+kiloconnect[bot]@users.noreply.github.com" + + // 10a — three commit classes + { + console.log(" 10a — three commit classes") + const dir = mktemp("docs-sync-learn-a-") + initRepoWithIdentity(dir) + fs.writeFileSync(path.join(dir, "base.txt"), "base\n") + gitIn(dir, ["add", "base.txt"]) + gitIn(dir, ["commit", "-m", "base"]) + + // Branch + gitIn(dir, ["checkout", "-b", "docs/auto-sync"]) + // 1. kiloconnect[bot] commit that touches packages/kilo-docs/pages/x.md + gitIn(dir, ["config", "user.email", kiloconnectBotEmail]) + fs.mkdirSync(path.join(dir, "packages", "kilo-docs", "pages"), { recursive: true }) + fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n") + gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"]) + gitIn(dir, ["commit", "-m", "docs: add x page"]) + const kiloconnectSha = gitIn(dir, ["rev-parse", "HEAD"]) + // 2. github-actions[bot] commit + gitIn(dir, ["config", "user.email", githubBotEmail]) + fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "y.md"), "# y\n") + gitIn(dir, ["add", "packages/kilo-docs/pages/y.md"]) + gitIn(dir, ["commit", "-m", "docs: add y page"]) + // 3. Merge commit (non-merge filter) + gitIn(dir, ["config", "user.email", "someone@example.com"]) + gitIn(dir, ["checkout", "-b", "tmp-merge"]) + fs.writeFileSync(path.join(dir, "z.txt"), "z\n") + gitIn(dir, ["add", "z.txt"]) + gitIn(dir, ["commit", "-m", "tmp"]) + gitIn(dir, ["checkout", "docs/auto-sync"]) + gitIn(dir, ["merge", "--no-ff", "tmp-merge", "-m", "merge tmp"]) + // 4. commit reachable from main + gitIn(dir, ["checkout", "main"]) + fs.writeFileSync(path.join(dir, "main-only.txt"), "main only\n") + gitIn(dir, ["add", "main-only.txt"]) + gitIn(dir, ["commit", "-m", "main only"]) + + gitIn(dir, ["checkout", "docs/auto-sync"]) + const tip = gitIn(dir, ["rev-parse", "HEAD"]) + + // Setup origin refs (needed by learn.mjs git commands) + const cwd = setupLearnRepo(dir) + + const fixturePath = writeFixture(dir, { + pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } }, + comments: [], + }) + + const callLog = path.join(dir, "kilo-calls.log") + const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog }) + writeExtractionDelta(dir, { add: [], remove: [] }) + + const result = runNodeScript(LEARN_SCRIPT, { + cwd: dir, + kiloDir, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_FIXTURE: fixturePath, + LEARNINGS_BUDGET_MINUTES: "1", + DOCS_SYNC_BACKOFF_MS: "0", + }, + }) + + // Assert: input file written, exactly one correction — kiloconnect only. + // github-actions[bot] commit excluded (criterion 5), merge commit excluded + // via --no-merges, main-reachable commit excluded by origin/main range (criterion 6). + const inputFile = path.join(dir, "docs-sync-out", "learnings-input.json") + assert.ok(fs.existsSync(inputFile), `expected ${inputFile}`) + const input = JSON.parse(fs.readFileSync(inputFile, "utf8")) + assert.equal(input.corrections.length, 1, "exactly one correction (kiloconnect commit)") + assert.equal( + input.corrections[0].source, + `commit:${kiloconnectSha.slice(0, 7)}`, + "correction must be kiloconnect commit only", + ) + } + + // 10rz — timestamp correlation with different timezone offsets + // A comment must map to the chronologically earliest eligible commit even when + // timestamps use different timezone offsets (Z vs +05:00). + { + console.log(" 10rz — timestamp correlation") + const dir = mktemp("docs-sync-learn-rz-") + initRepoWithIdentity(dir) + fs.writeFileSync(path.join(dir, "base.txt"), "base\n") + gitIn(dir, ["add", "base.txt"]) + gitIn(dir, ["commit", "-m", "base"]) + + gitIn(dir, ["checkout", "-b", "docs/auto-sync"]) + + // Commit A: non-UTC offset, chronologically earliest (UTC 08:00) + // iso = 2026-08-03T13:00:00+05:00 + fs.mkdirSync(path.join(dir, "packages", "kilo-docs", "pages"), { recursive: true }) + fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n") + gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"]) + gitIn(dir, ["commit", "-m", "first edit x", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`], { + GIT_COMMITTER_DATE: "2026-08-03T13:00:00+05:00", + }) + const shaA = gitIn(dir, ["rev-parse", "HEAD"]) + + // Commit B: UTC offset, chronologically later (UTC 09:00) + // iso = 2026-08-03T09:00:00Z — string comparison would pick this as "earlier" (09 < 13) + // but chronologically A is earlier (08:00 < 09:00) + fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n## edit\n") + gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"]) + gitIn(dir, ["commit", "-m", "second edit x", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`], { + GIT_COMMITTER_DATE: "2026-08-03T09:00:00Z", + }) + const shaB = gitIn(dir, ["rev-parse", "HEAD"]) + + // Seed LEARNINGS.md so existing entries are non-empty but irrelevant + const existing = [ + { id: "pre", rule: "Pre-existing rule.", scope: "both", source: "commit:0000000", date: "2026-01-01" }, + ] + const learningsPath = path.join(dir, "packages", "kilo-docs", "LEARNINGS.md") + fs.mkdirSync(path.dirname(learningsPath), { recursive: true }) + fs.writeFileSync(learningsPath, renderLearnings(existing)) + gitIn(dir, ["add", "packages/kilo-docs/LEARNINGS.md"]) + gitIn(dir, ["commit", "-m", "seed learnings", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`]) + + const cwd = setupLearnRepo(dir) + + // Comment at UTC 05:30 on x.md — before both commits chronologically. + // 05:30 < 08:00 (A) and 05:30 < 09:00 (B) → both eligible + // The chronologically earliest eligible commit is A (08:00). + const fixturePath = writeFixture(cwd, { + pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } }, + comments: [ + { + id: 101, + created_at: "2026-08-03T05:30:00Z", + path: "packages/kilo-docs/pages/x.md", + body: "Please fix the docs.", + author_association: "MEMBER", + user: { login: "maintainer" }, + }, + ], + }) + + const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog: path.join(cwd, "kilo-calls.log") }) + writeExtractionDelta(cwd, { add: [], remove: [] }) + + const result = runNodeScript(LEARN_SCRIPT, { + cwd, + kiloDir, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_FIXTURE: fixturePath, + LEARNINGS_BUDGET_MINUTES: "1", + DOCS_SYNC_BACKOFF_MS: "0", + }, + }) + + // Read learnings-input.json to inspect the correlation result + const inputFile = path.join(cwd, "docs-sync-out", "learnings-input.json") + assert.ok(fs.existsSync(inputFile), "learnings-input.json must exist") + const input = JSON.parse(fs.readFileSync(inputFile, "utf8")) + + // Both commits must appear as corrections + const commitA = input.corrections.find((c) => c.source === `commit:${shaA.slice(0, 7)}`) + const commitB = input.corrections.find((c) => c.source === `commit:${shaB.slice(0, 7)}`) + assert.ok(commitA, "commit A must be in corrections") + assert.ok(commitB, "commit B must be in corrections") + + // Comment must be associated with commit A (chronologically earliest) + assert.ok(commitA.comment, "commit A must have the comment associated") + assert.equal(commitA.comment.path, "packages/kilo-docs/pages/x.md") + assert.equal(commitB.comment, undefined, "commit B must not have the comment associated") + + // No standalone comment candidate — the comment was correlated, not orphaned + const standalone = input.corrections.filter((c) => c.source && c.source.startsWith("comment:")) + assert.equal(standalone.length, 0, "comment must be associated, not standalone") + } + + // 10b — watermark suppression (no model call when marker covers all) + { + console.log(" 10b — watermark suppression") + const dir = mktemp("docs-sync-learn-b-") + initRepoWithIdentity(dir) + fs.writeFileSync(path.join(dir, "base.txt"), "base\n") + gitIn(dir, ["add", "base.txt"]) + gitIn(dir, ["commit", "-m", "base"]) + + gitIn(dir, ["checkout", "-b", "docs/auto-sync"]) + // corrective commit + fs.mkdirSync(path.join(dir, "packages", "kilo-docs", "pages"), { recursive: true }) + fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n") + gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"]) + gitIn(dir, ["commit", "-m", "docs update", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`]) + + // Write LEARNINGS.md on the branch first, so the marker can point to the tip after it + const existing = [ + { id: "test", rule: "existing rule", scope: "both", source: "commit:0000000", date: "2026-01-01" }, + ] + const learningsPath = path.join(dir, "packages", "kilo-docs", "LEARNINGS.md") + const learningsContent = renderLearnings(existing) + fs.mkdirSync(path.dirname(learningsPath), { recursive: true }) + fs.writeFileSync(learningsPath, learningsContent) + gitIn(dir, ["add", "packages/kilo-docs/LEARNINGS.md"]) + gitIn(dir, ["commit", "-m", "seed learnings"]) + + const tip = gitIn(dir, ["rev-parse", "HEAD"]) + const tipDate = new Date().toISOString() + + const cwd = setupLearnRepo(dir) + const body = `` + const fixturePath = writeFixture(cwd, { + pr: { number: 1, head: { ref: "docs/auto-sync" }, body, user: { login: "github-actions[bot]" } }, + comments: [], + }) + + const callLog = path.join(cwd, "kilo-calls.log") + const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog }) + writeExtractionDelta(cwd, { add: [], remove: [] }) + + fs.writeFileSync(path.join(cwd, "docs-sync-out", "learnings.json"), JSON.stringify(existing, null, 2)) + + const result = runNodeScript(LEARN_SCRIPT, { + cwd, + kiloDir, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_FIXTURE: fixturePath, + LEARNINGS_BUDGET_MINUTES: "1", + DOCS_SYNC_BACKOFF_MS: "0", + }, + }) + + // Assert: stub never invoked, learnings.json unchanged, no model call + const calls = fs.existsSync(callLog) ? fs.readFileSync(callLog, "utf8").trim() : "" + assert.equal(calls, "", `kilo must not be invoked when marker covers all; got ${calls}`) + const out = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "learnings.json"), "utf8")) + assert.deepEqual(out, existing, "learnings.json must equal existing entries") + + // Run apply and prove LEARNINGS.md is byte-unchanged + const lp = path.join(cwd, "packages", "kilo-docs", "LEARNINGS.md") + const before = fs.readFileSync(lp, "utf8") + runNodeScript(LEARN_SCRIPT, { + cwd, + kiloDir, + args: ["--apply"], + env: { DOCS_SYNC_BACKOFF_MS: "0" }, + }) + const after = fs.readFileSync(lp, "utf8") + assert.equal(after, before, "LEARNINGS.md must be byte-unchanged after apply") + } + + // 10c — rerun idempotency + { + console.log(" 10c — rerun idempotency") + const dir = mktemp("docs-sync-learn-c-") + initRepoWithIdentity(dir) + fs.writeFileSync(path.join(dir, "base.txt"), "base\n") + gitIn(dir, ["add", "base.txt"]) + gitIn(dir, ["commit", "-m", "base"]) + + gitIn(dir, ["checkout", "-b", "docs/auto-sync"]) + fs.mkdirSync(path.join(dir, "packages", "kilo-docs", "pages"), { recursive: true }) + fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n") + gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"]) + gitIn(dir, ["commit", "-m", "docs update", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`]) + let tip = gitIn(dir, ["rev-parse", "HEAD"]) + + const add = [ + { + id: "new-rule", + rule: "A new rule from testing.", + scope: "both", + source: `commit:${tip.slice(0, 7)}`, + date: "2026-08-03", + }, + ] + let firstLEARNINGS + + // First run + { + const cwd = setupLearnRepo(dir) + const fixturePath = writeFixture(cwd, { + pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } }, + comments: [], + }) + const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog: path.join(cwd, "kilo-calls.log") }) + writeExtractionDelta(cwd, { add, remove: [] }) + const result = runNodeScript(LEARN_SCRIPT, { + cwd, + kiloDir, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_FIXTURE: fixturePath, + LEARNINGS_BUDGET_MINUTES: "1", + DOCS_SYNC_BACKOFF_MS: "0", + }, + }) + const out1 = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "learnings.json"), "utf8")) + assert.equal(out1.length, 1) + assert.equal(out1[0].id, "new-rule") + // Write LEARNINGS.md on the branch so second run sees existing entries + const learningsPath = path.join(dir, "packages", "kilo-docs", "LEARNINGS.md") + fs.mkdirSync(path.dirname(learningsPath), { recursive: true }) + fs.writeFileSync(learningsPath, renderLearnings(out1)) + gitIn(dir, ["add", "packages/kilo-docs/LEARNINGS.md"]) + gitIn(dir, ["commit", "-m", "seed learnings"]) + tip = gitIn(dir, ["rev-parse", "HEAD"]) + firstLEARNINGS = fs.readFileSync(path.join(dir, "packages", "kilo-docs", "LEARNINGS.md"), "utf8") + } + + // Second run with marker covering first run's result + { + const cwd = setupLearnRepo(dir) + const body = `` + const fixturePath = writeFixture(cwd, { + pr: { number: 1, head: { ref: "docs/auto-sync" }, body, user: { login: "github-actions[bot]" } }, + comments: [], + }) + const callLog2 = path.join(cwd, "kilo-calls-run2.log") + const kiloDir2 = makeStubKiloDir({ mode: "extraction-delta", callLog: callLog2 }) + writeExtractionDelta(cwd, { add, remove: [] }) + const result = runNodeScript(LEARN_SCRIPT, { + cwd, + kiloDir: kiloDir2, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_FIXTURE: fixturePath, + LEARNINGS_BUDGET_MINUTES: "1", + DOCS_SYNC_BACKOFF_MS: "0", + }, + }) + const calls2 = fs.existsSync(callLog2) ? fs.readFileSync(callLog2, "utf8").trim() : "" + assert.equal(calls2, "", "second run must not invoke kilo") + const out2 = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "learnings.json"), "utf8")) + assert.equal(out2.length, 1) + assert.equal(out2[0].id, "new-rule") + // Run apply and prove LEARNINGS.md is byte-identical after idempotent rerun + runNodeScript(LEARN_SCRIPT, { + cwd, + kiloDir: kiloDir2, + args: ["--apply"], + env: { DOCS_SYNC_BACKOFF_MS: "0" }, + }) + const afterApply = fs.readFileSync(path.join(dir, "packages", "kilo-docs", "LEARNINGS.md"), "utf8") + assert.equal(afterApply.length, firstLEARNINGS.length, "LEARNINGS.md must be same length after apply") + assert.equal(afterApply, firstLEARNINGS, "LEARNINGS.md must be byte-identical after apply") + } + } + + // 10d — contradiction replacement (applyDelta) + { + console.log(" 10d — contradiction replacement") + const old = [ + { id: "old-rule", rule: "Old rule text.", scope: "both", source: "commit:aaaaaaa", date: "2026-01-01" }, + ] + const add = [ + { id: "new-rule", rule: "New rule text.", scope: "both", source: "commit:bbbbbbb", date: "2026-02-01" }, + ] + const delta = { add, remove: ["old-rule"] } + const result = applyDelta(old, delta) + assert.equal(result.length, 1) + assert.equal(result[0].id, "new-rule") + // Third delta touching neither does not bring old back + const again = applyDelta(result, { add: [], remove: [] }) + assert.equal(again.length, 1) + assert.equal(again[0].id, "new-rule") + } + + // 10e — prompt injection (triage/edit argv carry tagged rules) + { + console.log(" 10e — prompt injection") + const dir = mktemp("docs-sync-learn-e-") + initRepoWithIdentity(dir) + fs.writeFileSync(path.join(dir, "base.txt"), "base\n") + gitIn(dir, ["add", "base.txt"]) + gitIn(dir, ["commit", "-m", "base"]) + gitIn(dir, ["checkout", "-b", "docs/auto-sync"]) + // Add a corrective commit so extraction has a candidate + fs.mkdirSync(path.join(dir, "packages", "kilo-docs", "pages"), { recursive: true }) + fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n") + gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"]) + gitIn(dir, ["commit", "-m", "docs update", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`]) + const commitSha = gitIn(dir, ["rev-parse", "HEAD"]) + + const cwd = setupLearnRepo(dir) + const fixturePath = writeFixture(cwd, { + pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } }, + comments: [], + }) + const callLog = path.join(cwd, "kilo-calls.log") + const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog }) + // Three entries: triage, edit, both — all from the same candidate source + const src = `commit:${commitSha.slice(0, 7)}` + writeExtractionDelta(cwd, { + add: [ + { id: "triage-rule", rule: "Triage-only rule text.", scope: "triage", source: src, date: "2026-08-01" }, + { id: "edit-rule", rule: "Edit-only rule text.", scope: "edit", source: src, date: "2026-08-02" }, + { id: "both-rule", rule: "Both scope rule text.", scope: "both", source: src, date: "2026-08-03" }, + ], + remove: [], + }) + + // Run extraction so it writes learnings-.md blocks (extraction step 13). + // Only extraction writes these files; --apply writes only LEARNINGS.md. + runNodeScript(LEARN_SCRIPT, { + cwd, + kiloDir, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_FIXTURE: fixturePath, + LEARNINGS_BUDGET_MINUTES: "1", + DOCS_SYNC_BACKOFF_MS: "0", + }, + }) + + const triageBlockPath = path.join(cwd, "docs-sync-out", "learnings-triage.md") + const editBlockPath = path.join(cwd, "docs-sync-out", "learnings-edit.md") + assert.ok(fs.existsSync(triageBlockPath), "learnings-triage.md must exist") + assert.ok(fs.existsSync(editBlockPath), "learnings-edit.md must exist") + + // Run triage.mjs against a recording stub. Copy the learnings block into + // its cwd so readLearningsBlock picks it up. + { + const triageCwd = setupTriageCwd([samplePr(1)]) + fs.copyFileSync(triageBlockPath, path.join(triageCwd, "docs-sync-out", "learnings-triage.md")) + const triageCallLog = path.join(triageCwd, "triage-calls.log") + const triageKiloDir = makeStubKiloDir({ mode: "record", callLog: triageCallLog }) + runNodeScript(TRIAGE_SCRIPT, { + cwd: triageCwd, + kiloDir: triageKiloDir, + env: { TRIAGE_MODEL: "test/model", DOCS_SYNC_BACKOFF_MS: "0" }, + }) + const logText = fs.readFileSync(triageCallLog, "utf8") + assert.ok(logText.includes("Triage-only rule text"), "triage argv must contain triage rule") + assert.ok(logText.includes("Both scope rule text"), "triage argv must contain both rule") + assert.ok(!logText.includes("Edit-only rule text"), "triage argv must not contain edit-only rule") + } + + // Run edit.mjs against a recording stub. + { + const triageEntry = { + pr: 1, + url: "https://github.com/Kilo-Org/cloud/pull/1", + docs_worthy: true, + reason: "needs docs", + target_sections: [], + priority: "medium", + } + const editCwd = setupEditCwd([samplePr(1)], [triageEntry]) + fs.copyFileSync(editBlockPath, path.join(editCwd, "docs-sync-out", "learnings-edit.md")) + const editCallLog = path.join(editCwd, "edit-calls.log") + const editKiloDir = makeStubKiloDir({ mode: "record", callLog: editCallLog }) + runNodeScript(EDIT_SCRIPT, { + cwd: editCwd, + kiloDir: editKiloDir, + env: { EDIT_MODEL: "test/model", DOCS_SYNC_BACKOFF_MS: "0" }, + }) + const logText = fs.readFileSync(editCallLog, "utf8") + assert.ok(logText.includes("Edit-only rule text"), "edit argv must contain edit rule") + assert.ok(logText.includes("Both scope rule text"), "edit argv must contain both rule") + assert.ok(!logText.includes("Triage-only rule text"), "edit argv must not contain triage-only rule") + } + } + + // 10f — failure path + { + console.log(" 10f — failure path") + const dir = mktemp("docs-sync-learn-f-") + initRepoWithIdentity(dir) + fs.writeFileSync(path.join(dir, "base.txt"), "base\n") + gitIn(dir, ["add", "base.txt"]) + gitIn(dir, ["commit", "-m", "base"]) + gitIn(dir, ["checkout", "-b", "docs/auto-sync"]) + fs.mkdirSync(path.join(dir, "packages", "kilo-docs", "pages"), { recursive: true }) + fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n") + gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"]) + gitIn(dir, ["commit", "-m", "docs update", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`]) + + const existing = [ + { id: "test", rule: "existing rule", scope: "both", source: "commit:0000000", date: "2026-01-01" }, + ] + // Write LEARNINGS.md on the branch so learn.mjs reads it as existing entries + const learningsPath = path.join(dir, "packages", "kilo-docs", "LEARNINGS.md") + fs.mkdirSync(path.dirname(learningsPath), { recursive: true }) + fs.writeFileSync(learningsPath, renderLearnings(existing)) + gitIn(dir, ["add", "packages/kilo-docs/LEARNINGS.md"]) + gitIn(dir, ["commit", "-m", "seed learnings"]) + + const cwd = setupLearnRepo(dir) + const fixturePath = writeFixture(cwd, { + pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } }, + comments: [], + }) + + // Stub exits 0 with garbage stdout (stderr-exit0 mode) + const stderrText = "some fake error stream" + const kiloDir = makeStubKiloDir({ mode: "stderr-exit0", stderrText }) + + const outputFile = path.join(cwd, "gh-output-f") + const result = runNodeScript(LEARN_SCRIPT, { + cwd, + kiloDir, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_FIXTURE: fixturePath, + LEARNINGS_BUDGET_MINUTES: "1", + DOCS_SYNC_BACKOFF_MS: "0", + GITHUB_OUTPUT: outputFile, + }, + }) + + assert.equal(result.status, 0, "learn.mjs must exit 0 on failure") + const out = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "learnings.json"), "utf8")) + assert.deepEqual(out, existing, "learnings.json must equal existing entries on failure") + // No marker PATCH file + assert.ok(!fs.existsSync(`${fixturePath}.patched`), "no marker PATCH on failure") + // No learned_through output + if (fs.existsSync(outputFile)) { + const ghOut = fs.readFileSync(outputFile, "utf8") + assert.ok(!ghOut.includes("learned_through="), "GITHUB_OUTPUT must not contain learned_through on failure") + } + + // Run apply and prove LEARNINGS.md is byte-unchanged + const lp = path.join(cwd, "packages", "kilo-docs", "LEARNINGS.md") + const before = fs.readFileSync(lp, "utf8") + runNodeScript(LEARN_SCRIPT, { + cwd, + kiloDir, + args: ["--apply"], + env: { DOCS_SYNC_BACKOFF_MS: "0" }, + }) + const after = fs.readFileSync(lp, "utf8") + assert.equal(after, before, "LEARNINGS.md must be byte-unchanged after apply on failure") + } + + // 10g — general-rule check (validateDelta rejections) + { + console.log(" 10g — general-rule check") + const existing = [] + const sources = ["commit:aaaaaaa"] + const entryWithPR = { + id: "bad-pr", + rule: "See #12716 for details", + scope: "both", + source: "commit:aaaaaaa", + date: "2026-08-03", + } + const entryWithURL = { + id: "bad-url", + rule: "Check https://example.com", + scope: "both", + source: "commit:aaaaaaa", + date: "2026-08-03", + } + const entryWithPerson = { + id: "bad-person", + rule: "Ask @emilieschario", + scope: "both", + source: "commit:aaaaaaa", + date: "2026-08-03", + } + const entryWithPage = { + id: "bad-page", + rule: "Edit packages/kilo-docs/pages/x.md", + scope: "both", + source: "commit:aaaaaaa", + date: "2026-08-03", + } + const entryBadSource = { + id: "bad-source", + rule: "A valid sentence.", + scope: "both", + source: "invented", + date: "2026-08-03", + } + const entryBadScope = { + id: "bad-scope", + rule: "A valid sentence.", + scope: "wrong", + source: "commit:aaaaaaa", + date: "2026-08-03", + } + const entryCollide = { + id: "existing-id", + rule: "A valid sentence.", + scope: "both", + source: "commit:aaaaaaa", + date: "2026-08-03", + } + const entryBadDate = { + id: "bad-date", + rule: "A valid sentence.", + scope: "both", + source: "commit:aaaaaaa", + date: "not-a-date", + } + const entryRemoveUnknown = { + id: "valid", + rule: "A valid sentence.", + scope: "both", + source: "commit:aaaaaaa", + date: "2026-08-03", + } + + const existingWithId = [ + { id: "existing-id", rule: "Existing rule.", scope: "both", source: "commit:aaaaaaa", date: "2026-01-01" }, + ] + + // PR number + { + const { rejected } = validateDelta( + { add: [entryWithPR], remove: [] }, + { existing, candidateSources: sources, deletedInWindow: [] }, + ) + assert.equal(rejected.length, 1, "PR number must be rejected") + assert.ok(rejected[0].reason, "rejection must carry a reason") + } + + // URL — docs-check-links.yml link-checks LEARNINGS.md with fail:true, so + // a URL in a rule would break CI on every bot commit. + { + const { rejected } = validateDelta( + { add: [entryWithURL], remove: [] }, + { existing, candidateSources: sources, deletedInWindow: [] }, + ) + assert.equal(rejected.length, 1, "URL must be rejected") + } + + // Person + { + const { rejected } = validateDelta( + { add: [entryWithPerson], remove: [] }, + { existing, candidateSources: sources, deletedInWindow: [] }, + ) + assert.equal(rejected.length, 1, "person reference must be rejected") + } + + // Docs page + { + const { rejected } = validateDelta( + { add: [entryWithPage], remove: [] }, + { existing, candidateSources: sources, deletedInWindow: [] }, + ) + assert.equal(rejected.length, 1, "docs page reference must be rejected") + } + + // Bad source + { + const { rejected } = validateDelta( + { add: [entryBadSource], remove: [] }, + { existing, candidateSources: sources, deletedInWindow: [] }, + ) + assert.equal(rejected.length, 1, "invented source must be rejected") + } + + // Bad scope + { + const { rejected } = validateDelta( + { add: [entryBadScope], remove: [] }, + { existing, candidateSources: sources, deletedInWindow: [] }, + ) + assert.equal(rejected.length, 1, "bad scope must be rejected") + } + + // Colliding id + { + const { rejected } = validateDelta( + { add: [entryCollide], remove: [] }, + { existing: existingWithId, candidateSources: sources, deletedInWindow: [] }, + ) + assert.equal(rejected.length, 1, "colliding id must be rejected") + } + + // Remove unknown id + { + const { rejected } = validateDelta( + { add: [entryRemoveUnknown], remove: ["unknown-id"] }, + { existing, candidateSources: sources, deletedInWindow: [] }, + ) + assert.equal(rejected.length, 1, "remove of unknown id must be rejected") + } + + // Bad date + { + const { rejected } = validateDelta( + { add: [entryBadDate], remove: [] }, + { existing, candidateSources: sources, deletedInWindow: [] }, + ) + assert.equal(rejected.length, 1, "bad date must be rejected") + } + } + + // 10h — comment trust (isTrustedComment) + { + console.log(" 10h — comment trust") + assert.equal(isTrustedComment({ author_association: "OWNER", user: { login: "owner-user" } }), true) + assert.equal(isTrustedComment({ author_association: "MEMBER", user: { login: "emilieschario" } }), true) + assert.equal(isTrustedComment({ author_association: "COLLABORATOR", user: { login: "collab-user" } }), true) + assert.equal(isTrustedComment({ author_association: "CONTRIBUTOR", user: { login: "kilo-code-bot[bot]" } }), false) + assert.equal(isTrustedComment({ author_association: "NONE", user: { login: "rando" } }), false) + // MEMBER whose login ends in [bot] + assert.equal(isTrustedComment({ author_association: "MEMBER", user: { login: "some-bot[bot]" } }), false) + } + + // 10i — draft gate (nonContentFiles) + { + console.log(" 10i — draft gate") + const files = ["packages/kilo-docs/LEARNINGS.md", "packages/kilo-docs/pages/a.md"] + const result = nonContentFiles(files) + assert.equal(result.length, 0, "LEARNINGS.md and pages must not trigger the draft gate") + // Still flags non-content + const withConfig = ["packages/kilo-docs/next.config.js"] + const flagged = nonContentFiles(withConfig) + assert.equal(flagged.length, 1, "next.config.js must still trigger the gate") + assert.equal(flagged[0], "packages/kilo-docs/next.config.js") + } + + // 10j — no --auto on extraction call + { + console.log(" 10j — no --auto on extraction call") + const src = fs.readFileSync(LEARN_SCRIPT, "utf8") + + // Find the extraction-mode runKilo args array + const argsStart = src.indexOf("runKilo({") + assert.ok(argsStart >= 0, "runKilo call must exist in learn.mjs") + const argsBlock = src.slice(argsStart, src.indexOf("})", argsStart) + 2) + assert.ok(!argsBlock.includes("--auto"), "extraction runKilo must not include --auto") + assert.ok(argsBlock.includes("-f"), "extraction runKilo must include -f") + } + + // 10k — hand-mangled file (parseLearnings) + { + console.log(" 10k — hand-mangled file") + const text = `# header + +- Valid rule. +- Broken meta. +Just prose, not a rule line. +` + const entries = parseLearnings(text) + assert.equal(entries.length, 1, "only valid entry must parse") + assert.equal(entries[0].id, "valid-rule") + } + + // 10l — first-run fallback (main when branch has none) + // Prove the git commands learn.mjs relies on: when the branch file is absent, + // git show origin/:packages/kilo-docs/LEARNINGS.md fails, and + // git show origin/main:packages/kilo-docs/LEARNINGS.md returns the main's entries. + { + console.log(" 10l — empty file fallback") + const dir = mktemp("docs-sync-learn-l-") + initRepoWithIdentity(dir) + + // Write LEARNINGS.md on main + const entries = [ + { id: "test", rule: "Test rule text.", scope: "both", source: "commit:aaaaaaa", date: "2026-01-01" }, + ] + const lp = path.join(dir, "packages", "kilo-docs", "LEARNINGS.md") + fs.mkdirSync(path.dirname(lp), { recursive: true }) + fs.writeFileSync(lp, renderLearnings(entries)) + gitIn(dir, ["add", "packages/kilo-docs/LEARNINGS.md"]) + gitIn(dir, ["commit", "-m", "main learnings"]) + + // Branch from main, then remove LEARNINGS.md + gitIn(dir, ["checkout", "-b", "docs/auto-sync"]) + fs.rmSync(lp) + gitIn(dir, ["add", "packages/kilo-docs/LEARNINGS.md"]) + gitIn(dir, ["commit", "-m", "remove learnings on branch"]) + + // Set up origin refs so git show origin/ resolves + setupLearnRepo(dir) + + // git show on branch must fail — file absent at that ref + let branchFailed = false + try { + gitIn(dir, ["show", "origin/docs/auto-sync:packages/kilo-docs/LEARNINGS.md"]) + } catch { + branchFailed = true + } + assert.ok(branchFailed, "git show on branch must fail when LEARNINGS.md absent") + + // git show on main must succeed with the main's entries + const mainContent = gitIn(dir, ["show", "origin/main:packages/kilo-docs/LEARNINGS.md"]) + const parsed = parseLearnings(mainContent) + assert.equal(parsed.length, 1, "main fallback must return the main's entries") + assert.equal(parsed[0].id, "test") + + // Also verify: empty parse and render (unit coverage of the empty case) + const empty = parseLearnings("") + assert.equal(empty.length, 0) + assert.deepEqual(empty, []) + const rendered = renderLearnings([]) + assert.ok(rendered.includes("")) + assert.ok(rendered.includes("")) + } + + // 10m — empty delta advances marker with no file change + { + console.log(" 10m — empty delta marker advance") + const dir = mktemp("docs-sync-learn-m-") + initRepoWithIdentity(dir) + fs.writeFileSync(path.join(dir, "base.txt"), "base\n") + gitIn(dir, ["add", "base.txt"]) + gitIn(dir, ["commit", "-m", "base"]) + gitIn(dir, ["checkout", "-b", "docs/auto-sync"]) + fs.mkdirSync(path.join(dir, "packages", "kilo-docs", "pages"), { recursive: true }) + fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n") + gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"]) + gitIn(dir, ["commit", "-m", "docs update", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`]) + const tip = gitIn(dir, ["rev-parse", "HEAD"]) + + const cwd = setupLearnRepo(dir) + const fixturePath = writeFixture(cwd, { + pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } }, + comments: [], + }) + + const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog: path.join(cwd, "kilo-calls.log") }) + writeExtractionDelta(cwd, { add: [], remove: [] }) + + const existing = [] + fs.writeFileSync(path.join(cwd, "docs-sync-out", "learnings.json"), JSON.stringify(existing, null, 2)) + const learningsPath = path.join(cwd, "packages", "kilo-docs", "LEARNINGS.md") + fs.mkdirSync(path.dirname(learningsPath), { recursive: true }) + fs.writeFileSync(learningsPath, renderLearnings(existing)) + const before = fs.readFileSync(learningsPath, "utf8") + + const outputFile = path.join(cwd, "gh-output-m") + const result = runNodeScript(LEARN_SCRIPT, { + cwd, + kiloDir, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_FIXTURE: fixturePath, + LEARNINGS_BUDGET_MINUTES: "1", + DOCS_SYNC_BACKOFF_MS: "0", + GITHUB_OUTPUT: outputFile, + }, + }) + + // learnings.json unchanged + const out = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "learnings.json"), "utf8")) + assert.deepEqual(out, existing) + + // No learned_through output (empty delta route omits it per G5 table) + if (fs.existsSync(outputFile)) { + const ghOut = fs.readFileSync(outputFile, "utf8") + assert.ok(!ghOut.includes("learned_through="), "GITHUB_OUTPUT must not contain learned_through on empty delta") + } + const patched = `${fixturePath}.patched` + assert.ok(fs.existsSync(patched), "marker PATCH file must be written for empty delta") + const markerText = fs.readFileSync(patched, "utf8") + assert.ok(markerText.includes(tip), "marker PATCH must contain tip SHA") + + runNodeScript(LEARN_SCRIPT, { + cwd, + kiloDir, + args: ["--apply"], + env: { DOCS_SYNC_BACKOFF_MS: "0" }, + }) + assert.equal(fs.readFileSync(learningsPath, "utf8"), before, "LEARNINGS.md must be byte-unchanged after apply") + + // patchMarkerIntoBody: existing marker → replaced in place + { + const oldBody = "some text\n\nmore text\n" + const newLine = "" + const patched = patchMarkerIntoBody(oldBody, newLine) + assert.ok(patched.includes(newLine), "new marker must be in body") + assert.ok(!patched.includes("commit=old"), "old marker must be gone") + assert.equal( + (patched.match(/" + const patched = patchMarkerIntoBody(oldBody, newLine) + assert.ok(patched.includes(newLine)) + } + } + + // 10n — non-empty delta routes through upsert (not learn.mjs PATCH) + { + console.log(" 10n — non-empty delta marker route") + const dir = mktemp("docs-sync-learn-n-") + initRepoWithIdentity(dir) + fs.writeFileSync(path.join(dir, "base.txt"), "base\n") + gitIn(dir, ["add", "base.txt"]) + gitIn(dir, ["commit", "-m", "base"]) + gitIn(dir, ["checkout", "-b", "docs/auto-sync"]) + fs.mkdirSync(path.join(dir, "packages", "kilo-docs", "pages"), { recursive: true }) + fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n") + gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"]) + gitIn(dir, ["commit", "-m", "docs update", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`]) + const source = `commit:${gitIn(dir, ["rev-parse", "HEAD"]).slice(0, 7)}` + const tip = gitIn(dir, ["rev-parse", "HEAD"]) + + const cwd = setupLearnRepo(dir) + const fixturePath = writeFixture(cwd, { + pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } }, + comments: [], + }) + + const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog: path.join(cwd, "kilo-calls.log") }) + writeExtractionDelta(cwd, { + add: [ + { id: "new-rule", rule: "A new rule.", scope: "both", source: `commit:${tip.slice(0, 7)}`, date: "2026-08-03" }, + ], + remove: [], + }) + + const summaryFile = path.join(cwd, "step-summary.md") + fs.writeFileSync(summaryFile, "") + const outputFile = path.join(cwd, "gh-output") + const result = runNodeScript(LEARN_SCRIPT, { + cwd, + kiloDir, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_FIXTURE: fixturePath, + LEARNINGS_BUDGET_MINUTES: "1", + DOCS_SYNC_BACKOFF_MS: "0", + GITHUB_STEP_SUMMARY: summaryFile, + GITHUB_OUTPUT: outputFile, + }, + }) + + // No marker PATCH file + assert.ok(!fs.existsSync(`${fixturePath}.patched`), "non-empty delta must not PATCH marker") + + // Assert learned_through was written to GITHUB_OUTPUT (non-empty delta route) + const ghOut = fs.readFileSync(outputFile, "utf8") + assert.ok(ghOut.includes("learned_through="), "GITHUB_OUTPUT must contain learned_through on non-empty delta") + assert.ok(ghOut.includes(`commit=${tip}`), "learned_through must contain tip SHA") + + // renderBody with learnedThrough marker + const marker = "" + const body1 = renderBody({ + date: "2026-08-03", + since: "2026-07-01T00:00:00.000Z", + through: "2026-08-03T00:00:00.000Z", + learnedThrough: marker, + changesRows: [], + pendingRows: [], + skippedRows: [], + verified: true, + draftReasons: [], + note: "", + }) + assert.ok(body1.includes(marker), "renderBody must emit marker when given") + + // renderBody with parameter omitted → no marker + const body2 = renderBody({ + date: "2026-08-03", + since: "2026-07-01T00:00:00.000Z", + through: "2026-08-03T00:00:00.000Z", + changesRows: [], + pendingRows: [], + skippedRows: [], + verified: true, + draftReasons: [], + note: "", + }) + assert.ok(!body2.includes("learned-through"), "renderBody without learnedThrough must emit no marker") + // Still round-trips + const extChanges = extractSectionRows(body2, "changes") + assert.deepEqual(extChanges, []) + } + + // 10o — deleted-in-window rejection + { + console.log(" 10o — deleted-in-window rejection") + // Normalized match catches near-identical wording + { + const delta = { + add: [ + { + id: "dup", + rule: "do not document experimental features!", + scope: "both", + source: "commit:aaaaaaa", + date: "2026-08-03", + }, + ], + remove: [], + } + const { rejected } = validateDelta(delta, { + existing: [], + candidateSources: ["commit:aaaaaaa"], + deletedInWindow: ["Do not document experimental features."], + }) + assert.equal(rejected.length, 1, "normalized match must reject deleted rule") + } + + // Different meaning on same topic — NOT rejected (accepted limit) + { + const delta = { + add: [ + { + id: "good", + rule: "Document experimental features in a separate section.", + scope: "both", + source: "commit:aaaaaaa", + date: "2026-08-03", + }, + ], + remove: [], + } + const { rejected } = validateDelta(delta, { + existing: [], + candidateSources: ["commit:aaaaaaa"], + deletedInWindow: ["Do not document experimental features."], + }) + assert.equal(rejected.length, 0, "different rule on same topic must not be rejected") + } + } + + // 10p — resolveLearnedThrough pure function and anti-drift assertions + { + console.log(" 10p — resolveLearnedThrough + anti-drift") + // Unit tests on the pure export + // env value set wins over body marker + assert.equal( + resolveLearnedThrough({ + envValue: "", + prBody: "", + }), + "", + ) + // env unset, body marker present → body wins + assert.equal( + resolveLearnedThrough({ envValue: "", prBody: "" }), + "", + ) + // both absent → empty string + assert.equal(resolveLearnedThrough({ envValue: "", prBody: "" }), "") + // env set to whitespace → treated as unset + assert.equal( + resolveLearnedThrough({ + envValue: " ", + prBody: "", + }), + "", + ) + + // renderBody emits no marker for "" + const bodyEmpty = renderBody({ + date: "d", + since: "s", + through: "t", + learnedThrough: "", + changesRows: [], + pendingRows: [], + skippedRows: [], + verified: true, + draftReasons: [], + note: "", + }) + assert.ok(!bodyEmpty.includes("learned-through"), "empty learnedThrough must emit no marker") + + // Anti-drift: static-source assertions on upsert-pr.mjs + const upsertSrc = fs.readFileSync(path.join(HERE, "upsert-pr.mjs"), "utf8") + + // (a) exactly one resolveLearnedThrough( call passing process.env.LEARNED_THROUGH + const calls = upsertSrc.match(/resolveLearnedThrough\(/g) || [] + // One in the export definition, one in the call site + assert.ok(calls.length >= 2, `expected at least 2 resolveLearnedThrough( occurrences; got ${calls.length}`) + assert.ok( + upsertSrc.includes("process.env.LEARNED_THROUGH"), + "resolveLearnedThrough must receive process.env.LEARNED_THROUGH", + ) + assert.ok(upsertSrc.includes("prBody"), "resolveLearnedThrough must receive prBody") + + // (b) prBody is at function scope (let prBody before the if block) + const prBodyIdx = upsertSrc.indexOf('let prBody = ""') + assert.ok(prBodyIdx >= 0, 'prBody must be declared at function scope with let prBody = ""') + const ifIdx = upsertSrc.indexOf('if (mode === "update"') + assert.ok(prBodyIdx < ifIdx, 'let prBody must appear before if (mode === "update"...)') + + // (c) renderBody({ argument object contains learnedThrough + const renderBodyIdx = upsertSrc.indexOf("const body = renderBody({") + assert.ok(renderBodyIdx >= 0, "renderBody call must exist") + const afterRenderBody = upsertSrc.slice(renderBodyIdx) + const renderBodyArgsEnd = afterRenderBody.indexOf("})") + const renderBodyArgs = afterRenderBody.slice(0, renderBodyArgsEnd) + assert.ok(renderBodyArgs.includes("learnedThrough"), "renderBody call in main() must pass learnedThrough") + } + + // 10q — dry run makes no live write + { + console.log(" 10q — dry run no live write") + const dir = mktemp("docs-sync-learn-q-") + initRepoWithIdentity(dir) + fs.writeFileSync(path.join(dir, "base.txt"), "base\n") + gitIn(dir, ["add", "base.txt"]) + gitIn(dir, ["commit", "-m", "base"]) + gitIn(dir, ["checkout", "-b", "docs/auto-sync"]) + fs.mkdirSync(path.join(dir, "packages", "kilo-docs", "pages"), { recursive: true }) + fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n") + gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"]) + gitIn(dir, ["commit", "-m", "docs update", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`]) + + // DRY_RUN=true + { + const cwd = setupLearnRepo(dir) + const fixturePath = writeFixture(cwd, { + pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } }, + comments: [], + }) + + const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog: path.join(cwd, "kilo-calls.log") }) + writeExtractionDelta(cwd, { add: [], remove: [] }) + + const outputFile = path.join(cwd, "gh-output-q-dry") + const result = runNodeScript(LEARN_SCRIPT, { + cwd, + kiloDir, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_FIXTURE: fixturePath, + LEARNINGS_BUDGET_MINUTES: "1", + DOCS_SYNC_BACKOFF_MS: "0", + DRY_RUN: "true", + GITHUB_OUTPUT: outputFile, + }, + }) + + assert.ok(!fs.existsSync(`${fixturePath}.patched`), "DRY_RUN must suppress marker PATCH") + if (fs.existsSync(outputFile)) { + const ghOut = fs.readFileSync(outputFile, "utf8") + assert.ok(!ghOut.includes("learned_through="), "GITHUB_OUTPUT must not contain learned_through on DRY_RUN") + } + assert.ok(result.stdout.includes("marker PATCH suppressed"), "stdout must log marker suppression for DRY_RUN") + assert.ok( + result.stdout.includes("would have written marker"), + "stdout must log the suppressed marker for DRY_RUN", + ) + } + + // LEARNINGS_NO_PATCH=1 (same mechanism) + { + const cwd = setupLearnRepo(dir) + const fixturePath = writeFixture(cwd, { + pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } }, + comments: [], + }) + + const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog: path.join(cwd, "kilo-calls.log") }) + writeExtractionDelta(cwd, { add: [], remove: [] }) + + const outputFile = path.join(cwd, "gh-output-q-nopatch") + const result = runNodeScript(LEARN_SCRIPT, { + cwd, + kiloDir, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_FIXTURE: fixturePath, + LEARNINGS_BUDGET_MINUTES: "1", + DOCS_SYNC_BACKOFF_MS: "0", + LEARNINGS_NO_PATCH: "1", + GITHUB_OUTPUT: outputFile, + }, + }) + + assert.ok(!fs.existsSync(`${fixturePath}.patched`), "LEARNINGS_NO_PATCH must suppress marker PATCH") + if (fs.existsSync(outputFile)) { + const ghOut = fs.readFileSync(outputFile, "utf8") + assert.ok( + !ghOut.includes("learned_through="), + "GITHUB_OUTPUT must not contain learned_through on LEARNINGS_NO_PATCH", + ) + } + assert.ok( + result.stdout.includes("marker PATCH suppressed"), + "stdout must log marker suppression for LEARNINGS_NO_PATCH", + ) + assert.ok( + result.stdout.includes("would have written marker"), + "stdout must log the suppressed marker for LEARNINGS_NO_PATCH", + ) + } + + // DRY_RUN=true with non-empty delta (suppresses learned_through output) + { + const cwd = setupLearnRepo(dir) + const tipSource = `commit:${gitIn(dir, ["rev-parse", "HEAD"]).slice(0, 7)}` + const fixturePath = writeFixture(cwd, { + pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } }, + comments: [], + }) + + const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog: path.join(cwd, "kilo-calls.log") }) + writeExtractionDelta(cwd, { + add: [ + { + id: "dry-suppress", + rule: "A rule suppressed under dry run.", + scope: "both", + source: tipSource, + date: "2026-08-03", + }, + ], + remove: [], + }) + + const outputFile = path.join(cwd, "gh-output-q-dry-nonempty") + const result = runNodeScript(LEARN_SCRIPT, { + cwd, + kiloDir, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_FIXTURE: fixturePath, + LEARNINGS_BUDGET_MINUTES: "1", + DOCS_SYNC_BACKOFF_MS: "0", + DRY_RUN: "true", + GITHUB_OUTPUT: outputFile, + }, + }) + + if (fs.existsSync(outputFile)) { + const ghOut = fs.readFileSync(outputFile, "utf8") + assert.ok( + !ghOut.includes("learned_through="), + "GITHUB_OUTPUT must not contain learned_through on DRY_RUN non-empty delta", + ) + } + assert.ok( + result.stdout.includes("learned-through output suppressed"), + "stdout must log learned-through output suppression for DRY_RUN non-empty delta", + ) + } + + // LEARNINGS_NO_PATCH=1 with non-empty delta + { + const cwd = setupLearnRepo(dir) + const tipSource = `commit:${gitIn(dir, ["rev-parse", "HEAD"]).slice(0, 7)}` + const fixturePath = writeFixture(cwd, { + pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } }, + comments: [], + }) + + const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog: path.join(cwd, "kilo-calls.log") }) + writeExtractionDelta(cwd, { + add: [ + { + id: "nopatch-suppress", + rule: "A rule suppressed under no-patch.", + scope: "both", + source: tipSource, + date: "2026-08-03", + }, + ], + remove: [], + }) + + const outputFile = path.join(cwd, "gh-output-q-nopatch-nonempty") + const result = runNodeScript(LEARN_SCRIPT, { + cwd, + kiloDir, + env: { + TRIAGE_MODEL: "test/model", + DOCS_SYNC_FIXTURE: fixturePath, + LEARNINGS_BUDGET_MINUTES: "1", + DOCS_SYNC_BACKOFF_MS: "0", + LEARNINGS_NO_PATCH: "1", + GITHUB_OUTPUT: outputFile, + }, + }) + + if (fs.existsSync(outputFile)) { + const ghOut = fs.readFileSync(outputFile, "utf8") + assert.ok( + !ghOut.includes("learned_through="), + "GITHUB_OUTPUT must not contain learned_through on LEARNINGS_NO_PATCH non-empty delta", + ) + } + assert.ok( + result.stdout.includes("learned-through output suppressed"), + "stdout must log learned-through output suppression for LEARNINGS_NO_PATCH non-empty delta", + ) + } + } + + // 10s — a failed API call must not disable the existing learnings + // The learn step is continue-on-error, and triage and edit read only the two prompt + // artifacts. So learn.mjs must write them before the first call that can throw. + { + console.log(" 10s — prompt artifacts survive an API failure") + const dir = mktemp("docs-sync-learn-s-") + const learningsPath = path.join(dir, "packages", "kilo-docs", "LEARNINGS.md") + fs.mkdirSync(path.dirname(learningsPath), { recursive: true }) + const seeded = [ + { + id: "seeded-rule", + rule: "Do not document features behind experimental flags.", + scope: "both", + source: "commit:aaaaaaa", + date: "2026-08-01", + }, + ] + fs.writeFileSync(learningsPath, renderLearnings(seeded)) + + // No DOCS_SYNC_FIXTURE and an empty GITHUB_REPOSITORY: repo() throws inside + // extract(). It stands for any API failure before the artifacts exist. + const failEnv = { + TRIAGE_MODEL: "test/model", + GITHUB_REPOSITORY: "", + GITHUB_OUTPUT: path.join(dir, "gh-output-s"), + GITHUB_STEP_SUMMARY: path.join(dir, "gh-summary-s"), + DOCS_SYNC_BACKOFF_MS: "0", + } + const result = runNodeScript(LEARN_SCRIPT, { cwd: dir, env: failEnv }) + assert.notEqual(result.status, 0, "extraction must fail without GITHUB_REPOSITORY") + + const triagePath = path.join(dir, "docs-sync-out", "learnings-triage.md") + const editPath = path.join(dir, "docs-sync-out", "learnings-edit.md") + for (const f of [triagePath, editPath]) { + assert.ok(fs.existsSync(f), `${path.basename(f)} must survive the failure`) + assert.ok( + fs.readFileSync(f, "utf8").includes("Do not document features behind experimental flags."), + `${path.basename(f)} must carry the checked-out rule`, + ) + } + + // An empty file must clear the stale block, not leave the earlier rule in place. + fs.writeFileSync(learningsPath, renderLearnings([])) + runNodeScript(LEARN_SCRIPT, { cwd: dir, env: failEnv }) + assert.ok(!fs.existsSync(triagePath), "an empty learnings file must remove learnings-triage.md") + assert.ok(!fs.existsSync(editPath), "an empty learnings file must remove learnings-edit.md") + } + + // 10t — the direct marker PATCH must not overwrite a concurrent body edit + // The body read at step 1 predates the extraction call, so learn.mjs must re-read + // the body immediately before the PATCH. + { + console.log(" 10t — marker PATCH preserves a concurrent body edit") + const dir = mktemp("docs-sync-learn-t-") + initRepoWithIdentity(dir) + fs.writeFileSync(path.join(dir, "base.txt"), "base\n") + gitIn(dir, ["add", "base.txt"]) + gitIn(dir, ["commit", "-m", "base"]) + gitIn(dir, ["checkout", "-b", "docs/auto-sync"]) + + // github-actions[bot] authored the only branch commit, so there is no candidate + // correction and no model call. The run goes straight to the direct marker PATCH. + const learningsPath = path.join(dir, "packages", "kilo-docs", "LEARNINGS.md") + fs.mkdirSync(path.dirname(learningsPath), { recursive: true }) + fs.writeFileSync(learningsPath, renderLearnings([])) + gitIn(dir, ["add", "packages/kilo-docs/LEARNINGS.md"]) + gitIn(dir, ["commit", "-m", "seed learnings", "--author", `github-actions[bot] <${githubBotEmail}>`]) + gitIn(dir, ["remote", "add", "origin", dir]) // learn.mjs fetches origin itself + const cwd = setupLearnRepo(dir) + const tip = gitIn(dir, ["rev-parse", "HEAD"]) + + // Stub GitHub API. The second read of the pull request returns the maintainer edit. + const serverDir = mktemp("docs-sync-api-t-") + const portFile = path.join(serverDir, "port") + const patchFile = path.join(serverDir, "patch.json") + const serverScript = path.join(serverDir, "server.cjs") + fs.writeFileSync( + serverScript, + `const fs = require("node:fs") +const http = require("node:http") +let reads = 0 +const json = (res, data) => { + res.writeHead(200, { "content-type": "application/json" }) + res.end(JSON.stringify(data)) +} +const server = http.createServer((req, res) => { + let raw = "" + req.on("data", (c) => (raw += c)) + req.on("end", () => { + if (req.method === "PATCH") return fs.writeFileSync(process.env.PATCH_FILE, raw), json(res, {}) + if (req.url.startsWith("/search/issues")) return json(res, { items: [{ number: 1 }] }) + if (req.url.includes("/comments")) return json(res, []) + if (req.url.includes("/pulls/1")) { + const body = reads++ === 0 ? process.env.BODY_BEFORE : process.env.BODY_AFTER + return json(res, { + number: 1, + body, + head: { ref: "docs/auto-sync" }, + user: { login: "github-actions[bot]" }, + }) + } + json(res, {}) + }) +}) +server.listen(0, "127.0.0.1", () => fs.writeFileSync(process.env.PORT_FILE, String(server.address().port))) +`, + ) + + const bodyBefore = "Rolling PR body.\n\n" + const humanEdit = "A maintainer edited the body while extraction ran." + const child = spawn(process.execPath, [serverScript], { + stdio: "ignore", + env: { + ...process.env, + PORT_FILE: portFile, + PATCH_FILE: patchFile, + BODY_BEFORE: bodyBefore, + BODY_AFTER: bodyBefore + humanEdit + "\n", + }, + }) + + try { + let port = "" + for (let i = 0; i < 100 && !port; i++) { + if (fs.existsSync(portFile)) port = fs.readFileSync(portFile, "utf8").trim() + else sleepSync(50) + } + assert.ok(port, "the stub API server must report a port") + + const result = runNodeScript(LEARN_SCRIPT, { + cwd, + env: { + TRIAGE_MODEL: "test/model", + GITHUB_REPOSITORY: "acme/repo", + GH_TOKEN: "stub-token", + DOCS_SYNC_API_BASE: `http://127.0.0.1:${port}`, + GITHUB_OUTPUT: path.join(dir, "gh-output-t"), + GITHUB_STEP_SUMMARY: path.join(dir, "gh-summary-t"), + LEARNINGS_BUDGET_MINUTES: "1", + DOCS_SYNC_BACKOFF_MS: "0", + }, + }) + assert.equal(result.status, 0, `learn.mjs must succeed against the stub API: ${result.output}`) + + assert.ok(fs.existsSync(patchFile), "the run must PATCH the pull request body") + const patchedBody = JSON.parse(fs.readFileSync(patchFile, "utf8")).body + assert.ok(patchedBody.includes(humanEdit), "the concurrent body edit must survive the marker PATCH") + assert.ok(patchedBody.includes(tip), "the PATCH must carry the new tip SHA") + assert.ok(!patchedBody.includes("commit=old"), "the old marker must be replaced") + assert.equal( + (patchedBody.match(/\nmore" + const parsed = parseLearnedThrough(body) + assert.equal(parsed.commit, "abc1234") + assert.equal(parsed.comment, "2026-08-03T12:00:00Z") + + // none values → null + const noneBody = "" + const noneParsed = parseLearnedThrough(noneBody) + assert.equal(noneParsed.commit, null) + assert.equal(noneParsed.comment, null) + + // absent → both null + const absent = parseLearnedThrough("no marker") + assert.equal(absent.commit, null) + assert.equal(absent.comment, null) + } + + // renderLearnings deterministic order + { + console.log(" 10 — renderLearnings deterministic order") + const entries = [ + { id: "b", rule: "B rule.", scope: "both", source: "commit:bb", date: "2026-08-02" }, + { id: "a", rule: "A rule.", scope: "both", source: "commit:aa", date: "2026-08-01" }, + { id: "c", rule: "C rule.", scope: "both", source: "commit:cc", date: "2026-08-01" }, + ] + const r1 = renderLearnings(entries) + const r2 = renderLearnings(entries) + assert.equal(r1, r2, "renderLearnings must be deterministic") + // Order: date ascending, then id ascending. So a before b before c (a.date < c.date, both before b) + const aPos = r1.indexOf("A rule") + const cPos = r1.indexOf("C rule") + const bPos = r1.indexOf("B rule") + assert.ok(aPos < cPos, "a (earlier date) must come before c") + assert.ok(cPos < bPos, "c (same date as a but later id) must come before b (later date)") + } +} + +// Case 11 — the created rolling PR gets an assignee and a review request +function case11_prOwner() { + console.log("case 11 — created PR assignee and reviewer") + const src = fs.readFileSync(path.join(HERE, "upsert-pr.mjs"), "utf8") + + assert.ok(/const DOCS_OWNER = "\S+"/.test(src), "DOCS_OWNER must be a module constant") + assert.ok(src.includes("/assignees`, {"), "created PR must POST assignees") + assert.ok(src.includes("/requested_reviewers`, {"), "created PR must POST requested_reviewers") + + // Both calls belong to the create arm, after the PR exists. + const createIdx = src.indexOf("const pr = await api(`/repos/${repo()}/pulls`") + assert.ok(createIdx >= 0, "create-PR call must exist") + assert.ok(src.indexOf("/assignees`, {") > createIdx, "assignees POST must follow PR creation") + assert.ok(src.indexOf("/requested_reviewers`, {") > createIdx, "reviewer POST must follow PR creation") + + // A failure here must not fail the run — the PR is already open. + const ownerIdx = src.indexOf("/assignees`, {") + const tryIdx = src.lastIndexOf("try {", ownerIdx) + const catchIdx = src.indexOf("} catch", ownerIdx) + assert.ok(tryIdx >= 0 && catchIdx > src.indexOf("/requested_reviewers`, {"), "both POSTs must sit in one try/catch") +} + +// --------------------------------------------------------------------------- +// main +// --------------------------------------------------------------------------- +function main() { + const cases = [ + case1_mergeOrFallback, + case2_defectB, + case2b_autoFlag, + case2c_stderrLogAlways, + case2d_redactEnvSecrets, + case2e_prefixSecretOrdering, + case2f_redactStdout, + case2g_redactStream, + case2h_pendingCauseIsReadable, + case2i_budgetsFitWork, + case3_watermark, + case4_routing, + case5_recollection, + case6_budgets, + case7_cap, + case8_triage, + case9_reverts, + case10_learnings, + case11_prOwner, + ] + let failed = 0 + for (const fn of cases) { + try { + fn() + console.log(` ok: ${fn.name}`) + } catch (err) { + failed++ + console.error(` FAIL: ${fn.name}`) + console.error(err) + } finally { + cleanup() + } + } + if (failed > 0) { + console.error(`\nselftest: ${failed} case(s) failed`) + process.exit(1) + } + console.log("\nselftest: all cases passed") +} + +main() diff --git a/.github/docs-sync/triage-prompt.md b/.github/docs-sync/triage-prompt.md new file mode 100644 index 00000000000..dbf2341e70f --- /dev/null +++ b/.github/docs-sync/triage-prompt.md @@ -0,0 +1,20 @@ +You are the triage pass of an automated documentation pipeline for Kilo Code. Kilo Code is an open-source agentic engineering platform: VS Code extension, JetBrains plugin, CLI, and the kilo.ai cloud platform (teams, KiloClaw, gateway, code reviews). + +The attached `digest.json` file contains PRs recently merged to Kilo-Org/cloud and Kilo-Org/kilocode. Your only job is to decide which of them require changes to the public product documentation at kilo.ai/docs. + +A PR is docs-worthy ONLY if a user of Kilo Code would need to learn something new or change how they use the product after this PR ships. Examples: new commands, flags, settings, UI workflows, providers, pricing/limits changes, breaking behavior changes, or fixes that change documented behavior. + +A PR is NOT docs-worthy when it is: an internal refactor, infrastructure or CI work, a feature-flag scaffold that is not yet user-visible, test or dependency work, a bug fix that merely restores already-documented behavior, a change only visible to contributors or self-hosters, an internal technical fix or plumbing change with no user-visible behavior or workflow change (for example stream-liveness/watchdog plumbing, internal retry or timeout handling with no user-facing setting), or UI or rendering polish that does not change what a user must do or learn. + +Rules: + +- Include every input PR exactly once, identified by its `number` and `url`. Never invent PRs. +- A digest entry with a `reverted_by` field had its changes reverted by that later PR in the same window. Treat such PRs as not docs-worthy unless the entry's body or file list shows clear evidence the change was re-landed after the revert. When `reverted_by` drives your verdict, the `reason` must cite it, e.g. "reverted by https://github.com/Kilo-Org/kilocode/pull/12497". +- When unsure, set `docs_worthy` to false and explain the doubt in `reason`. +- `target_sections` is only filled for docs-worthy PRs. Use rough docs areas, e.g. `getting-started`, `code-with-ai/platforms/cli`, `code-with-ai/platforms/vscode`, `code-with-ai/agents`, `ai-providers`, `teams`, `enterprise`, `automate`. +- `reason` is one short sentence, written for the human who reviews the final docs PR. +- `priority` reflects user impact: high = most users affected, medium = notable subset, low = edge case. + +Respond with a STRICT JSON array and nothing else: no prose, no markdown fences, no comments. Schema: + +[{"pr": 123, "url": "https://github.com/Kilo-Org/kilocode/pull/123", "docs_worthy": true, "reason": "Adds --variant flag to kilo run", "target_sections": ["code-with-ai/platforms/cli"], "priority": "high"}] diff --git a/.github/docs-sync/triage.mjs b/.github/docs-sync/triage.mjs new file mode 100644 index 00000000000..eb680846510 --- /dev/null +++ b/.github/docs-sync/triage.mjs @@ -0,0 +1,198 @@ +// kilocode_change - new file + +/** + * Runs the LLM triage pass over docs-sync-out/digest.json in chunks. + * + * A daily window holds ~30-50 PRs; a replay can hold several hundred. A + * single triage call over that volume truncates its JSON output, so the + * digest is split into chunks of CHUNK_SIZE and each chunk is triaged with + * its own `kilo run` call. A chunk that fails, is only partially classified, + * or is deferred by the wall-clock budget is marked pending:true (still + * docs_worthy:false so filter-worthy excludes it) so the watermark holds + * back and the next run re-collects those PRs. + * + * Env: TRIAGE_MODEL (provider/model), KILO_API_KEY + KILO_ORG_ID (gateway auth, set by + * the workflow; the kilo provider reads them natively). Reads the prompt from triage-prompt.md next to this script. + * Budget: TRIAGE_BUDGET_MINUTES (default 35). Test hook: DOCS_SYNC_BACKOFF_MS. + */ + +import fs from "node:fs" +import path from "node:path" +import { fileURLToPath } from "node:url" +import { parseTriageEntries } from "./extract-json.mjs" +import { appendSummary, backoffMsForAttempt, deadline, remainingMs, runKilo, sleepSync } from "./lib.mjs" +import { readLearningsBlock } from "./learn.mjs" + +const CHUNK_SIZE = 25 +const ATTEMPTS = 3 +const OUT_DIR = "docs-sync-out" +const CHUNK_TIMEOUT_MS = 10 * 60 * 1000 + +const HERE = path.dirname(fileURLToPath(import.meta.url)) +const prompt = fs.readFileSync(path.join(HERE, "triage-prompt.md"), "utf8") + readLearningsBlock("triage") +const model = process.env.TRIAGE_MODEL +if (!model) throw new Error("TRIAGE_MODEL is required") + +const TRIAGE_BUDGET_MINUTES = Number(process.env.TRIAGE_BUDGET_MINUTES) || 35 + +const digest = JSON.parse(fs.readFileSync(`${OUT_DIR}/digest.json`, "utf8")) + +function formatCause(result) { + const bits = [] + if (result.timedOut) bits.push("timed out") + if (result.exitCode !== null && result.exitCode !== undefined) bits.push(`exit ${result.exitCode}`) + if (result.stderrTail) bits.push(result.stderrTail.replaceAll("\n", " ").slice(0, 200)) + return bits.join("; ") || "no diagnostic" +} + +function pendingEntry(d, reason) { + return { + pr: d.number, + url: d.url, + docs_worthy: false, + pending: true, + reason, + target_sections: [], + priority: "medium", + } +} + +function triageChunk(chunk, index, budgetDeadline) { + const chunkFile = `${OUT_DIR}/triage-chunk-${index}.json` + fs.writeFileSync(chunkFile, JSON.stringify(chunk, null, 2)) + + let lastCause = "triage failed to classify this PR" + for (let attempt = 1; attempt <= ATTEMPTS; attempt++) { + const left = remainingMs(budgetDeadline) + if (left < CHUNK_TIMEOUT_MS) { + lastCause = `triage budget exhausted before chunk ${index} attempt ${attempt}` + console.warn( + `chunk ${index}: stopping retries — remaining budget cannot fit another ${CHUNK_TIMEOUT_MS / 60000}m attempt`, + ) + break + } + + // Headless `kilo run` auto-rejects every permission ask; without --auto the + // agent cannot run shell commands. SECURITY: --auto grants unrestricted bash + // to an agent steered by external PR content. Hardening deferred: a scoped + // permission.bash map via KILO_CONFIG_CONTENT should replace --auto once the + // required shell patterns are stable (see PR #12605 review thread). + const result = runKilo({ + args: ["run", "--auto", prompt, "-m", model, "--dir", process.cwd(), "-f", chunkFile], + timeoutMs: Math.min(CHUNK_TIMEOUT_MS, left), + streamStdout: false, + label: `triage chunk ${index} attempt ${attempt}`, + }) + + const raw = result.stdout + if (raw) fs.writeFileSync(`${OUT_DIR}/triage-raw-${index}.txt`, raw) + + const entries = raw ? parseTriageEntries(raw) : null + if (entries) { + // An entry for a PR outside this chunk must not win the shared dedupe + // against the chunk that actually owns it — drop foreign entries. + const allowed = new Set(chunk.map((d) => d.url)) + const owned = entries.filter((e) => allowed.has(e.url)) + if (owned.length !== entries.length) { + console.warn(`chunk ${index}: dropped ${entries.length - owned.length} entries for PRs outside the chunk`) + } + if (owned.length > 0) return owned + } + + // Exit 0 is not success: unparseable output is a failure logged WITH + // the captured stderrTail and exit code on every attempt. + const cause = formatCause(result) + lastCause = `triage chunk ${index}: ${cause}` + console.warn( + `chunk ${index} attempt ${attempt}: no valid JSON in output` + + ` (exit ${result.exitCode}${result.timedOut ? ", timed out" : ""})` + + (result.stderrTail ? `\nstderr tail:\n${result.stderrTail}` : "\nstderr tail: (empty)"), + ) + + if (attempt < ATTEMPTS) { + const wait = backoffMsForAttempt(attempt) + const afterWait = remainingMs(budgetDeadline) - wait + if (wait > 0 && afterWait >= CHUNK_TIMEOUT_MS) { + console.warn(`chunk ${index}: backing off ${wait / 1000}s before attempt ${attempt + 1}`) + sleepSync(wait) + } else if (wait > 0) { + console.warn(`chunk ${index}: skipping backoff — remaining budget cannot fit attempt ${attempt + 1} after wait`) + } + } + } + + console.warn( + `::warning::chunk ${index} failed triage after up to ${ATTEMPTS} attempts; marking ${chunk.length} PRs pending`, + ) + return chunk.map((d) => + pendingEntry( + d, + lastCause.includes("triage failed") ? lastCause : `triage failed to classify this PR (${lastCause})`, + ), + ) +} + +const chunks = [] +for (let i = 0; i < digest.length; i += CHUNK_SIZE) { + chunks.push(digest.slice(i, i + CHUNK_SIZE)) +} +console.log(`triaging ${digest.length} PRs in ${chunks.length} chunks of up to ${CHUNK_SIZE}`) + +const budgetDeadline = deadline(TRIAGE_BUDGET_MINUTES) +const merged = [] +const seen = new Set() + +for (let i = 0; i < chunks.length; i++) { + const left = remainingMs(budgetDeadline) + if (left < CHUNK_TIMEOUT_MS) { + const deferredPrs = chunks.slice(i).reduce((n, c) => n + c.length, 0) + console.warn( + `stopping triage before chunk ${i}: remaining budget (${Math.ceil(left / 1000)}s) cannot fit a ${CHUNK_TIMEOUT_MS / 60000}m chunk; deferring ${deferredPrs} PRs`, + ) + const cause = `triage budget exhausted before chunk ${i} (${Math.ceil(left / 1000)}s left)` + for (let j = i; j < chunks.length; j++) { + for (const d of chunks[j]) { + if (seen.has(d.url)) continue + seen.add(d.url) + merged.push(pendingEntry(d, cause)) + } + } + break + } + + for (const e of triageChunk(chunks[i], i, budgetDeadline)) { + if (seen.has(e.url)) continue + seen.add(e.url) + merged.push(e) + } +} + +// Coverage: every digest PR gets a triage entry. Partial-chunk backfill and +// any other missing URL are pending:true — not a genuine "not worthy" verdict. +for (const d of digest) { + if (seen.has(d.url)) continue + merged.push(pendingEntry(d, "not classified by triage")) +} + +fs.writeFileSync(`${OUT_DIR}/triage.json`, JSON.stringify(merged, null, 2)) +const worthy = merged.filter((e) => e.docs_worthy).length +const pending = merged.filter((e) => e.pending === true) +console.log(`triage complete: ${merged.length} entries, ${worthy} docs-worthy, ${pending.length} pending`) + +// Upsert is gated off when worthy == 0, so triage emits its own Step Summary +// listing every PR it marked pending:true and why. +if (pending.length > 0) { + const lines = pending.map((e) => `- [${e.url}] ${e.reason}`) + appendSummary( + `### docs-sync: triage pending (will retry)\n\n${pending.length} PR(s) were not classified and will be re-collected on the next run:\n\n${lines.join("\n")}`, + ) +} + +// Replay warning (S2j): warn IFF since-override AND something pending AND +// docs-worthy count is 0 (Upsert is gated off, so noDiffReport never runs). +const sinceOverride = process.env.SINCE_OVERRIDE === "true" +if (sinceOverride && pending.length > 0 && worthy === 0) { + console.warn( + "::warning::docs-sync since-override replay left uncovered PRs and wrote no PR body (worthy=0); re-run the override — the watermark was not held back in the body", + ) +} diff --git a/.github/docs-sync/upsert-pr.mjs b/.github/docs-sync/upsert-pr.mjs new file mode 100644 index 00000000000..d4b7d2a21fc --- /dev/null +++ b/.github/docs-sync/upsert-pr.mjs @@ -0,0 +1,550 @@ +// kilocode_change - new file + +/** + * Commits the agent's packages/kilo-docs changes, pushes the rolling branch, + * and creates or updates the rolling auto-docs PR. + * + * No-op when the agent produced no docs changes. PRs become drafts when the + * diff exceeds the file cap or verification failed. The PR body carries + * marker-delimited sections so later runs can append rows, plus a + * machine-readable processed-through watermark. + * + * Watermark invariant: processed-through never moves past a PR that has no + * terminal outcome. Terminal := action !== "pending" (a deliberate agent + * "skipped" IS terminal). Uncovered PRs hold the marker at earliest + * merged_at − 1 ms so collect's merged:>=since re-collects them next run. + * Three review rounds found four independent defects in a queue-based + * alternative (unreachable gate, empty-PR creation, cap-overflow loss, + * draft-state corruption); a held-back watermark has none of those modes. + */ + +import { execFileSync } from "node:child_process" +import fs from "node:fs" +import { pathToFileURL } from "node:url" + +const BRANCH = process.env.BRANCH || "docs/auto-sync" +const FILE_CAP = 15 +const ROW_CAP = 150 +const PENDING_DISPLAY_CAP = 60 +const SUMMARY_FILE = ".docs-sync-summary.json" +// Owner of the rolling docs PR: assigned and asked for review on creation. +const DOCS_OWNER = "emilieschario" +const DOCS_PATH = "packages/kilo-docs" +export const LEARNINGS_FILE = "packages/kilo-docs/LEARNINGS.md" + +const git = (args) => + execFileSync("git", args, { stdio: ["ignore", "pipe", "inherit"] }) + .toString() + .trim() + +// Agent-generated strings land in the PR body next to machine-read markers. +// Strip HTML-comment sequences so a crafted/adversarial value cannot forge +// section boundaries or the processed-through watermark. +function clean(value) { + return String(value ?? "") + .replaceAll("", "") +} + +function shortRef(url) { + return clean(url).replace("https://github.com/", "").replace("/pull/", "#") +} + +function changeRow(e) { + return `| ${clean(e.action).replaceAll("|", "\\|")} | [${shortRef(e.url)}](${clean(e.url)}) |` +} + +function skippedRow(e) { + const reason = clean(e.reason).replaceAll("|", "\\|").replaceAll("\n", " ") + return `| [${shortRef(e.url)}](${clean(e.url)}) | ${reason} |` +} + +function pendingRow(e) { + const reason = clean(e.reason ?? e.cause ?? "") + .replaceAll("|", "\\|") + .replaceAll("\n", " ") + return `| [${shortRef(e.url)}](${clean(e.url)}) | ${reason} |` +} + +export function extractSectionRows(body, name) { + const m = String(body ?? "").match( + new RegExp(`([\\s\\S]*?)`), + ) + if (!m) return [] + return m[1] + .split("\n") + .map((l) => l.trim()) + .filter( + (l) => + l.startsWith("|") && + !l.startsWith("| ---") && + !/^\|\s*Docs change/.test(l) && + !/^\|\s*PR\s*\|/.test(l) && + !/^\|\s*Why\s*\|/.test(l), + ) +} + +function section(name, header, rows) { + const body = rows.length > 0 ? [header, "| --- | --- |", ...rows].join("\n") : "_None._" + return `\n${body}\n` +} + +export function renderBody({ + date, + since, + through, + learnedThrough = "", + changesRows, + pendingRows, + skippedRows, + verified, + draftReasons, + note, +}) { + const pendingDisplay = + pendingRows.length > PENDING_DISPLAY_CAP + ? [...pendingRows.slice(0, PENDING_DISPLAY_CAP), `| +${pendingRows.length - PENDING_DISPLAY_CAP} more | |`] + : pendingRows + + return `## Automated docs sync — ${date} + +This PR keeps kilo.ai/docs in sync with features merged to [Kilo-Org/cloud](https://github.com/Kilo-Org/cloud) and [Kilo-Org/kilocode](https://github.com/Kilo-Org/kilocode). Every change below links to the merged PR it documents. + +- Window: \`${since}\` → \`${through}\` +- Verification (docs build + tests): **${verified ? "passing" : "FAILING — needs a human look"}** +${note ? `- ${note}\n` : ""}${draftReasons.length > 0 ? `- Draft because: ${draftReasons.join("; ")}\n` : ""} +### Changes + +${section("changes", "| Docs change | Source |", changesRows)} + +### Pending — will retry + +${section("pending", "| PR | Why |", pendingDisplay)} + +### Considered, no docs change needed + +${section("skipped", "| PR | Reason |", skippedRows)} + +--- + +(bot) Generated by the docs-sync workflow. Humans review and merge; while this PR stays open, the next daily run appends new changes here. Branch: \`${BRANCH}\`. + +${learnedThrough ? learnedThrough + "\n" : ""}` +} + +function mergeRows(oldRows, newRows) { + const seen = new Set() + const out = [] + for (const row of [...oldRows, ...newRows]) { + if (seen.has(row)) continue + seen.add(row) + out.push(row) + } + return out.slice(-ROW_CAP) +} + +function readJson(path, fallback) { + try { + return JSON.parse(fs.readFileSync(path, "utf8")) + } catch { + return fallback + } +} + +/** + * Uncovered = (worthy URLs with no summary row) ∪ (summary action "pending") + * ∪ (triage entries with pending: true). A worthy PR is covered iff it has a + * summary row whose action !== "pending" and carries no triage pending flag. + */ +export function computeUncovered({ worthy, summary, triage }) { + const worthyList = Array.isArray(worthy) ? worthy : [] + const summaryList = Array.isArray(summary) ? summary : [] + const triageList = Array.isArray(triage) ? triage : [] + + const summaryByUrl = new Map() + for (const e of summaryList) { + if (e?.url) summaryByUrl.set(e.url, e) + } + + const triagePendingByUrl = new Map() + for (const e of triageList) { + if (e?.url && e.pending === true) triagePendingByUrl.set(e.url, e) + } + + /** @type {Map} */ + const out = new Map() + + for (const w of worthyList) { + const url = w?.url + if (!url) continue + const row = summaryByUrl.get(url) + if (!row) { + out.set(url, { + url, + pr: w.number ?? w.pr, + reason: "no edit summary row (edit pass did not cover this PR)", + }) + continue + } + if (row.action === "pending") { + out.set(url, { + url, + pr: row.pr ?? w.number ?? w.pr, + reason: row.reason || "edit pass pending", + }) + } + } + + // Summary pending rows for URLs not in worthy (defensive). + for (const row of summaryList) { + if (row?.action === "pending" && row.url && !out.has(row.url)) { + out.set(row.url, { + url: row.url, + pr: row.pr, + reason: row.reason || "edit pass pending", + }) + } + } + + for (const [url, e] of triagePendingByUrl) { + if (out.has(url)) continue + out.set(url, { + url, + pr: e.pr, + reason: e.reason || "triage pending", + }) + } + + return [...out.values()] +} + +/** + * processed-through = now when uncovered is empty; otherwise earliest + * merged_at among uncovered PRs minus 1 ms (from digest-full.json). + * When uncovered is non-empty but no merged_at resolves, hold at + * `fallback` (the run's window start / SINCE): every uncovered PR was + * collected via merged:>=since, so holding there re-collects all of them. + * Never advance past unresolved uncovered PRs (Defect-B permanent-loss). + */ +export function computeProcessedThrough({ uncovered, digest, now, fallback }) { + const nowIso = typeof now === "string" ? now : new Date(now).toISOString() + if (!uncovered || uncovered.length === 0) return nowIso + + const digestList = Array.isArray(digest) ? digest : [] + const byUrl = new Map(digestList.filter((d) => d?.url).map((d) => [d.url, d])) + + let earliest = null + for (const u of uncovered) { + const d = byUrl.get(u.url) + const mergedAt = d?.merged_at + if (!mergedAt) continue + const t = Date.parse(mergedAt) + if (!Number.isFinite(t)) continue + if (earliest === null || t < earliest) earliest = t + } + + if (earliest === null) { + // digest-full missing/corrupt while uncovered is non-empty: hold at + // window start so collect's merged:>=since re-collects every PR. + // Never use now−1ms — that strands uncovered PRs permanently. + const fallbackMs = fallback == null ? NaN : Date.parse(fallback) + if (!Number.isFinite(fallbackMs)) { + throw new Error( + `docs-sync: cannot resolve merged_at for ${uncovered.length} uncovered PR(s) and fallback/SINCE is missing or unparseable; refusing to advance processed-through`, + ) + } + const fallbackIso = new Date(fallbackMs).toISOString() + console.warn( + `::warning::docs-sync: merge times for ${uncovered.length} uncovered PR(s) could not be resolved; holding watermark at window start ${fallbackIso}`, + ) + return fallbackIso + } + + return new Date(earliest - 1).toISOString() +} + +/** + * Content gate: legitimate bot edits are docs pages and nav files. Only + * those are built and tested during verify (content-integrity.test.ts + * walks pages/ only), so anything else in the docs package forces human + * review. LEARNINGS.md is a root-level .md file, like the three sibling + * .md files already at that level, so it is not built or tested and is + * safe to exclude from the gate. + */ +export function nonContentFiles(changedFiles) { + return (Array.isArray(changedFiles) ? changedFiles : []).filter( + (f) => + f !== LEARNINGS_FILE && + !f.startsWith("packages/kilo-docs/pages/") && + !f.startsWith("packages/kilo-docs/lib/nav/"), + ) +} + +/** + * Route summary + triage into the three body sections. + * changesRows = action neither skipped nor pending + * pendingRows = uncovered from computeUncovered + * skippedRows = action === "skipped" ∪ triage docs_worthy false && !pending + */ +export function routeRows({ summary, triage, uncovered }) { + const summaryList = Array.isArray(summary) ? summary : [] + const triageList = Array.isArray(triage) ? triage : [] + const uncoveredList = Array.isArray(uncovered) ? uncovered : [] + + const changesEntries = summaryList.filter((e) => e.action !== "skipped" && e.action !== "pending") + const skippedEntries = [ + ...triageList.filter((e) => e.docs_worthy === false && e.pending !== true), + ...summaryList.filter((e) => e.action === "skipped"), + ] + + return { + changesRows: changesEntries.map(changeRow), + pendingRows: uncoveredList.map(pendingRow), + skippedRows: skippedEntries.map(skippedRow), + } +} + +/** + * Drop pre-existing Considered rows whose reason contains any of the three + * legacy failure literals (substring match — live rows carry longer strings). + * Genuine no-doc-needed rows are untouched. + */ +export function dropLegacySkipped(rows) { + const list = Array.isArray(rows) ? rows : [] + const needles = ["edit pass failed or timed out", "triage failed to classify", "not classified by triage"] + return list.filter((row) => { + const s = String(row ?? "") + return !needles.some((n) => s.includes(n)) + }) +} + +/** + * Resolve the learned-through marker for renderBody. + * + * Order: env LEARNED_THROUGH when set and non-empty; else the marker + * parsed out of the existing PR body; else "". + * The fallback is load-bearing: a run where extraction was skipped, + * failed, or already PATCHed the marker itself must not clobber a good + * marker. + */ +export function resolveLearnedThrough({ envValue, prBody }) { + const fromEnv = String(envValue ?? "").trim() + if (fromEnv) return fromEnv + const m = String(prBody ?? "").match(//) + return m ? m[0] : "" +} + +/** + * No-diff early-return report. Returns summary markdown and an optional + * replay warning. Warns IFF sinceOverride && uncovered non-empty (no commit + * happened — that is the caller's situation). + */ +export function noDiffReport({ uncovered, sinceOverride }) { + const list = Array.isArray(uncovered) ? uncovered : [] + const lines = + list.length === 0 + ? ["The agent found nothing worth documenting in this window."] + : [ + `No packages/kilo-docs diff was produced, but ${list.length} PR(s) remain uncovered and will be re-collected on the next scheduled run:`, + "", + ...list.map((u) => `- [${u.url}] ${u.reason || "uncovered"}`), + ] + + const summary = `### docs-sync: no docs changes\n\n${lines.join("\n")}` + + let warning = null + if (sinceOverride && list.length > 0) { + warning = + "docs-sync since-override replay left uncovered PRs and wrote no PR body (no docs commit); re-run the override — the watermark was not held back in the body" + } + + return { summary, warning } +} + +async function main() { + const { api, appendOutput, appendSummary, repo } = await import("./lib.mjs") + + const now = process.env.PROCESSED_THROUGH ?? new Date().toISOString() + const since = process.env.SINCE ?? "unknown" + const sinceOverride = process.env.SINCE_OVERRIDE === "true" + const mode = ["update", "conflict"].includes(process.env.PREP_MODE) ? process.env.PREP_MODE : "fresh" + const existingPr = process.env.PR_NUMBER || "" + const verified = process.env.VERIFIED === "true" + const date = now.slice(0, 10) + + // The agent's run summary is consumed here and never committed. + const agentSummary = readJson(SUMMARY_FILE, []) + fs.rmSync(SUMMARY_FILE, { force: true }) + const triage = readJson("docs-sync-out/triage.json", []) + const worthy = readJson("docs-sync-out/worthy.json", []) + const digest = readJson("docs-sync-out/digest-full.json", []) + + // Order matters: compute uncovered BEFORE the no-diff early return so + // noDiffReport can name every held-back PR. + const uncovered = computeUncovered({ worthy, summary: agentSummary, triage }) + + if (git(["status", "--porcelain", "--", DOCS_PATH]) === "") { + console.log("no packages/kilo-docs changes produced; nothing to commit") + const { summary, warning } = noDiffReport({ uncovered, sinceOverride }) + appendSummary(summary) + if (warning) console.warn(`::warning::${warning}`) + return + } + + // Git identity is configured once in docs-sync.yml (Configure git identity) + // before any commit-creating step, including prepare-branch's merge. + git(["add", DOCS_PATH]) + git(["commit", "-m", `docs: sync with merged PRs (${date})`]) + + // Watermark: now when fully covered; else earliest uncovered merged_at − 1ms. + // Pass SINCE as fallback so missing digest-full cannot strand uncovered PRs. + const through = computeProcessedThrough({ uncovered, digest, now, fallback: since }) + + // The draft cap bounds the cumulative PR diff, not just this run's commit. + const changedFiles = git(["diff", "--name-only", "origin/main...HEAD", "--", DOCS_PATH]).split("\n").filter(Boolean) + const draftReasons = [] + if (changedFiles.length > FILE_CAP) draftReasons.push(`diff exceeds ${FILE_CAP} files (${changedFiles.length})`) + if (!verified) draftReasons.push("docs build/tests not passing") + // Content gate: legitimate bot edits are docs pages and nav files. Anything + // else in the docs package (build config, components, tests) executes + // during the verify build, so force human review before merge. + const nonContent = nonContentFiles(changedFiles) + if (nonContent.length > 0) { + // File paths are agent-chosen; sanitize before they land in the PR body. + const listed = nonContent + .slice(0, 5) + .map((f) => clean(f).replaceAll("|", "\\|")) + .join(", ") + draftReasons.push(`touches non-content files outside pages/ and lib/nav/: ${listed}`) + } + const draft = draftReasons.length > 0 + + git( + mode === "update" + ? ["push", "origin", `HEAD:${BRANCH}`] + : ["push", "--force-with-lease", "origin", `HEAD:${BRANCH}`], + ) + + const { + changesRows: changesNew, + pendingRows: pendingNew, + skippedRows: skippedNew, + } = routeRows({ + summary: agentSummary, + triage, + uncovered, + }) + + let oldChanges = [] + let oldSkipped = [] + let oldPending = [] + let prBody = "" + if (mode === "update" && existingPr) { + const pr = await api(`/repos/${repo()}/pulls/${existingPr}`) + prBody = pr.body ?? "" + oldChanges = extractSectionRows(pr.body, "changes") + oldSkipped = dropLegacySkipped(extractSectionRows(pr.body, "skipped")) + oldPending = extractSectionRows(pr.body, "pending") + } + + // Pending is replaced each run (informational only); do not merge legacy + // pending rows — uncovered is recomputed fresh. oldPending is read only so + // extractSectionRows stays exercised; discarded deliberately. + void oldPending + + const learnedThrough = resolveLearnedThrough({ envValue: process.env.LEARNED_THROUGH, prBody }) + + const body = renderBody({ + date, + since, + through, + learnedThrough, + changesRows: mergeRows(oldChanges, changesNew), + pendingRows: pendingNew, + skippedRows: mergeRows(oldSkipped, skippedNew), + verified, + draftReasons, + note: + mode === "conflict" && existingPr + ? `Continues from #${existingPr}, whose branch conflicted with \`main\` (its commits are preserved there).` + : "", + }) + + try { + await api(`/repos/${repo()}/labels`, { + method: "POST", + body: { name: "auto-docs", color: "1d76db", description: "Automated docs-sync PRs" }, + }) + } catch (err) { + if (err.status !== 422) throw err // 422 = label already exists + } + + let prNumber + let prUrl + if (mode === "update" && existingPr) { + const pr = await api(`/repos/${repo()}/pulls/${existingPr}`, { + method: "PATCH", + body: { title: `docs: auto-sync with merged PRs (through ${date})`, body }, + }) + prNumber = pr.number + prUrl = pr.html_url + await api(`/repos/${repo()}/issues/${prNumber}/comments`, { + method: "POST", + body: { + body: `(bot) Appended changes processed through \`${through}\`. Verification: **${verified ? "passing" : "failing"}**.${draft ? ` Draft because: ${draftReasons.join("; ")}.` : ""}`, + }, + }) + } else { + const pr = await api(`/repos/${repo()}/pulls`, { + method: "POST", + body: { + title: `docs: auto-sync with merged PRs (through ${date})`, + head: BRANCH, + base: "main", + body, + draft, + }, + }) + prNumber = pr.number + prUrl = pr.html_url + await api(`/repos/${repo()}/issues/${prNumber}/labels`, { method: "POST", body: { labels: ["auto-docs"] } }) + // Best effort: the PR already exists here, so a non-collaborator or a + // revoked account must not fail the run. + try { + await api(`/repos/${repo()}/issues/${prNumber}/assignees`, { + method: "POST", + body: { assignees: [DOCS_OWNER] }, + }) + await api(`/repos/${repo()}/pulls/${prNumber}/requested_reviewers`, { + method: "POST", + body: { reviewers: [DOCS_OWNER] }, + }) + } catch (err) { + console.warn(`::warning::docs-sync: could not assign or request review from ${DOCS_OWNER}: ${err.message}`) + } + if (mode === "conflict" && existingPr) { + await api(`/repos/${repo()}/issues/${existingPr}/comments`, { + method: "POST", + body: { + body: `(bot) This branch conflicted with \`main\`, so the sync continues in ${prUrl}. Commits on this branch are preserved — please close this PR after the new one is reviewed.`, + }, + }) + } + } + + appendOutput("pr_url", prUrl) + appendSummary( + `### docs-sync PR\n\n- ${prUrl}\n- changed files: ${changedFiles.length}\n- draft: ${draft}\n- uncovered: ${uncovered.length}\n- processed-through: ${through}\n`, + ) + console.log( + `PR ${prNumber}: ${prUrl} (draft=${draft}, files=${changedFiles.length}, uncovered=${uncovered.length}, through=${through})`, + ) +} + +const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href +if (isMain) { + main().catch((err) => { + console.error(err) + process.exit(1) + }) +} diff --git a/.github/docs-sync/watermark.mjs b/.github/docs-sync/watermark.mjs new file mode 100644 index 00000000000..32986fef67f --- /dev/null +++ b/.github/docs-sync/watermark.mjs @@ -0,0 +1,107 @@ +// kilocode_change - new file + +/** + * Resolves the docs-sync watermark: the timestamp of the newest source PR the + * bot has already processed. Derived from the bot's own PRs (marker in the PR + * body), so there is no external state to keep consistent. + * + * Priority: workflow_dispatch input `since` > latest open bot PR marker > + * last merged bot PR marker > 72h ago. Hard cap: never look back more than + * 14 days — unless the human explicitly requested a window via INPUT_SINCE. + */ + +import { pathToFileURL } from "node:url" +import { appendOutput, appendSummary, repo, searchIssues } from "./lib.mjs" + +const FALLBACK_HOURS = 72 +const CAP_DAYS = 14 +const MARKER = // + +function extractMarker(body) { + const m = (body ?? "").match(MARKER) + if (!m) return null + const d = new Date(m[1]) + return Number.isNaN(d.getTime()) ? null : d +} + +async function findWatermark() { + const r = repo() + for (const state of ["open", "merged"]) { + const query = `repo:${r} is:pr label:auto-docs sort:created-desc ${state === "open" ? "is:open" : "is:merged"}` + const prs = await searchIssues(query, { maxPages: 1 }) + for (const pr of prs) { + // Only trust markers on PRs authored by the bot itself: bodies are + // editable and the label can be applied by anyone with triage access. + if (pr.user?.login !== "github-actions[bot]") continue + const marker = extractMarker(pr.body) + if (marker) { + console.log(`watermark from ${state} PR #${pr.number}: ${marker.toISOString()}`) + return marker + } + } + } + return null +} + +/** + * Apply the 14-day lookback cap. When `explicit` is true (dispatch override), + * the cap is skipped so a human-requested recovery window is not silently + * shortened. Returns `{ since, clamped }`. + */ +export function applyCap(since, now, { explicit = false } = {}) { + if (explicit) { + console.log(`14-day cap skipped: INPUT_SINCE was set explicitly (${since.toISOString()})`) + return { since, clamped: false } + } + const cap = new Date(now.getTime() - CAP_DAYS * 24 * 3600 * 1000) + if (since < cap) { + const from = since.toISOString() + const to = cap.toISOString() + console.warn( + `::warning::docs-sync watermark clamped from ${from} to ${to} (${CAP_DAYS}-day cap). ` + + `Anything still uncovered before ${to} is abandoned and needs a human.`, + ) + return { since: cap, clamped: true } + } + return { since, clamped: false } +} + +async function main() { + const now = new Date() + let since + let explicit = false + + const input = (process.env.INPUT_SINCE ?? "").trim() + if (input) { + since = new Date(input) + if (Number.isNaN(since.getTime())) { + throw new Error(`Invalid INPUT_SINCE: ${input}`) + } + explicit = true + console.log(`watermark from dispatch input: ${since.toISOString()}`) + } else { + since = (await findWatermark()) ?? new Date(now.getTime() - FALLBACK_HOURS * 3600 * 1000) + } + + // A forged, edited, or malformed marker in the future would silently match + // nothing in the merged:>= search; clamp it loudly. + if (since > now) { + console.warn(`watermark ${since.toISOString()} is in the future, clamping to now`) + since = now + } + + ;({ since } = applyCap(since, now, { explicit })) + + appendOutput("since", since.toISOString()) + appendOutput("now", now.toISOString()) + appendOutput("since_override", explicit ? "true" : "false") + appendSummary(`### docs-sync watermark\n\n- since: \`${since.toISOString()}\`\n- now: \`${now.toISOString()}\`\n`) +} + +const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href +if (isMain) { + main().catch((err) => { + console.error(err) + process.exit(1) + }) +} diff --git a/.github/workflows/auto-docs.yml b/.github/workflows/auto-docs.yml index 5f84be47f38..2e7db5814cc 100644 --- a/.github/workflows/auto-docs.yml +++ b/.github/workflows/auto-docs.yml @@ -26,15 +26,18 @@ concurrency: jobs: call-valtown: + # kilocode_change start if: > github.event_name == 'workflow_dispatch' || ( + github.repository == 'Kilo-Org/kilocode' && github.event.pull_request.merged == true && ( startsWith(github.event.pull_request.title, 'feat:') || startsWith(github.event.pull_request.title, 'feat(') ) ) + # kilocode_change end runs-on: ubuntu-latest timeout-minutes: 5 diff --git a/.github/workflows/check-opencode-annotations.yml b/.github/workflows/check-opencode-annotations.yml index f494bbb4c8e..51d40efb260 100644 --- a/.github/workflows/check-opencode-annotations.yml +++ b/.github/workflows/check-opencode-annotations.yml @@ -41,6 +41,9 @@ jobs: - name: Check Effect Promise facade allowlist run: bun run script/check-opencode-promise-facades.ts + - name: Check domain architecture boundaries and ratchets + run: bun run script/check-architecture.ts + - name: Check model tool network boundary run: bun run script/check-model-tool-network.ts diff --git a/.github/workflows/check-org-member.yml b/.github/workflows/check-org-member.yml index e059f8bb0fa..b0fcaf5f7a9 100644 --- a/.github/workflows/check-org-member.yml +++ b/.github/workflows/check-org-member.yml @@ -18,7 +18,7 @@ on: jobs: check: - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: blacksmith-2vcpu-ubuntu-2404 # kilocode_change outputs: is-member: ${{ steps.check.outputs['is-member'] }} steps: diff --git a/.github/workflows/codeql-kotlin.yml b/.github/workflows/codeql-kotlin.yml index 5f0cec56c87..8ac284fcecf 100644 --- a/.github/workflows/codeql-kotlin.yml +++ b/.github/workflows/codeql-kotlin.yml @@ -19,6 +19,7 @@ on: jobs: analyze: name: Analyze (java-kotlin) + if: github.event_name == 'workflow_dispatch' || github.repository == 'Kilo-Org/kilocode' runs-on: ubuntu-latest permissions: security-events: write @@ -39,6 +40,12 @@ jobs: distribution: temurin java-version: "21" + - name: Free disk space for CodeQL + shell: bash + run: | + sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /usr/local/share/boost + docker system prune --all --force || true + - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 86b7a69a053..af3851afcbe 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,6 +24,7 @@ on: jobs: analyze: name: Analyze (${{ matrix.language }}) + if: github.event_name == 'workflow_dispatch' || github.repository == 'Kilo-Org/kilocode' # Runner size impacts CodeQL analysis time. To learn more, please see: # - https://gh.io/recommended-hardware-resources-for-running-codeql # - https://gh.io/supported-runners-and-hardware-resources diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml index d8b3485035b..8d3ca8877b6 100644 --- a/.github/workflows/containers.yml +++ b/.github/workflows/containers.yml @@ -16,6 +16,7 @@ permissions: jobs: build: + if: github.event_name == 'workflow_dispatch' || github.repository == 'Kilo-Org/kilocode' # kilocode_change runs-on: blacksmith-4vcpu-ubuntu-2404 steps: # kilocode_change start diff --git a/.github/workflows/disabled/compliance-close.yml.disabled b/.github/workflows/disabled/compliance-close.yml.disabled index 14e68701e57..a83824e5cf6 100644 --- a/.github/workflows/disabled/compliance-close.yml.disabled +++ b/.github/workflows/disabled/compliance-close.yml.disabled @@ -34,10 +34,48 @@ jobs: const now = Date.now(); const twoHours = 2 * 60 * 60 * 1000; + const orgMemberAssociations = new Set(['OWNER', 'MEMBER']); + const agentLogin = 'opencode-agent[bot]'; + const { data: file } = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: '.github/TEAM_MEMBERS', + ref: 'dev', + }); + const teamMembers = new Set( + Buffer.from(file.content, 'base64') + .toString() + .split('\n') + .map((line) => line.trim().toLowerCase()) + .filter(Boolean) + ); + + function isExempt(item) { + const login = item.user?.login?.toLowerCase(); + return ( + login === agentLogin || + orgMemberAssociations.has(item.author_association) || + (login && teamMembers.has(login)) + ); + } for (const item of items) { const isPR = !!item.pull_request; const kind = isPR ? 'PR' : 'issue'; + const login = item.user?.login; + + if (isExempt(item)) { + core.info(`Skipping ${kind} #${item.number}; author ${login || 'unknown'} is exempt`); + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: item.number, + name: 'needs:compliance', + }); + } catch (e) {} + continue; + } const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, diff --git a/.github/workflows/disabled/duplicate-issues.yml.disabled b/.github/workflows/disabled/duplicate-issues.yml.disabled index ea2a67cb782..f0e68202481 100644 --- a/.github/workflows/disabled/duplicate-issues.yml.disabled +++ b/.github/workflows/disabled/duplicate-issues.yml.disabled @@ -17,12 +17,31 @@ jobs: with: fetch-depth: 1 + - name: Check exempt issue author + id: author + run: | + LOGIN="${{ github.event.issue.user.login }}" + ASSOCIATION="${{ github.event.issue.author_association }}" + + if [ "$LOGIN" = "opencode-agent[bot]" ] || + [ "$ASSOCIATION" = "OWNER" ] || + [ "$ASSOCIATION" = "MEMBER" ] || + grep -qxiF "$LOGIN" .github/TEAM_MEMBERS; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "Skipping issue automation for exempt author: $LOGIN ($ASSOCIATION)" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + - uses: ./.github/actions/setup-bun + if: steps.author.outputs.skip != 'true' - name: Install opencode + if: steps.author.outputs.skip != 'true' run: curl -fsSL https://kilo.ai/install | bash - name: Check duplicates and compliance + if: steps.author.outputs.skip != 'true' env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -38,6 +57,7 @@ jobs: opencode run -m opencode/claude-sonnet-4-6 "A new issue has been created: Issue number: ${{ github.event.issue.number }} + Issue author association: ${{ github.event.issue.author_association }} Lookup this issue with gh issue view ${{ github.event.issue.number }}. @@ -49,6 +69,8 @@ jobs: Check whether the issue follows our contributing guidelines and issue templates. + If the issue author association is OWNER or MEMBER, skip this compliance check. Do not add the needs:compliance label for organization-owned issues. + This project has three issue templates that every issue MUST use one of: 1. Bug Report - requires a Description field with real content @@ -83,7 +105,7 @@ jobs: Based on your findings, post a SINGLE comment on issue #${{ github.event.issue.number }}. Build the comment as follows: - If the issue is NOT compliant, start the comment with: + If the issue is NOT compliant and the author association is not OWNER or MEMBER, start the comment with: Then explain what needs to be fixed and that they have 2 hours to edit the issue before it is automatically closed. Also add the label needs:compliance to the issue using: gh issue edit ${{ github.event.issue.number }} --add-label needs:compliance @@ -118,7 +140,7 @@ jobs: Remember: post at most ONE comment combining all findings. If everything is fine, post nothing." recheck-compliance: - if: github.event.action == 'edited' && contains(github.event.issue.labels.*.name, 'needs:compliance') + if: github.event.action == 'edited' && contains(github.event.issue.labels.*.name, 'needs:compliance') # kilocode_change runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: contents: read @@ -129,12 +151,31 @@ jobs: with: fetch-depth: 1 + - name: Check exempt issue author + id: author + run: | + LOGIN="${{ github.event.issue.user.login }}" + ASSOCIATION="${{ github.event.issue.author_association }}" + + if [ "$LOGIN" = "opencode-agent[bot]" ] || + [ "$ASSOCIATION" = "OWNER" ] || + [ "$ASSOCIATION" = "MEMBER" ] || + grep -qxiF "$LOGIN" .github/TEAM_MEMBERS; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "Skipping issue automation for exempt author: $LOGIN ($ASSOCIATION)" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + - uses: ./.github/actions/setup-bun + if: steps.author.outputs.skip != 'true' - name: Install opencode + if: steps.author.outputs.skip != 'true' run: curl -fsSL https://kilo.ai/install | bash - name: Recheck compliance + if: steps.author.outputs.skip != 'true' env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -148,9 +189,12 @@ jobs: } run: | opencode run -m opencode/claude-sonnet-4-6 "Issue #${{ github.event.issue.number }} was previously flagged as non-compliant and has been edited. + Issue author association: ${{ github.event.issue.author_association }} Lookup this issue with gh issue view ${{ github.event.issue.number }}. + If the issue author association is OWNER or MEMBER, remove the needs:compliance label if present, delete the previous compliance comment if present, and do not post a new comment. + Re-check whether the issue now follows our contributing guidelines and issue templates. This project has three issue templates that every issue MUST use one of: diff --git a/.github/workflows/disabled/storybook.yml.disabled b/.github/workflows/disabled/storybook.yml.disabled index 1e652104d69..be2e099d0ed 100644 --- a/.github/workflows/disabled/storybook.yml.disabled +++ b/.github/workflows/disabled/storybook.yml.disabled @@ -9,6 +9,7 @@ on: - "bun.lock" - "packages/storybook/**" - "packages/ui/**" + - "packages/session-ui/**" pull_request: branches: [dev] paths: @@ -17,6 +18,7 @@ on: - "bun.lock" - "packages/storybook/**" - "packages/ui/**" + - "packages/session-ui/**" workflow_dispatch: concurrency: diff --git a/.github/workflows/disabled/triage.yml.disabled b/.github/workflows/disabled/triage.yml.disabled index 0b2c9d89a1c..a1dfc0840b3 100644 --- a/.github/workflows/disabled/triage.yml.disabled +++ b/.github/workflows/disabled/triage.yml.disabled @@ -16,13 +16,32 @@ jobs: with: fetch-depth: 1 + - name: Check exempt issue author + id: author + run: | + LOGIN="${{ github.event.issue.user.login }}" + ASSOCIATION="${{ github.event.issue.author_association }}" + + if [ "$LOGIN" = "opencode-agent[bot]" ] || + [ "$ASSOCIATION" = "OWNER" ] || + [ "$ASSOCIATION" = "MEMBER" ] || + grep -qxiF "$LOGIN" .github/TEAM_MEMBERS; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "Skipping issue automation for exempt author: $LOGIN ($ASSOCIATION)" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + - name: Setup Bun + if: steps.author.outputs.skip != 'true' uses: ./.github/actions/setup-bun - name: Install opencode + if: steps.author.outputs.skip != 'true' run: curl -fsSL https://kilo.ai/install | bash - name: Triage issue + if: steps.author.outputs.skip != 'true' env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 316fa748384..cf25f784707 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -13,7 +13,7 @@ on: jobs: build: name: Build docs site - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: blacksmith-4vcpu-ubuntu-2404 # kilocode_change steps: - name: Checkout repository uses: actions/checkout@v6 # kilocode_change diff --git a/.github/workflows/docs-sync.yml b/.github/workflows/docs-sync.yml new file mode 100644 index 00000000000..1ae78f3edb4 --- /dev/null +++ b/.github/workflows/docs-sync.yml @@ -0,0 +1,252 @@ +# kilocode_change - new file +name: docs-sync + +# Daily bot: collects PRs merged to Kilo-Org/cloud and Kilo-Org/kilocode, +# triages them for docs relevance, runs Kilo CLI headless to update +# packages/kilo-docs, and maintains one rolling PR for human review. +# +# Security posture: scheduled/manual runs check out the dispatched ref and may +# push/comment with write permissions and org secrets. PR runs (paths-limited to +# this workflow and .github/docs-sync/**) execute branch code only in a +# read-only, secretless `selftest` job that never pushes, comments, or calls an +# LLM. `pull_request` (not `pull_request_target`) keeps fork tokens read-only. +# State is derived from the bot's own PRs (watermark marker in the PR body), so +# missed or failed runs self-heal on the next run. + +on: + schedule: + - cron: "0 7 * * *" # 07:00 UTC daily + workflow_dispatch: + inputs: + since: + description: "Override watermark (ISO date, e.g. 2026-07-20). Default: last processed-through marker, 72h fallback, 14d cap." + required: false + type: string + dry_run: + description: "Collect + triage only, no edits, no PR" + type: boolean + default: false + pull_request: + paths: + - ".github/docs-sync/**" + - ".github/workflows/docs-sync.yml" + +permissions: + contents: write # push the rolling branch, create the auto-docs label + pull-requests: write # create/update the rolling PR + issues: write # comment on the rolling PR + +concurrency: + group: ${{ github.event_name == 'pull_request' && format('docs-sync-pr-{0}', github.event.pull_request.number) || 'docs-sync' }} + cancel-in-progress: false + +env: + TRIAGE_MODEL: ${{ vars.DOCS_SYNC_TRIAGE_MODEL || 'kilo/moonshotai/kimi-k3' }} + EDIT_MODEL: ${{ vars.DOCS_SYNC_EDIT_MODEL || 'kilo/moonshotai/kimi-k3' }} + +jobs: + selftest: + if: github.repository == 'Kilo-Org/kilocode' + runs-on: blacksmith-4vcpu-ubuntu-2404 + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: "24" + package-manager-cache: false + + - name: Run docs-sync selftest + run: node .github/docs-sync/selftest.mjs + + sync: + if: github.repository == 'Kilo-Org/kilocode' && github.event_name != 'pull_request' + runs-on: blacksmith-4vcpu-ubuntu-2404 + # Budget: 4 setup/collect + 10 learn + 90 triage + 120 edit + 2 verify + 10 fix + 2 upsert = 238 min, 12-minute reserve. + # These are ceilings, not costs: a caught-up run triages ~2 chunks and edits + # ~1 batch and finishes in ~25 min. The old 35/50 pair was the binding + # constraint on backlog drain — run 30306629290 deferred 54 PRs untriaged and + # 31 unedited purely on budget, with no attempt made. See the throughput note + # in the PR description for the arithmetic. + timeout-minutes: 250 + env: + # Both are required: without KILO_ORG_ID the gateway bills the key + # owner's personal balance (402 "Add credits") instead of the org. + KILO_API_KEY: ${{ secrets.KILO_API_KEY }} + KILO_ORG_ID: ${{ secrets.KILO_ORG_ID }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 # prepare-branch merges main into the rolling branch + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: "24" + package-manager-cache: false + + - name: Run docs-sync selftest + run: node .github/docs-sync/selftest.mjs + + - name: Install Kilo CLI + run: | + npm install -g @kilocode/cli + kilo --version + + - name: Resolve watermark + id: wm + env: + GH_TOKEN: ${{ github.token }} + INPUT_SINCE: ${{ inputs.since }} + run: node .github/docs-sync/watermark.mjs + + - name: Learn from maintainer corrections + id: learn + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + LEARNINGS_BUDGET_MINUTES: "10" + DRY_RUN: ${{ inputs.dry_run }} + run: node .github/docs-sync/learn.mjs + + - name: Collect merged PRs + id: collect + env: + GH_TOKEN: ${{ github.token }} + run: node .github/docs-sync/collect.mjs --since "${{ steps.wm.outputs.since }}" + + - name: Triage merged PRs (LLM, chunked) + id: triage + if: steps.collect.outputs.count != '0' + env: + SINCE_OVERRIDE: ${{ steps.wm.outputs.since_override }} + # Default 35 fit only 8 of 11 chunks on a 254-PR window. Headroom for + # --auto making chunks slower now that the agent really runs commands. + TRIAGE_BUDGET_MINUTES: "90" + run: node .github/docs-sync/triage.mjs + + - name: Filter docs-worthy PRs + id: worthy + if: steps.collect.outputs.count != '0' + run: | + node .github/docs-sync/filter-worthy.mjs \ + docs-sync-out/digest-full.json docs-sync-out/triage.json docs-sync-out/worthy.json + count=$(node -p "require('./docs-sync-out/worthy.json').length") + echo "count=$count" >> "$GITHUB_OUTPUT" + if [ "$count" = "0" ]; then + echo "No docs-worthy PRs in this window; skipping edit/verify/PR." + fi + + - name: Setup Bun + if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true + uses: ./.github/actions/setup-bun + + - name: Prepare rolling branch + id: prep + if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true + env: + GH_TOKEN: ${{ github.token }} + run: node .github/docs-sync/prepare-branch.mjs + + # After prepare-branch checks out the rolling branch and merges main, the + # worktree holds main's scripts. Restore the dispatched ref's copies so a + # branch-dispatch AC9 run actually exercises the fixed code. git restore + # (not checkout) leaves them unstaged so upsert-pr's bare commit won't + # include them in the docs PR. + - name: Restore docs-sync scripts from the dispatched ref + if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true + run: git restore --source=${{ github.sha }} -- .github/docs-sync + + - name: Update docs (Kilo CLI, batched) + if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true + continue-on-error: true + env: + # Default 50 fit only 4 of 11 batches. A healthy --auto batch is ~8 min, + # and edit.mjs will not start a batch without EDIT_BATCH_TIMEOUT_MINUTES + # (15) left, so 120 covers 14 batches = 70 PRs against ~5 worthy/day. + EDIT_BUDGET_MINUTES: "120" + run: node .github/docs-sync/edit.mjs + + - name: Verify docs build and tests + id: verify + if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true + continue-on-error: true + env: + NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.POSTHOG_API_KEY }} + run: | + set -o pipefail + { bun run --filter @kilocode/kilo-docs build && bun run --filter @kilocode/kilo-docs test; } 2>&1 | tee docs-sync-out/verify.log + + - name: Fix verify failures (one pass) + id: fix + if: steps.verify.outcome == 'failure' + continue-on-error: true + timeout-minutes: 10 + env: + NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.POSTHOG_API_KEY }} + run: | + set -o pipefail + # Headless kilo run auto-rejects every permission ask; the runner has no + # user config granting bash, so without --auto the agent cannot run ordinary + # shell commands against the repository. + kilo run --auto "The docs build or tests failed. Read the attached docs-sync-out/verify.log and fix the packages/kilo-docs changes so they pass. Do not revert doc edits; fix them. Do not modify anything outside packages/kilo-docs." \ + -m "$EDIT_MODEL" --dir "$GITHUB_WORKSPACE" -f docs-sync-out/verify.log \ + | node .github/docs-sync/redact-stream.mjs \ + | tee -a docs-sync-out/edit-log.txt \ + || echo "::warning::kilo fix pass exited nonzero; re-verifying anyway" + # The rebuild below decides this step's outcome, not the agent's exit code. + # Without the guard above, `set -o pipefail` + the default `bash -e` would + # abort here once the CLI half of this PR ships: a mid-stream session error + # (or an auto-rejected ask) exits 1, verify2.log is never written, and + # `Re-verify status` reports VERIFIED=false even when the docs build fine. + { bun run --filter @kilocode/kilo-docs build && bun run --filter @kilocode/kilo-docs test; } 2>&1 | tee docs-sync-out/verify2.log + + - name: Re-verify status + id: verified + if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true + env: + VERIFY_OUTCOME: ${{ steps.verify.outcome }} + FIX_OUTCOME: ${{ steps.fix.outcome }} + run: | + if [ "$VERIFY_OUTCOME" = "success" ] || [ "$FIX_OUTCOME" = "success" ]; then + echo "ok=true" >> "$GITHUB_OUTPUT" + else + echo "ok=false" >> "$GITHUB_OUTPUT" + fi + + - name: Write the learnings file + if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true + run: node .github/docs-sync/learn.mjs --apply + + - name: Upsert rolling PR + if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true + env: + GH_TOKEN: ${{ github.token }} + LEARNED_THROUGH: ${{ steps.learn.outputs.learned_through }} + PROCESSED_THROUGH: ${{ steps.wm.outputs.now }} + SINCE: ${{ steps.wm.outputs.since }} + SINCE_OVERRIDE: ${{ steps.wm.outputs.since_override }} + BRANCH: ${{ steps.prep.outputs.branch }} + PREP_MODE: ${{ steps.prep.outputs.mode }} + PR_NUMBER: ${{ steps.prep.outputs.pr_number }} + VERIFIED: ${{ steps.verified.outputs.ok }} + run: node .github/docs-sync/upsert-pr.mjs + + - name: Upload run artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: docs-sync-out + path: docs-sync-out/ + retention-days: 14 + if-no-files-found: ignore diff --git a/.github/workflows/nix-hashes.yml b/.github/workflows/nix-hashes.yml index 8d9980049f1..701957c36e8 100644 --- a/.github/workflows/nix-hashes.yml +++ b/.github/workflows/nix-hashes.yml @@ -57,14 +57,24 @@ jobs: BUILD_LOG=$(mktemp) trap 'rm -f "$BUILD_LOG"' EXIT - # Build with fakeHash to trigger hash mismatch and reveal correct hash - nix build ".#packages.${SYSTEM}.node_modules_updater" --no-link 2>&1 | tee "$BUILD_LOG" || true + HASH="" + MAX_ATTEMPTS=3 + for ((ATTEMPT = 1; ATTEMPT <= MAX_ATTEMPTS; ATTEMPT++)); do + # Build with fakeHash to trigger hash mismatch and reveal correct hash + nix build ".#packages.${SYSTEM}.node_modules_updater" --no-link 2>&1 | tee "$BUILD_LOG" || true - # Extract hash from build log with portability - HASH="$(nix run --inputs-from . nixpkgs#gnugrep -- -oP 'got:\s*\Ksha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | tail -n1 || true)" + HASH="$(nix run --inputs-from . nixpkgs#gnugrep -- -oP 'got:\s*\Ksha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | tail -n1 || true)" + + [ -n "$HASH" ] && break + + if [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; then + echo "::warning::Attempt ${ATTEMPT}/${MAX_ATTEMPTS} produced no hash for ${SYSTEM}; retrying in $((ATTEMPT * 10))s" + sleep $((ATTEMPT * 10)) + fi + done if [ -z "$HASH" ]; then - echo "::error::Failed to compute hash for ${SYSTEM}" + echo "::error::Failed to compute hash for ${SYSTEM} after ${MAX_ATTEMPTS} attempts" cat "$BUILD_LOG" exit 1 fi diff --git a/.github/workflows/publish-jetbrains-bundled.yml b/.github/workflows/publish-jetbrains-bundled.yml new file mode 100644 index 00000000000..fa0046ad27a --- /dev/null +++ b/.github/workflows/publish-jetbrains-bundled.yml @@ -0,0 +1,330 @@ +# kilocode_change - new file +name: publish-jetbrains-bundled + +on: + workflow_dispatch: + inputs: + pr: + description: Merged JetBrains release PR number to bundle + required: true + type: string + merge_commit: + description: Merge commit SHA from the reviewed release PR + required: true + type: string + +concurrency: + group: publish-jetbrains-bundled-pr-${{ inputs.pr }} + cancel-in-progress: false + +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + validate: + if: github.repository == 'Kilo-Org/kilocode' + runs-on: blacksmith-4vcpu-ubuntu-2404 + permissions: + contents: read + pull-requests: read + outputs: + version: ${{ steps.release.outputs.version }} + kind: ${{ steps.release.outputs.kind }} + tag: ${{ steps.release.outputs.tag }} + channel: ${{ steps.release.outputs.marketplace_channel }} + steps: + - name: Checkout trusted validation scripts + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: main + + - name: Setup Bun for validation + uses: ./.github/actions/setup-bun + + - name: Checkout merged release PR for validation + uses: actions/checkout@v6 + with: + fetch-depth: 0 + path: release + persist-credentials: false + ref: ${{ inputs.merge_commit }} + + - name: Validate release PR and tag + id: release + working-directory: release + run: bun ../script/jetbrains-release-validate.ts --pr "$PR_NUMBER" + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + PR_NUMBER: ${{ inputs.pr }} + + bundle: + needs: validate + if: github.repository == 'Kilo-Org/kilocode' + runs-on: blacksmith-8vcpu-ubuntu-2404 + permissions: + actions: read + contents: write + outputs: + version: ${{ needs.validate.outputs.version }} + kind: ${{ needs.validate.outputs.kind }} + steps: + - name: Checkout merged release PR metadata + uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ inputs.merge_commit }} + + - name: Save reviewed release metadata + run: | + cp packages/kilo-jetbrains/CHANGELOG.md "$RUNNER_TEMP/jetbrains-CHANGELOG.md" + cp packages/kilo-jetbrains/gradle.properties "$RUNNER_TEMP/jetbrains-gradle.properties" + + - name: Checkout release tag + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ needs.validate.outputs.tag }} + + - name: Restore reviewed release metadata + run: | + cp "$RUNNER_TEMP/jetbrains-CHANGELOG.md" packages/kilo-jetbrains/CHANGELOG.md + cp "$RUNNER_TEMP/jetbrains-gradle.properties" packages/kilo-jetbrains/gradle.properties + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "24" + + - name: Setup Bun + uses: ./.github/actions/setup-bun + + - name: Install dependencies + run: bun install + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Validate signing secrets + run: | + missing=0 + for name in JETBRAINS_CERTIFICATE_CHAIN JETBRAINS_PRIVATE_KEY JETBRAINS_PRIVATE_KEY_PASSWORD; do + if [[ -z "${!name}" ]]; then + echo "Missing required secret: $name" >&2 + missing=1 + fi + done + exit "$missing" + env: + JETBRAINS_CERTIFICATE_CHAIN: ${{ secrets.JETBRAINS_CERTIFICATE_CHAIN }} + JETBRAINS_PRIVATE_KEY: ${{ secrets.JETBRAINS_PRIVATE_KEY }} + JETBRAINS_PRIVATE_KEY_PASSWORD: ${{ secrets.JETBRAINS_PRIVATE_KEY_PASSWORD }} + + - name: Write signing secrets to temp files + run: | + dir="$RUNNER_TEMP/jetbrains-signing" + mkdir -m 700 -p "$dir" + chain="$dir/certificate-chain.pem" + key="$dir/private-key.pem" + umask 077 + printf '%s' "$JETBRAINS_CERTIFICATE_CHAIN" > "$chain" + printf '%s' "$JETBRAINS_PRIVATE_KEY" > "$key" + chmod 600 "$chain" "$key" + echo "JETBRAINS_CERTIFICATE_CHAIN_FILE=$chain" >> "$GITHUB_ENV" + echo "JETBRAINS_PRIVATE_KEY_FILE=$key" >> "$GITHUB_ENV" + env: + JETBRAINS_CERTIFICATE_CHAIN: ${{ secrets.JETBRAINS_CERTIFICATE_CHAIN }} + JETBRAINS_PRIVATE_KEY: ${{ secrets.JETBRAINS_PRIVATE_KEY }} + + - name: Build signed bundled plugin + working-directory: packages/kilo-jetbrains + run: | + args=( + -Pproduction=true + -Pkilo.version="$VERSION" + -Pkilo.channel="$CHANNEL" + -Pkilo.cli.bundled=true + ) + ./gradlew clean buildPlugin signPlugin verifyPluginSignature verifyPlugin "${args[@]}" + env: + GH_TOKEN: ${{ github.token }} + GITHUB_TOKEN: ${{ github.token }} + VERSION: ${{ needs.validate.outputs.version }} + CHANNEL: ${{ needs.validate.outputs.channel }} + JETBRAINS_PRIVATE_KEY_PASSWORD: ${{ secrets.JETBRAINS_PRIVATE_KEY_PASSWORD }} + + - name: Remove signing secret temp files + if: always() + run: rm -rf "$RUNNER_TEMP/jetbrains-signing" + + - name: Resolve bundled archive + id: archive + run: | + mapfile -t signed < <(compgen -G "packages/kilo-jetbrains/build/distributions/*-signed.zip") + if [[ "${#signed[@]}" -ne 1 ]]; then + echo "Expected exactly one signed bundled JetBrains plugin ZIP, found ${#signed[@]}." >&2 + printf '%s\n' "${signed[@]}" >&2 + exit 1 + fi + asset="kilo-code-${VERSION}-bundled.zip" + dest="packages/kilo-jetbrains/build/release/$asset" + mkdir -p "$(dirname "$dest")" + cp "${signed[0]}" "$dest" + echo "asset=$asset" >> "$GITHUB_OUTPUT" + echo "path=$dest" >> "$GITHUB_OUTPUT" + env: + VERSION: ${{ needs.validate.outputs.version }} + + - name: Upload bundled ZIP to GitHub Release + run: gh release upload "$TAG" "$ARCHIVE" --clobber --repo "$GITHUB_REPOSITORY" + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ needs.validate.outputs.tag }} + ARCHIVE: ${{ steps.archive.outputs.path }} + + - name: Resolve bundled asset URL + id: asset + run: | + url="$(gh release view "$TAG" --json assets --jq '.assets[] | select(.name == env.ASSET) | .url' --repo "$GITHUB_REPOSITORY")" + if [[ -z "$url" ]]; then + echo "Could not resolve GitHub Release URL for $ASSET" >&2 + exit 1 + fi + echo "url=$url" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ needs.validate.outputs.tag }} + ASSET: ${{ steps.archive.outputs.asset }} + + - name: Generate stable plugin repository XML + if: needs.validate.outputs.kind == 'stable' + run: | + mkdir -p pages/jetbrains + python3 <<'PY' + import html + import io + import os + import zipfile + import xml.etree.ElementTree as ET + + archive = os.environ["ARCHIVE"] + asset = os.environ["ASSET_URL"] + version = os.environ["VERSION"] + + def plugin_xml(path): + with zipfile.ZipFile(path) as zip: + for name in zip.namelist(): + if name.endswith("META-INF/plugin.xml"): + return zip.read(name) + for name in zip.namelist(): + if not name.endswith(".jar"): + continue + with zipfile.ZipFile(io.BytesIO(zip.read(name))) as jar: + for item in jar.namelist(): + if item.endswith("META-INF/plugin.xml"): + return jar.read(item) + raise SystemExit("bundled plugin ZIP did not contain META-INF/plugin.xml") + + root = ET.fromstring(plugin_xml(archive)) + + def text(name, default=""): + item = root.find(name) + return item.text.strip() if item is not None and item.text else default + + def cdata(value): + return "", "]]]]>") + "]]>" + + plugin = text("id", "ai.kilocode.jetbrains") + name = text("name", "Kilo Code") + vendor = text("vendor", "Kilo Code") + desc = text("description") + notes = text("change-notes") + idea = root.find("idea-version") + attrs = "" + if idea is not None: + since = idea.attrib.get("since-build") + until = idea.attrib.get("until-build") + if since: + attrs += f' since-build="{html.escape(since)}"' + if until: + attrs += f' until-build="{html.escape(until)}"' + + xml = [ + '', + '', + f' ', + f' {html.escape(name)}', + f' {html.escape(vendor)}', + f' ', + ] + if desc: + xml.append(f' {cdata(desc)}') + if notes: + xml.append(f' {cdata(notes)}') + xml.extend([' ', '', '']) + with open("pages/jetbrains/updatePlugins.xml", "w", encoding="utf-8") as file: + file.write("\n".join(xml)) + PY + env: + ARCHIVE: ${{ steps.archive.outputs.path }} + ASSET_URL: ${{ steps.asset.outputs.url }} + VERSION: ${{ needs.validate.outputs.version }} + + - name: Upload stable Pages source + if: needs.validate.outputs.kind == 'stable' + uses: actions/upload-artifact@v4 + with: + name: jetbrains-pages-${{ needs.validate.outputs.version }} + path: pages + if-no-files-found: error + + - name: Upload workflow artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: kilo-jetbrains-bundled-${{ needs.validate.outputs.version }} + path: | + packages/kilo-jetbrains/build/release/*.zip + pages/jetbrains/updatePlugins.xml + if-no-files-found: ignore + + pages: + needs: bundle + if: needs.bundle.outputs.kind == 'stable' + runs-on: blacksmith-4vcpu-ubuntu-2404 + permissions: + actions: read + id-token: write + pages: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Download stable Pages source + uses: actions/download-artifact@v4 + with: + name: jetbrains-pages-${{ needs.bundle.outputs.version }} + path: pages + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v4 + with: + path: pages + + - name: Deploy Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/publish-jetbrains.yml b/.github/workflows/publish-jetbrains.yml index ffc83d070be..0de42df9ee6 100644 --- a/.github/workflows/publish-jetbrains.yml +++ b/.github/workflows/publish-jetbrains.yml @@ -7,12 +7,23 @@ on: - closed branches: - main + workflow_dispatch: + inputs: + pr: + description: Merged JetBrains release PR number to publish + required: true + type: string + merge_commit: + description: Merge commit SHA from the reviewed release PR + required: true + type: string concurrency: - group: publish-jetbrains-pr-${{ github.event.pull_request.number }} + group: publish-jetbrains-pr-${{ github.event.pull_request.number || inputs.pr }} cancel-in-progress: false permissions: + actions: write contents: write pull-requests: read @@ -23,17 +34,23 @@ jobs: publish: if: >- github.repository == 'Kilo-Org/kilocode' && - github.event.pull_request.merged == true && - startsWith(github.event.pull_request.head.ref, 'jetbrains/release/') && - contains(github.event.pull_request.labels.*.name, 'jetbrains-release') && - github.event.pull_request.head.repo.full_name == github.repository + ( + ( + github.event_name == 'pull_request' && + github.event.pull_request.merged == true && + startsWith(github.event.pull_request.head.ref, 'jetbrains/release/') && + contains(github.event.pull_request.labels.*.name, 'jetbrains-release') && + github.event.pull_request.head.repo.full_name == github.repository + ) || + github.event_name == 'workflow_dispatch' + ) runs-on: blacksmith-8vcpu-ubuntu-2404 steps: - name: Checkout merged release PR uses: actions/checkout@v6 with: fetch-depth: 0 - ref: ${{ github.event.pull_request.merge_commit_sha }} + ref: ${{ github.event.pull_request.merge_commit_sha || inputs.merge_commit }} - name: Setup Bun for validation uses: ./.github/actions/setup-bun @@ -44,7 +61,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} - PR_NUMBER: ${{ github.event.pull_request.number }} + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr }} - name: Save reviewed release metadata run: | @@ -82,11 +99,6 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - - name: Install build tools - run: | - sudo apt-get update - sudo apt-get install -y patchelf zip - - name: Validate publishing secrets run: | missing=0 @@ -103,19 +115,11 @@ jobs: JETBRAINS_PRIVATE_KEY: ${{ secrets.JETBRAINS_PRIVATE_KEY }} JETBRAINS_PRIVATE_KEY_PASSWORD: ${{ secrets.JETBRAINS_PRIVATE_KEY_PASSWORD }} - - name: Prepare CLI resources - working-directory: packages/kilo-jetbrains - run: bun script/build.ts --production --prepare-cli - env: - KILO_VERSION: ${{ steps.release.outputs.version }} - KILO_CHANNEL: ${{ steps.release.outputs.cli_channel }} - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - - name: Verify plugin working-directory: packages/kilo-jetbrains run: ./gradlew verifyPlugin -Pproduction=true -Pkilo.channel="$CHANNEL" env: + GH_TOKEN: ${{ github.token }} VERSION: ${{ steps.release.outputs.version }} CHANNEL: ${{ steps.release.outputs.marketplace_channel }} @@ -185,6 +189,19 @@ jobs: ARCHIVE: ${{ steps.archive.outputs.path }} NOTES: packages/kilo-jetbrains/build/release-notes.md + - name: Dispatch bundled GitHub release build + continue-on-error: true + run: | + gh workflow run publish-jetbrains-bundled.yml \ + --repo "$GITHUB_REPOSITORY" \ + --ref main \ + -f pr="$PR_NUMBER" \ + -f merge_commit="$MERGE_COMMIT" + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr }} + MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha || inputs.merge_commit }} + - name: Upload workflow artifact if: always() uses: actions/upload-artifact@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c2f0f56de15..9d8ad01d2fc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,7 +35,9 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inpu permissions: id-token: write contents: write + issues: write # kilocode_change - label automated JetBrains CLI pin bump PRs packages: write + pull-requests: write # kilocode_change - create automated JetBrains CLI pin bump PRs jobs: version: @@ -84,6 +86,16 @@ jobs: - uses: ./.github/actions/setup-bun + # kilocode_change start + - name: Setup Zig for Linux sandbox helpers + run: | + curl --fail --location --retry 3 \ + https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz \ + --output "$RUNNER_TEMP/zig.tar.xz" + echo "473ec26806133cf4d1918caf1a410f8403a13d979726a9045b421b685031a982 $RUNNER_TEMP/zig.tar.xz" | sha256sum --check --status + tar -xJf "$RUNNER_TEMP/zig.tar.xz" -C "$RUNNER_TEMP" + echo "$RUNNER_TEMP/zig-linux-x86_64-0.14.0" >> "$GITHUB_PATH" + # kilocode_change end - name: Build id: build run: | @@ -179,6 +191,18 @@ jobs: smoke_host() { binary="$1" "$binary" --version + helper="$(dirname "$binary")/bwrap" + if [[ "${{ matrix.target }}" == linux-* ]]; then + test -x "$helper" + grep -q '^SPDX-License-Identifier: LGPL-2.0-or-later$' "$(dirname "$binary")/licenses/bubblewrap/NOTICE" + "$helper" --version + # The live user-namespace bootstrap depends on the runner's kernel/AppArmor policy + # (GitHub-hosted Ubuntu 24.04 sets kernel.apparmor_restrict_unprivileged_userns=1), not on + # the shipped artifact. The runtime probe degrades gracefully, so keep this check non-fatal. + "$helper" --unshare-user --disable-userns --unshare-pid --die-with-parent --new-session \ + --ro-bind / / --dev /dev --proc /proc -- "$helper" --version \ + || echo "unprivileged user namespaces unavailable on this runner; skipping live sandbox check" + fi root="$(mktemp -d)" trap 'rm -rf "$root"' RETURN ( @@ -212,8 +236,10 @@ jobs: # as optional packages and must be installed for any Bun-compiled musl binary to run. apk add --no-cache libstdc++ libgcc # kilocode_change end - binary="/dist/$PACKAGE/bin/kilo" - "$binary" --version + binary="/dist/$PACKAGE/bin/kilo" # kilocode_change + "$binary" --version # kilocode_change + "/dist/$PACKAGE/bin/bwrap" --version # kilocode_change + grep -q '\''^SPDX-License-Identifier: LGPL-2.0-or-later$'\'' "/dist/$PACKAGE/bin/licenses/bubblewrap/NOTICE" # kilocode_change root="$(mktemp -d)" trap '\''rm -rf "$root"'\'' EXIT unset KILO_MODELS_PATH KILO_MODELS_URL KILO_CONFIG KILO_CONFIG_DIR @@ -232,7 +258,7 @@ jobs: name: Validate CLI (windows-${{ matrix.arch }}) needs: build-cli runs-on: ${{ matrix.runner }} - timeout-minutes: 20 + timeout-minutes: 20 # kilocode_change strategy: fail-fast: false matrix: diff --git a/.github/workflows/test-jetbrains.yml b/.github/workflows/test-jetbrains.yml index 8469b98776a..f138be40802 100644 --- a/.github/workflows/test-jetbrains.yml +++ b/.github/workflows/test-jetbrains.yml @@ -33,6 +33,9 @@ jobs: filters: | jetbrains: - '**' + - '!.changeset/**' + - '!.kilo/plans/**' + - '!packages/opencode/**' - '!packages/kilo-vscode/**' - '!packages/kilo-docs/**' @@ -41,8 +44,6 @@ jobs: needs: changes if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.jetbrains == 'true' runs-on: blacksmith-4vcpu-ubuntu-2404 - container: - image: ghcr.io/kilo-org/build/jetbrains:24.04 defaults: run: shell: bash @@ -55,12 +56,27 @@ jobs: - name: Mark workspace as git-safe run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - - name: Install dependencies - run: bun install + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version-file: package.json + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' }} - name: Run JetBrains unit tests run: bun script/test-ci.ts working-directory: packages/kilo-jetbrains + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Publish JetBrains unit reports if: always() diff --git a/.github/workflows/test-vscode.yml b/.github/workflows/test-vscode.yml index 60569585848..a19617ef324 100644 --- a/.github/workflows/test-vscode.yml +++ b/.github/workflows/test-vscode.yml @@ -21,7 +21,7 @@ on: jobs: unit: name: unit tests - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: blacksmith-4vcpu-ubuntu-2404 # kilocode_change defaults: run: shell: bash diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4b59bbb1208..cf54aa3081b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,14 +3,14 @@ name: test on: push: branches: - - main + - main # kilocode_change pull_request: workflow_dispatch: concurrency: - # Keep every run on main so cancelled checks do not pollute the default branch + # Keep every run on main so cancelled checks do not pollute the default branch # kilocode_change # commit history. PRs and other branches still share a group and cancel stale runs. - group: ${{ case(github.ref == 'refs/heads/main', format('{0}-{1}', github.workflow, github.run_id), format('{0}-{1}', github.workflow, github.event.pull_request.number || github.ref)) }} + group: ${{ case(github.ref == 'refs/heads/main', format('{0}-{1}', github.workflow, github.run_id), format('{0}-{1}', github.workflow, github.event.pull_request.number || github.ref)) }} # kilocode_change cancel-in-progress: true permissions: @@ -22,91 +22,273 @@ env: jobs: # kilocode_change start + changes: + name: detect general unit test changes + runs-on: blacksmith-4vcpu-ubuntu-2404 + permissions: + contents: read + pull-requests: read + outputs: + settings: ${{ steps.matrix.outputs.settings }} + general: ${{ steps.matrix.outputs.general }} + steps: + - name: Checkout repository + if: github.event_name != 'workflow_dispatch' + uses: actions/checkout@v6 + + - name: Detect general unit test changes + if: github.event_name != 'workflow_dispatch' + id: filter + uses: Kilo-Org/paths-filter@668c092af3649c4b664c54e4b704aa46782f6f7c # v3 + with: + predicate-quantifier: every + filters: | + general: + - '**' + - '!.changeset/**' + - '!.kilo/plans/**' + - '!packages/kilo-jetbrains/**' + - '!packages/kilo-vscode/**' + - '!packages/kilo-docs/**' + - '!packages/extensions/zed/**' + - '!specs/**' + - '!perf/**' + + - name: Configure unit matrix + id: matrix + env: + GENERAL: ${{ github.event_name != 'pull_request' || steps.filter.outputs.general == 'true' }} + run: | + if [ "$GENERAL" != "true" ]; then + echo 'general=false' >> "$GITHUB_OUTPUT" + echo 'settings=[{"os":"linux","index":1,"total":1,"host":"blacksmith-4vcpu-ubuntu-2404","run":false,"packages":false}]' >> "$GITHUB_OUTPUT" + exit 0 + fi + echo 'general=true' >> "$GITHUB_OUTPUT" + # kilocode_change - Windows runs 6 shards at KILO_TEST_CONCURRENCY=2. Measured: a + # 5-shard matrix was worse on both wall-clock (slowest job 9.3m vs 6.7m) and machine + # minutes (43 vs 37) — with 2 workers per shard, packing more work per shard loses. + echo 'settings=[{"os":"linux","index":1,"total":2,"host":"blacksmith-4vcpu-ubuntu-2404","run":true,"packages":true},{"os":"linux","index":2,"total":2,"host":"blacksmith-4vcpu-ubuntu-2404","run":true,"packages":false},{"os":"macos","index":1,"total":1,"host":"macos-15","run":true,"packages":true},{"os":"windows","index":1,"total":6,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":true},{"os":"windows","index":2,"total":6,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":false},{"os":"windows","index":3,"total":6,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":false},{"os":"windows","index":4,"total":6,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":false},{"os":"windows","index":5,"total":6,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":false},{"os":"windows","index":6,"total":6,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":false}]' >> "$GITHUB_OUTPUT" + # kilocode_change end unit: - name: unit (${{ matrix.settings.name }}) + # kilocode_change start + name: ${{ !matrix.settings.run && 'unit (unchanged)' || matrix.settings.total > 1 && format('unit ({0}, {1}/{2})', matrix.settings.os, matrix.settings.index, matrix.settings.total) || format('unit ({0})', matrix.settings.os) }} + needs: changes + # kilocode_change end strategy: fail-fast: false matrix: - settings: - - name: linux - host: blacksmith-4vcpu-ubuntu-2404 # kilocode_change - - name: macos - host: macos-15 # kilocode_change - - name: windows - host: blacksmith-4vcpu-windows-2025 # kilocode_change + settings: ${{ fromJSON(needs.changes.outputs.settings) }} # kilocode_change runs-on: ${{ matrix.settings.host }} timeout-minutes: 45 # kilocode_change + # kilocode_change start - manual dispatches exist to validate real execution (soak runs, + # timing measurements); every turbo step in this job bypasses the cache on dispatch + # instead of replaying a same-commit hit and executing nothing. + env: + TURBO_FORCE: ${{ github.event_name == 'workflow_dispatch' && 'true' || '' }} + # kilocode_change end defaults: run: shell: bash steps: + # kilocode_change start + - name: Skip unchanged general unit tests + if: ${{ !matrix.settings.run }} + run: echo "Only isolated product, documentation, or metadata files changed; general unit tests are unchanged." + # kilocode_change end + + # kilocode_change start - Defender real-time scanning taxes every process spawn and + # temp-file write on Windows; the suite spawns hundreds of test processes and builds + # temp git repos, so exclude the runner's work and temp paths up front. Best effort by + # design: on images where Defender is absent or locked down, the suite runs unchanged. + - name: Exclude runner work dirs from Defender scanning + if: matrix.settings.run && runner.os == 'Windows' + continue-on-error: true + shell: pwsh + run: | + $applied = 0 + foreach ($dir in @($env:GITHUB_WORKSPACE, $env:RUNNER_TEMP, $env:TEMP, "$env:USERPROFILE\.bun")) { + if (-not $dir) { continue } + try { + Add-MpPreference -ExclusionPath $dir -ErrorAction Stop + $applied++ + } catch { + Write-Host "Defender exclusion failed for ${dir}: $($_.Exception.Message)" + } + } + Write-Host "Defender path exclusions applied: $applied" + # kilocode_change end + - name: Checkout repository + if: matrix.settings.run # kilocode_change uses: actions/checkout@v6 # kilocode_change with: token: ${{ secrets.GITHUB_TOKEN }} # kilocode_change start - name: Setup Node + if: matrix.settings.run id: setup-node continue-on-error: ${{ runner.os == 'Windows' }} - uses: actions/setup-node@v6 # kilocode_change + uses: actions/setup-node@v6 with: node-version: "24" + # kilocode_change end + # kilocode_change start - name: Retry Setup Node on Windows - if: runner.os == 'Windows' && steps.setup-node.outcome == 'failure' + if: matrix.settings.run && runner.os == 'Windows' && steps.setup-node.outcome == 'failure' uses: actions/setup-node@v6 with: node-version: "24" # kilocode_change end - name: Setup Bun + if: matrix.settings.run # kilocode_change uses: ./.github/actions/setup-bun + # kilocode_change start + - name: Setup Linux sandbox helper + if: matrix.settings.run && runner.os == 'Linux' + uses: ./.github/actions/setup-linux-sandbox + # kilocode_change end - name: Configure git identity + if: matrix.settings.run # kilocode_change + # kilocode_change start run: | git config --global user.email "kilo-maintainer[bot]@users.noreply.github.com" git config --global user.name "kilo-maintainer[bot]" + # kilocode_change end - name: Cache Turbo + if: matrix.settings.run # kilocode_change uses: actions/cache@v5 # kilocode_change with: - path: node_modules/.cache/turbo - key: turbo-${{ runner.os }}-${{ hashFiles('turbo.json', '**/package.json') }}-${{ github.sha }} + path: .turbo/cache # kilocode_change + key: turbo-${{ runner.os }}-${{ hashFiles('turbo.json', 'bun.lock') }}-${{ matrix.settings.os }}-${{ matrix.settings.index }}-${{ github.sha }} # kilocode_change + # kilocode_change start restore-keys: | - turbo-${{ runner.os }}-${{ hashFiles('turbo.json', '**/package.json') }}- + turbo-${{ runner.os }}-${{ hashFiles('turbo.json', 'bun.lock') }}-${{ matrix.settings.os }}-${{ matrix.settings.index }}- + turbo-${{ runner.os }}-${{ hashFiles('turbo.json', 'bun.lock') }}- turbo-${{ runner.os }}- + # kilocode_change end - - name: Run unit tests - run: bun turbo test:ci --filter='!@kilocode/kilo-jetbrains' + # kilocode_change start - test non-CLI packages separately from sharded CLI tests + - name: Verify package test scheduling + if: matrix.settings.run && matrix.settings.packages && matrix.settings.os == 'linux' + run: bun run script/check-test-ci.ts + + - name: Run root tooling unit tests + if: matrix.settings.run && matrix.settings.packages && matrix.settings.os == 'linux' + run: bun run test:script:ci + + - name: Run non-CLI unit tests + if: matrix.settings.run && matrix.settings.packages + run: bun turbo test:ci --output-logs=errors-only --log-order=grouped --log-prefix=task --filter='!@kilocode/cli' --filter='!@kilocode/kilo-jetbrains' env: - KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }} - KILO_TEST_PROFILE: ${{ runner.os == 'macOS' && github.event_name == 'pull_request' && 'darwin' || '' }} # kilocode_change + KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: "true" # kilocode_change - non-CLI tests use the watcher-free unit-test profile + # kilocode_change end - - name: Run HttpApi exerciser gates - if: runner.os == 'Linux' # kilocode_change + # kilocode_change start - ensure the Darwin profile cannot suppress its own validation + - name: Validate Darwin CLI test profile + if: matrix.settings.run && matrix.settings.os == 'macos' working-directory: packages/opencode - run: bun run test:httpapi + run: bun test test/kilocode/test-profile.test.ts + # kilocode_change end + + # kilocode_change start - run Kilo CLI tests through platform-specific shards + - name: Run CLI unit tests + if: matrix.settings.run + run: bun turbo test:ci --output-logs=errors-only --log-order=grouped --log-prefix=task --filter='@kilocode/cli' + env: + KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: "true" # kilocode_change - was Windows-only; the CLI now starts a watcher per instance, too heavy/racy for unit tests. Watcher tests opt back in. + KILO_TEST_PROFILE: ${{ matrix.settings.os == 'macos' && 'darwin' || '' }} + KILO_TEST_SHARD: ${{ format('{0}/{1}', matrix.settings.index, matrix.settings.total) }} + # kilocode_change - cap parallelism on the 4-vCPU Windows runner. At the default + # min(4, cpus)=4, four heavy real-server test files share 4 vCPUs (~1 each) and blow + # their per-test timeouts; 2 gives each process real CPU headroom. Windows grows to + # 6 shards to absorb the lower per-shard parallelism. Linux/macOS (not timeout + # offenders; macOS is a single unsharded job) keep the default. + KILO_TEST_CONCURRENCY: ${{ matrix.settings.os == 'windows' && '2' || '' }} + # kilocode_change - 600s on every OS: the deadline guards against hangs, and a + # saturated shard can legitimately hold a heavy real-subprocess file past 300s + # (seen on Linux: run-process passing every test, killed at the deadline twice). + KILO_TEST_FILE_TIMEOUT: "600000" + # kilocode_change end + # kilocode_change start - name: Publish unit reports - if: always() + if: always() && matrix.settings.run uses: mikepenz/action-junit-report@bccf2e31636835cf0874589931c4116687171386 # v6.4.0 with: - report_paths: packages/*/.artifacts/unit/junit.xml - check_name: "unit results (${{ matrix.settings.name }})" + report_paths: | + .artifacts/unit/junit.xml + packages/*/.artifacts/unit/junit.xml + annotate_only: true detailed_summary: true include_time_in_summary: true fail_on_failure: false - name: Upload unit artifacts - if: always() - uses: actions/upload-artifact@v7 # kilocode_change + if: always() && matrix.settings.run + # kilocode_change - diagnostics only: a runner-side network blip here must not + # fail a job whose tests already passed (seen as ECONNREFUSED on Blacksmith). + continue-on-error: true + uses: actions/upload-artifact@v7 with: - name: unit-${{ matrix.settings.name }}-${{ github.run_attempt }} + name: unit-${{ matrix.settings.os }}-${{ matrix.settings.index }}-${{ github.run_attempt }} include-hidden-files: true if-no-files-found: ignore retention-days: 7 - path: packages/*/.artifacts/unit/junit.xml + path: | + .artifacts/unit/junit.xml + packages/*/.artifacts/unit/junit.xml + # kilocode_change end + + # kilocode_change start + httpapi: + name: HttpApi exerciser + needs: changes + if: needs.changes.outputs.general == 'true' + runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 15 + defaults: + run: + shell: bash + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Bun + uses: ./.github/actions/setup-bun + + - name: Setup Linux sandbox helper + uses: ./.github/actions/setup-linux-sandbox + + - name: Configure git identity + run: | + git config --global user.email "kilo-maintainer[bot]@users.noreply.github.com" + git config --global user.name "kilo-maintainer[bot]" + + - name: Cache Turbo + uses: actions/cache@v5 + with: + path: .turbo/cache + key: turbo-${{ runner.os }}-${{ hashFiles('turbo.json', 'bun.lock') }}-httpapi-${{ github.sha }} + restore-keys: | + turbo-${{ runner.os }}-${{ hashFiles('turbo.json', 'bun.lock') }}-httpapi- + turbo-${{ runner.os }}-${{ hashFiles('turbo.json', 'bun.lock') }}- + turbo-${{ runner.os }}- + + - name: Check generated client + if: runner.os == 'Linux' + working-directory: packages/client + run: bun run check:generated + - name: Run HttpApi exerciser gates + run: bun turbo test:httpapi --filter='@kilocode/cli' + env: + KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: "true" # kilocode_change - a watcher per scenario instance is too heavy for the exerciser + # kilocode_change end # kilocode_change start @@ -119,19 +301,40 @@ jobs: uses: ./.github/workflows/test-jetbrains.yml # kilocode_change end + # kilocode_change start + unit-required: + name: unit (linux) + runs-on: blacksmith-4vcpu-ubuntu-2404 + needs: + - unit + if: always() + steps: + - name: Verify unit matrix passed + run: | + echo "unit=${{ needs.unit.result }}" + test "${{ needs.unit.result }}" = "success" + # kilocode_change end # kilocode_change start required: name: test (linux) runs-on: blacksmith-4vcpu-ubuntu-2404 needs: + - changes - unit + - httpapi - jetbrains if: always() steps: - - name: Verify upstream test jobs passed + - name: Verify upstream test jobs passed # kilocode_change run: | echo "unit=${{ needs.unit.result }}" + echo "httpapi=${{ needs.httpapi.result }}" echo "jetbrains=${{ needs.jetbrains.result }}" test "${{ needs.unit.result }}" = "success" + if [ "${{ needs.changes.outputs.general }}" = "true" ]; then + test "${{ needs.httpapi.result }}" = "success" + else + test "${{ needs.httpapi.result }}" = "skipped" + fi test "${{ needs.jetbrains.result }}" = "success" # kilocode_change end diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 11ef4e1ab75..d2391e77781 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -8,9 +8,44 @@ on: workflow_dispatch: jobs: + # kilocode_change start + changes: + name: detect typecheck changes + runs-on: blacksmith-4vcpu-ubuntu-2404 + outputs: + javascript: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.filter.outputs.javascript }} + jetbrains: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.filter.outputs.jetbrains }} + steps: + - name: Checkout repository + if: github.event_name != 'workflow_dispatch' + uses: actions/checkout@v6 + + - name: Detect typecheck changes + if: github.event_name != 'workflow_dispatch' + id: filter + uses: Kilo-Org/paths-filter@668c092af3649c4b664c54e4b704aa46782f6f7c # v3 + with: + predicate-quantifier: every + filters: | + javascript: + - '**' + - '!.changeset/**' + - '!.kilo/plans/**' + - '!packages/kilo-jetbrains/**' + jetbrains: + - '**' + - '!.changeset/**' + - '!.kilo/plans/**' + - '!packages/opencode/**' + - '!packages/kilo-vscode/**' + - '!packages/kilo-docs/**' + # kilocode_change end + typecheck-js: name: typecheck-js - runs-on: blacksmith-4vcpu-ubuntu-2404 + needs: changes # kilocode_change + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.javascript == 'true' # kilocode_change + runs-on: blacksmith-4vcpu-ubuntu-2404 # kilocode_change steps: - name: Checkout repository uses: actions/checkout@v6 # kilocode_change @@ -29,6 +64,8 @@ jobs: # kilocode_change start typecheck-jetbrains: name: typecheck-jetbrains + needs: changes + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.jetbrains == 'true' runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository @@ -57,13 +94,27 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2404 needs: - typecheck-js + - changes - typecheck-jetbrains if: always() steps: - name: Verify typecheck jobs passed run: | + echo "changes=${{ needs.changes.result }}" + echo "javascript=${{ needs.changes.outputs.javascript }}" echo "typecheck-js=${{ needs.typecheck-js.result }}" echo "typecheck-jetbrains=${{ needs.typecheck-jetbrains.result }}" - test "${{ needs.typecheck-js.result }}" = "success" - test "${{ needs.typecheck-jetbrains.result }}" = "success" + test "${{ needs.changes.result }}" = "success" + if [ "${{ needs.changes.outputs.javascript }}" = "true" ]; then + test "${{ needs.typecheck-js.result }}" = "success" + else + test "${{ needs.changes.outputs.javascript }}" = "false" + test "${{ needs.typecheck-js.result }}" = "skipped" + fi + if [ "${{ needs.changes.outputs.jetbrains }}" = "true" ]; then + test "${{ needs.typecheck-jetbrains.result }}" = "success" + else + test "${{ needs.changes.outputs.jetbrains }}" = "false" + test "${{ needs.typecheck-jetbrains.result }}" = "skipped" + fi # kilocode_change end diff --git a/.github/workflows/visual-regression.yml b/.github/workflows/visual-regression.yml index 69ebd6a17a6..dbe2a07c244 100644 --- a/.github/workflows/visual-regression.yml +++ b/.github/workflows/visual-regression.yml @@ -1,19 +1,23 @@ +# kilocode_change - new file name: Visual Regression Tests on: pull_request: types: [opened, synchronize, reopened] +permissions: + contents: read + pull-requests: read + jobs: check-paths: name: Check changed paths - runs-on: ubuntu-latest + runs-on: ubuntu-latest # kilocode_change outputs: matched: ${{ steps.filter.outputs.matched }} - is_fork: ${{ steps.fork-check.outputs.is_fork }} + can_autocommit: ${{ steps.autocommit-check.outputs.can_autocommit }} steps: - - uses: actions/checkout@v6 # kilocode_change - # kilocode_change start + - uses: actions/checkout@v6 - name: Check changed files id: filter env: @@ -23,7 +27,7 @@ jobs: matched=false while IFS= read -r file; do case "$file" in - packages/kilo-ui/*|packages/ui/*|packages/util/*|packages/sdk/js/*|packages/kilo-vscode/webview-ui/*|packages/kilo-vscode/.storybook/*|packages/kilo-vscode/tests/visual-regression*|packages/kilo-vscode/tests/permission-dock-dropdown*|packages/kilo-vscode/tests/accessibility*|packages/kilo-docs/public/img/screenshot-tests/*|.github/workflows/visual-regression.yml) + packages/kilo-ui/*|packages/ui/*|packages/util/*|packages/sdk/js/*|packages/kilo-vscode/webview-ui/*|packages/kilo-vscode/.storybook/*|packages/kilo-vscode/tests/visual-regression*|packages/kilo-vscode/tests/permission-dock-dropdown*|packages/kilo-vscode/tests/accessibility*|packages/kilo-docs/public/img/screenshot-tests/*|.github/actions/setup-bun/*|.github/workflows/visual-regression.yml) matched=true break ;; @@ -31,7 +35,6 @@ jobs: done < <(gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls/${PR}/files" --jq '.[].filename') echo "matched=$matched" >> "$GITHUB_OUTPUT" echo "matched=$matched" - # kilocode_change end - name: Check if PR is from a fork id: fork-check run: | @@ -40,31 +43,43 @@ jobs: else echo "is_fork=false" >> "$GITHUB_OUTPUT" fi + - name: Check baseline auto-commit permissions + id: autocommit-check + env: + MAINTAINER_APP_ID: ${{ secrets.KILO_MAINTAINER_APP_ID }} + MAINTAINER_APP_SECRET: ${{ secrets.KILO_MAINTAINER_APP_SECRET }} + run: | + if [ "${{ steps.fork-check.outputs.is_fork }}" != "true" ] && [ -n "$MAINTAINER_APP_ID" ] && [ -n "$MAINTAINER_APP_SECRET" ]; then + echo "can_autocommit=true" >> "$GITHUB_OUTPUT" + else + echo "can_autocommit=false" >> "$GITHUB_OUTPUT" + fi visual-regression: needs: check-paths if: needs.check-paths.outputs.matched == 'true' - name: Visual Regression (kilo-ui) + name: Visual Regression (kilo-ui) # kilocode_change runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 15 steps: - name: Checkout (internal) - if: needs.check-paths.outputs.is_fork != 'true' - uses: actions/checkout@v6 # kilocode_change + if: needs.check-paths.outputs.can_autocommit == 'true' + uses: actions/checkout@v6 with: lfs: true - token: ${{ secrets.BOT_PAT }} + # Use github.token for LFS access. The maintainer app is used only for generated commits. + token: ${{ github.token }} ref: ${{ github.head_ref }} - - name: Checkout (fork) - if: needs.check-paths.outputs.is_fork == 'true' - uses: actions/checkout@v6 # kilocode_change + - name: Checkout (read-only) + if: needs.check-paths.outputs.can_autocommit != 'true' + uses: actions/checkout@v6 with: lfs: true - name: Check if HEAD is a baseline update commit - if: needs.check-paths.outputs.is_fork != 'true' + if: needs.check-paths.outputs.can_autocommit == 'true' id: check-baseline-commit run: | COMMIT_MSG=$(git log -1 --format=%s) @@ -76,22 +91,11 @@ jobs: fi - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Cache Bun modules - uses: actions/cache@v5 # kilocode_change - with: - path: ~/.bun/install/cache - key: bun-${{ hashFiles('bun.lock') }} - - - name: Install dependencies - run: bun install + uses: ./.github/actions/setup-bun # kilocode_change - name: Cache Playwright browsers id: playwright-cache - uses: actions/cache@v5 # kilocode_change + uses: actions/cache@v5 with: path: ~/.cache/ms-playwright key: playwright-${{ hashFiles('packages/kilo-ui/package.json') }} @@ -107,7 +111,7 @@ jobs: - name: Cache Storybook build id: storybook-cache - uses: actions/cache@v5 # kilocode_change + uses: actions/cache@v5 with: path: packages/kilo-ui/storybook-static key: storybook-${{ hashFiles('packages/kilo-ui/src/**', 'packages/kilo-ui/.storybook/**', 'packages/ui/src/**', 'packages/kilo-ui/package.json') }} @@ -144,22 +148,22 @@ jobs: " working-directory: packages/kilo-ui - - name: Check for baseline changes (fork PRs) - if: needs.check-paths.outputs.is_fork == 'true' + - name: Check for baseline changes (read-only PRs) + if: needs.check-paths.outputs.can_autocommit != 'true' run: | git add packages/kilo-docs/public/img/screenshot-tests/kilo-ui/ if git diff --cached --quiet; then echo "No visual regression detected." else echo "::error::Visual regression detected. Screenshot baselines have changed." - echo "::error::Since this PR is from a fork, updated screenshots cannot be committed automatically." + echo "::error::Since this PR cannot receive baseline commits automatically, updated screenshots cannot be committed." echo "::error::Please ask a Kilo developer for help updating the screenshots." git diff --cached --stat exit 1 fi - name: Fail if baselines still changing after auto-update - if: needs.check-paths.outputs.is_fork != 'true' && steps.check-baseline-commit.outputs.is_baseline_update == 'true' + if: needs.check-paths.outputs.can_autocommit == 'true' && steps.check-baseline-commit.outputs.is_baseline_update == 'true' run: | git add packages/kilo-docs/public/img/screenshot-tests/kilo-ui/ if git diff --cached --quiet; then @@ -172,34 +176,42 @@ jobs: exit 1 fi - - name: Commit and push new baselines (if any) - if: needs.check-paths.outputs.is_fork != 'true' && steps.check-baseline-commit.outputs.is_baseline_update != 'true' - id: commit-baselines - env: - GH_TOKEN: ${{ secrets.BOT_PAT }} + - name: Check for baseline updates + if: needs.check-paths.outputs.can_autocommit == 'true' && steps.check-baseline-commit.outputs.is_baseline_update != 'true' + id: baseline-changes run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" git add packages/kilo-docs/public/img/screenshot-tests/kilo-ui/ if git diff --cached --quiet; then - echo "No new baselines — nothing to commit." echo "changed=false" >> "$GITHUB_OUTPUT" else - git commit -m "chore: update visual regression baselines" - git lfs push --all origin - git push --no-verify echo "changed=true" >> "$GITHUB_OUTPUT" fi + - name: Setup Git Committer + if: steps.baseline-changes.outputs.changed == 'true' + uses: ./.github/actions/setup-git-committer + with: + kilo-maintainer-app-id: ${{ secrets.KILO_MAINTAINER_APP_ID }} + kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }} + + - name: Commit and push new baselines (if any) + if: steps.baseline-changes.outputs.changed == 'true' + id: commit-baselines + run: | + git commit -m "chore: update visual regression baselines" + git lfs push --all origin + git push --no-verify origin "HEAD:${GITHUB_HEAD_REF}" + echo "changed=true" >> "$GITHUB_OUTPUT" + - name: Fail if baselines changed - if: needs.check-paths.outputs.is_fork != 'true' && steps.commit-baselines.outputs.changed == 'true' + if: needs.check-paths.outputs.can_autocommit == 'true' && steps.commit-baselines.outputs.changed == 'true' run: | echo "::error::Visual regression baselines changed. New baselines have been committed to the branch. Please pull and review." exit 1 - name: Upload test results on failure if: failure() - uses: actions/upload-artifact@v7 # kilocode_change + uses: actions/upload-artifact@v7 with: name: visual-regression-results path: packages/kilo-ui/test-results/ @@ -208,31 +220,30 @@ jobs: visual-regression-vscode: needs: check-paths if: needs.check-paths.outputs.matched == 'true' - name: Visual Regression (kilo-vscode webview) + name: Visual Regression (kilo-vscode webview) # kilocode_change runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 15 - # kilocode_change start env: NODE_OPTIONS: --max-old-space-size=4096 - # kilocode_change end steps: - name: Checkout (internal) - if: needs.check-paths.outputs.is_fork != 'true' - uses: actions/checkout@v6 # kilocode_change + if: needs.check-paths.outputs.can_autocommit == 'true' + uses: actions/checkout@v6 with: lfs: true - token: ${{ secrets.BOT_PAT }} + # Use github.token for LFS access. The maintainer app is used only for generated commits. + token: ${{ github.token }} ref: ${{ github.head_ref }} - - name: Checkout (fork) - if: needs.check-paths.outputs.is_fork == 'true' - uses: actions/checkout@v6 # kilocode_change + - name: Checkout (read-only) + if: needs.check-paths.outputs.can_autocommit != 'true' + uses: actions/checkout@v6 with: lfs: true - name: Check if HEAD is a baseline update commit - if: needs.check-paths.outputs.is_fork != 'true' + if: needs.check-paths.outputs.can_autocommit == 'true' id: check-baseline-commit-vscode run: | COMMIT_MSG=$(git log -1 --format=%s) @@ -244,22 +255,11 @@ jobs: fi - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Cache Bun modules - uses: actions/cache@v5 # kilocode_change - with: - path: ~/.bun/install/cache - key: bun-${{ hashFiles('bun.lock') }} - - - name: Install dependencies - run: bun install + uses: ./.github/actions/setup-bun # kilocode_change - name: Cache Playwright browsers id: playwright-cache-vscode - uses: actions/cache@v5 # kilocode_change + uses: actions/cache@v5 with: path: ~/.cache/ms-playwright key: playwright-vscode-${{ hashFiles('packages/kilo-vscode/package.json') }} @@ -275,7 +275,7 @@ jobs: - name: Cache Storybook build id: storybook-cache-vscode - uses: actions/cache@v5 # kilocode_change + uses: actions/cache@v5 with: path: packages/kilo-vscode/storybook-static key: storybook-vscode-${{ hashFiles('packages/kilo-vscode/webview-ui/src/**', 'packages/kilo-vscode/.storybook/**', 'packages/kilo-ui/src/**', 'packages/ui/src/**', 'packages/kilo-vscode/package.json') }} @@ -284,18 +284,16 @@ jobs: if: steps.storybook-cache-vscode.outputs.cache-hit != 'true' run: bun run build-storybook working-directory: packages/kilo-vscode - # kilocode_change start env: NODE_OPTIONS: --max-old-space-size=4096 - # kilocode_change end - - name: Generate baselines and enforce webview accessibility checks # kilocode_change + - name: Generate baselines and enforce webview accessibility checks run: bun run test:visual:update working-directory: packages/kilo-vscode env: CI: true PLAYWRIGHT_WORKERS: "4" - NODE_OPTIONS: --max-old-space-size=4096 # kilocode_change + NODE_OPTIONS: --max-old-space-size=4096 - name: Remove stale baselines for deleted stories run: | @@ -344,22 +342,22 @@ jobs: " working-directory: packages/kilo-vscode - - name: Check for baseline changes (fork PRs) - if: needs.check-paths.outputs.is_fork == 'true' + - name: Check for baseline changes (read-only PRs) + if: needs.check-paths.outputs.can_autocommit != 'true' run: | git add packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/ if git diff --cached --quiet; then echo "No visual regression detected." else echo "::error::Visual regression detected. Screenshot baselines have changed." - echo "::error::Since this PR is from a fork, updated screenshots cannot be committed automatically." + echo "::error::Since this PR cannot receive baseline commits automatically, updated screenshots cannot be committed." echo "::error::Please ask a Kilo developer for help updating the screenshots." git diff --cached --stat exit 1 fi - name: Fail if baselines still changing after auto-update - if: needs.check-paths.outputs.is_fork != 'true' && steps.check-baseline-commit-vscode.outputs.is_baseline_update == 'true' + if: needs.check-paths.outputs.can_autocommit == 'true' && steps.check-baseline-commit-vscode.outputs.is_baseline_update == 'true' run: | git add packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/ if git diff --cached --quiet; then @@ -372,34 +370,42 @@ jobs: exit 1 fi - - name: Commit and push new baselines (if any) - if: needs.check-paths.outputs.is_fork != 'true' && steps.check-baseline-commit-vscode.outputs.is_baseline_update != 'true' - id: commit-baselines-vscode - env: - GH_TOKEN: ${{ secrets.BOT_PAT }} + - name: Check for baseline updates + if: needs.check-paths.outputs.can_autocommit == 'true' && steps.check-baseline-commit-vscode.outputs.is_baseline_update != 'true' + id: baseline-changes-vscode run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" git add packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/ if git diff --cached --quiet; then - echo "No new baselines — nothing to commit." echo "changed=false" >> "$GITHUB_OUTPUT" else - git commit -m "chore: update kilo-vscode visual regression baselines" - git lfs push --all origin - git push --no-verify echo "changed=true" >> "$GITHUB_OUTPUT" fi + - name: Setup Git Committer + if: steps.baseline-changes-vscode.outputs.changed == 'true' + uses: ./.github/actions/setup-git-committer + with: + kilo-maintainer-app-id: ${{ secrets.KILO_MAINTAINER_APP_ID }} + kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }} + + - name: Commit and push new baselines (if any) + if: steps.baseline-changes-vscode.outputs.changed == 'true' + id: commit-baselines-vscode + run: | + git commit -m "chore: update kilo-vscode visual regression baselines" + git lfs push --all origin + git push --no-verify origin "HEAD:${GITHUB_HEAD_REF}" + echo "changed=true" >> "$GITHUB_OUTPUT" + - name: Fail if baselines changed - if: needs.check-paths.outputs.is_fork != 'true' && steps.commit-baselines-vscode.outputs.changed == 'true' + if: needs.check-paths.outputs.can_autocommit == 'true' && steps.commit-baselines-vscode.outputs.changed == 'true' run: | echo "::error::Visual regression baselines changed. New baselines have been committed to the branch. Please pull and review." exit 1 - name: Upload test results on failure if: failure() - uses: actions/upload-artifact@v7 # kilocode_change + uses: actions/upload-artifact@v7 with: name: visual-regression-vscode-results path: packages/kilo-vscode/test-results/ diff --git a/.github/workflows/watch-opencode-releases.yml b/.github/workflows/watch-opencode-releases.yml index a3d766b7aed..46d2359e756 100644 --- a/.github/workflows/watch-opencode-releases.yml +++ b/.github/workflows/watch-opencode-releases.yml @@ -12,7 +12,7 @@ permissions: jobs: check-release: - if: github.repository == 'Kilo-Org/kilocode' && vars.OPENCODE_WATCH_ENABLED != 'false' + if: vars.OPENCODE_WATCH_ENABLED != 'false' && (github.event_name == 'workflow_dispatch' || github.repository == 'Kilo-Org/kilocode') # kilocode_change runs-on: ubuntu-latest timeout-minutes: 5 steps: diff --git a/.gitignore b/.gitignore index 5611c39a766..16f4034b7f6 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,8 @@ storybook-static .turbo **/.serena .serena/ +**/.omo +.omo/ /result refs Session.vim @@ -65,6 +67,11 @@ tsconfig.tsbuildinfo .kilocode/yarn.lock .kilocode/node_modules +# Runtime-generated per-directory TUI config (e.g. written by `PATCH /tui/config` +# during local testing); should never be committed. +packages/*/.kilo/tui.json +packages/*/.kilo/tui.jsonc + # Test Artifacts packages/app/.artifacts/ packages/opencode/.artifacts/ diff --git a/.husky/pre-push b/.husky/pre-push index 5d3cc53411b..efe6a35a751 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -17,4 +17,34 @@ if (process.versions.bun !== expectedBunVersion) { console.warn(`Warning: Bun version ${process.versions.bun} differs from expected ${expectedBunVersion}`); } ' -bun typecheck + +# kilocode_change start +jetbrains=0 +zero=0000000000000000000000000000000000000000 + +while read -r local_ref local_oid remote_ref remote_oid; do + if [ "$local_oid" = "$zero" ]; then + continue + fi + + base="$remote_oid" + if [ "$remote_oid" = "$zero" ]; then + base=$(git merge-base "$local_oid" "$1/main" 2>/dev/null || git hash-object -t tree /dev/null) + fi + + if ! git diff --quiet "$base" "$local_oid" -- . \ + ':(exclude).changeset/**' \ + ':(exclude)packages/kilo-vscode/**' \ + ':(exclude)packages/kilo-docs/**'; then + jetbrains=1 + fi +done + +bun typecheck --filter='!@kilocode/kilo-jetbrains' + +if [ "$jetbrains" -eq 1 ]; then + bun typecheck --filter='@kilocode/kilo-jetbrains' +else + echo "Skipping JetBrains typecheck (no relevant changes)" +fi +# kilocode_change end diff --git a/.kilo/command/review-upstream-merge.md b/.kilo/command/review-upstream-merge.md index 36237d81c1c..ce2625a7116 100644 --- a/.kilo/command/review-upstream-merge.md +++ b/.kilo/command/review-upstream-merge.md @@ -62,6 +62,18 @@ Pay special attention to: When in doubt, add a finding. A human will verify it. Compiling code is not proof the chain is intact. +## CONFIG_REGRESSION.md + +Check whether this PR introduces or re-introduces fallback logic for `opencode` config files, or accidentally breaks code that now correctly expects only `.kilo`-based configuration. + +Kilo removed fallback support for `opencode` config directories. Look for: + +- Any new or restored code that reads from `opencode` config paths. +- Upstream additions to config discovery, loading, or path resolution that add `opencode` fallback candidates we stripped. +- Changes that break `.kilo`-only config lookup by removing or reordering it in a multi-path search. + +When in doubt, add a finding. A human should verify config path changes manually. + ## TESTS.md Check whether this PR removed any Kilo-specific tests. diff --git a/.kilo/plans/1782926865817-jetbrains-model-picker-details-plan.md b/.kilo/plans/1782926865817-jetbrains-model-picker-details-plan.md new file mode 100644 index 00000000000..ac6d9d6ec5e --- /dev/null +++ b/.kilo/plans/1782926865817-jetbrains-model-picker-details-plan.md @@ -0,0 +1,84 @@ +# JetBrains Model Picker Details Plan + +## Goal +Add a JetBrains model picker maximize/minimize affordance that expands the popup to roughly double width and shows model details comparable to VS Code's `ModelPreview`. Persist the last expanded/collapsed state in IntelliJ `PropertiesComponent`. First-time default is collapsed. + +## Decisions +- Scope is full VS Code parity where JetBrains can receive the same model metadata: pricing, release date, descriptions, modalities/capabilities, terminal-bench, auto-routing details, context limits, badges, and provider/model identity. +- Default state is collapsed for users without a saved property. +- Expanded state is app-level UI state stored in `PropertiesComponent`, not backend/session state. +- Use Swing/IntelliJ platform components only. Do not introduce Compose, JCEF, or UI DSL. +- Keep existing compact list behavior when collapsed. In expanded mode, show a retained details panel beside the list. + +## Affected Areas +- `packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ProviderDto.kt` +- `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt` +- `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt` +- `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt` +- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt` +- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt` +- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt` +- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker*.kt` +- `packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle*.properties` +- Existing tests under `packages/kilo-jetbrains/frontend/src/test/...` and `backend/src/test/...` + +## Implementation Steps +1. Extend model metadata DTOs. + - Add serializable nested DTOs to `ProviderDto.kt` for preview metadata matching VS Code provider types where practical: cost/cache cost, capabilities/input modalities, options/description, auto-routing model ids, terminal-bench score/cost. + - Extend `ModelDto` with nullable fields such as `inputPrice`, `outputPrice`, `contextLength`, `releaseDate`, `latest`, `cost`, `capabilities`, `options`, `autoRouting`, and `terminalBench`. + - Preserve existing fields (`limit`, `variants`, `free`, `byok`, `mayTrainOnYourPrompts`) to avoid breaking callers. + +2. Parse and map the extra metadata in the JetBrains backend. + - Extend `ModelInfo` in `KiloWorkspaceState.kt` with matching nullable metadata fields. + - Update `KiloCliDataParser.parseModel` and `parseModelDto` to read the fields currently used by VS Code: `inputPrice`, `outputPrice`, `contextLength`, `releaseDate`, `latest`, `cost`, `capabilities.input`, `options.description`, `autoRouting.models`, and `terminalBench`. + - Keep permissive parsing: missing or malformed optional fields should result in `null` or empty data, not provider load failure. + - Update `KiloWorkspaceDtoMapper.model` to copy all new fields to `ModelDto`. + +3. Carry metadata through the frontend session/settings model flow. + - Extend session `ModelItem` and picker `ModelPicker.Item` to hold the preview metadata. + - Update `SessionController` -> `SessionModel`, `SessionUi`, and `ModelsSettingsUi.items()` mappings so metadata survives into the picker. + - Pass `limit` through to `ModelPicker.Item`; it is already available in `ModelItem` but currently dropped in `SessionUi`. + +4. Add a retained Swing details panel. + - Create a small `ModelDetailsPanel` near `session/ui/model/` or inside `ModelPicker.kt` if compact enough. + - Use `JBLabel`, `SimpleColoredComponent`, `JBHtmlPane` or safe HTML helpers, `Stack`, and `JBUI` spacing. + - Render VS Code-equivalent sections where data exists: header name/provider/star, free/BYOK/training badges, pricing, context window, release date, capabilities/modalities, terminal-bench, description, auto-routing candidates, and ids. + - Sanitize/escape model descriptions. Use `HtmlChunk`/`HtmlBuilder` or platform escaping helpers; do not concatenate unescaped HTML. + - Details panel should update from the currently previewed row in expanded mode and fall back to the active selected model when no preview row is set. + +5. Rework popup layout and size behavior. + - Add an expand/minimize button to the search/header row in `ModelPicker.showPopup()`. + - Use pointer cursor on the button and localized tooltips/accessibility names: `Maximize model details` and `Minimize model details` or equivalent. + - Persist state under a key such as `kilo.model.picker.expanded` using `PropertiesComponent.getInstance().getBoolean(key, false)` and `setValue(key, value.toString())`. + - Collapsed mode keeps the current list-only width behavior. + - Expanded mode lays out list and details side-by-side with a splitter/separator and sets preferred width to approximately double the collapsed width, clamped to available screen bounds. Keep `setLocateWithinScreenBounds(true)`. + - Make `JBPopup` resizable only if the implementation can keep the retained layout stable; otherwise keep non-resizable and compute deterministic sizes. + +6. Match VS Code interaction semantics where useful. + - Collapsed mode: clicking a model selects it immediately, preserving current behavior. + - Expanded mode: clicking/hovering or keyboard navigation updates the preview; Enter selects; double-click selects; Escape closes. + - Favorite star behavior remains unchanged and updates the details panel favorite state when toggled. + - When collapsing from expanded, clear transient preview state and show selected/active model on next expansion. + +7. Add localization strings. + - Add base `KiloBundle.properties` entries for maximize/minimize tooltips, detail labels, pricing labels, context, capabilities, terminal-bench, description, auto-routing choices, and unavailable values. + - If existing localized files require key completeness, add English fallback values there too; otherwise rely on base bundle fallback consistent with project practice. + +8. Add tests. + - Backend parser tests: ensure all new metadata fields parse from representative provider JSON and missing optional fields are tolerated. + - Mapper/serialization tests: ensure new DTO fields survive backend -> shared DTO serialization. + - `ModelPickerTest`: verify expanded property default is collapsed, toggle persists true/false, tooltip/icon state switches maximize/minimize, expanded width is greater than collapsed width, preview panel displays selected/hovered model metadata, and pointer cursor is set on the expand control. + - Renderer/details tests should exercise actual Swing components on the EDT using existing `BasePlatformTestCase` patterns. + - Update existing tests/helpers that construct `ModelPicker.Item`, `ModelItem`, or `ModelDto` as needed using default values to keep changes minimal. + +## Risks And Constraints +- Full parity depends on metadata already present in CLI provider JSON. If a field is absent from the backend response for a provider, the details panel should omit that row rather than showing placeholders everywhere. +- Adding fields to shared serializable DTOs affects split-mode RPC compatibility. Keep new fields nullable/defaulted for safe deserialization. +- Avoid making provider loading fragile: optional preview metadata parsing must not throw on unexpected types. +- The popup is a lightweight `JBPopup`; changing size after showing may require recreating/repacking carefully. Prefer computing expanded/collapsed preferred size before show and updating the content with `revalidate()`/`repaint()` on toggle. +- Keep all Swing mutation on the EDT. + +## Validation +- From `packages/kilo-jetbrains/`: run `./gradlew typecheck` or `bun run typecheck`. +- From `packages/kilo-jetbrains/`: run targeted tests for backend parser/serialization and frontend model picker tests, or `./gradlew test` if targeted Gradle filters are not straightforward. +- Manually verify in sandbox if feasible: `./gradlew runIde`, open model picker, confirm first open is collapsed, maximize doubles width and shows details, minimize returns to compact width, tooltip/cursor are correct, and the last state persists after closing/reopening the popup. diff --git a/.kilo/plans/1783518847053-prompt-rollback-redo-jetbrains-parity.md b/.kilo/plans/1783518847053-prompt-rollback-redo-jetbrains-parity.md new file mode 100644 index 00000000000..dd71fdffc7f --- /dev/null +++ b/.kilo/plans/1783518847053-prompt-rollback-redo-jetbrains-parity.md @@ -0,0 +1,111 @@ +# Prompt Rollback + Redo — JetBrains Parity + +Bring the VS Code "rollback / redo" (CLI `revert` / `unrevert`) feature to the JetBrains plugin, and fix the misleading VS Code hint string. Implementation-capable agent required — this plan touches source and tests. + +## Context (verified) + +- CLI already exposes everything needed; no CLI release or SDK regen required. Pinned CLI version is `7.4.1` (`packages/kilo-jetbrains/package.json`). + - `POST /session/{id}/revert` body `{ messageID, partID? }`, `POST /session/{id}/unrevert`. + - `Session.revert?: { messageID, partID?, snapshot?, diff? }` — `packages/sdk/js/src/gen/types.gen.ts:554`. Pushed via `session.updated` SSE. +- VS Code entry points: `RevertBanner.tsx`, `VscodeSessionTurn.tsx`, `session.tsx` context. Per-user-message rollback restores reverted prompt text into the input; redo banner shows count + per-file diff stats + Redo / Redo All + hint; reverted messages/parts hidden (`id >= revert.messageID`, partID partial-turn boundary). +- JetBrains split-mode packages differ from AGENTS.md's stated `ai.kilocode.jetbrains`; actual source roots are: + - shared → `packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/` + - backend → `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/` + - frontend → `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/` +- All target files confirmed to exist: `SessionDto.kt`, `KiloSessionRpcApi.kt`, `KiloBackendChatManager.kt`, `KiloSessionRpcApiImpl.kt`, `KiloSessionService.kt`, `SessionController.kt`, `SessionModel.kt`, `SessionModelEvent.kt`, `SessionView.kt`, `MessageView.kt`, `MessageToolbar.kt`, `PromptPanel.kt`, `PromptEditorTextField.kt`, plus reuse targets `DiffStatBadge.kt`, `HoverIcon.kt`, `PermissionDiffView.kt`. + +## Decisions + +- **Reuse `DiffStatBadge`** (already the shared per-file +/- renderer used by `PermissionDiffView`) for the banner's per-file diff rows. No new diff-stat component. +- **Reuse the existing user-prompt hover toolbar (`MessageToolbar`)** for the per-message rollback control. +- **Reuse the existing `session.updated → model.setSession(...)` pipeline** for the revert boundary; no new SSE handling. +- **Stale non-English hint strings: reset all to the new English value** and flag for re-translation (applies to VS Code locales; JetBrains base bundle only, others fall back). + +## Part A — VS Code string fix + +File: `packages/kilo-vscode/webview-ui/src/i18n/en.ts:138` + +- Change `revert.banner.hint` from `"Send a new message to make this permanent"` to `"You can redo these changes until you send a new message"`. +- In the 19 non-English locale files under `packages/kilo-vscode/webview-ui/src/i18n/` (ar, br, bs, da, de, es, fr, it, ja, ko, nl, no, pl, ru, th, tr, uk, zh, zht), overwrite `revert.banner.hint` with the identical new English string; leave a note that these need re-translation. +- Do not touch the other revert keys (`revert.banner.count_one`, `revert.banner.count_other`, `revert.banner.redo`, `revert.banner.redo.all`, `revert.disabled.agentBusy`) — only the hint changes meaning. + +## Part B — JetBrains implementation + +### 1. Shared (DTO + RPC contract) + +- `shared/.../rpc/dto/SessionDto.kt`: add `val revert: SessionRevertDto? = null`; add `@Serializable data class SessionRevertDto(val messageID: String, val partID: String? = null, val snapshot: String? = null, val diff: String? = null)`. Field names must match CLI JSON for kotlinx auto-mapping through `session.updated`. +- `shared/.../rpc/KiloSessionRpcApi.kt`: add + - `suspend fun revert(id: String, directory: String, messageID: String, partID: String?)` + - `suspend fun unrevert(id: String, directory: String)` + +### 2. Backend (RPC impl + HTTP) + +- `backend/.../app/KiloBackendChatManager.kt`: add `revert(id, dir, messageID, partID?)` → `POST /session/$id/revert?directory=…` with `{messageID, partID?}` body, and `unrevert(id, dir)` → `POST /session/$id/unrevert?directory=…`. Mirror the existing `compact()` OkHttp pattern; add JSON body builders to `KiloCliDataParser`. +- `backend/.../rpc/KiloSessionRpcApiImpl.kt`: implement both, resolve worktree dir via `sessions.getDirectory(id, directory)`, wrap in `ready { }` like `compact`/`abort`. + +### 3. Frontend service + +- `frontend/.../app/KiloSessionService.kt`: add `suspend revert(...)` / `unrevert(...)` wrappers over the RPC, using `durable {}` per split-mode rules. + +### 4. Frontend controller (`session/controller/SessionController.kt`) + +- Add EDT-entry actions dispatched to the coroutine scope: + - `revert(messageID, partID?)` + - `unrevert()` + - `redo()` — mirror `RevertBanner.handleRedo`: find the next user message after the boundary and revert to it; `unrevert()` if none. + - `redoAll()` → `unrevert()`. +- Revert boundary already arrives via `is ChatEventDto.SessionUpdated -> model.setSession(...)` — no new SSE handling. +- On revert, restore the reverted user prompt text into the input (`session/ui/prompt/PromptPanel.kt` / `PromptEditorTextField`); on full redo (`redoAll` / final `unrevert`), clear it — matching VS Code and TUI. + +### 5. Frontend model (`session/model/SessionModel.kt`, `SessionModelEvent.kt`) + +- Add `SessionModelEvent.RevertChanged(revert: SessionRevertDto?)` with a stable `toString()`, fired from `setSession()` when the revert value changes. Reset in `loadHistory()` and `clear()`. +- Add derived helpers: `revert(): SessionRevertDto?` and `revertedCount()` (count of user messages with `id >= boundary`, mirroring `session.tsx`). +- Transcript hiding: expose whether a message id is hidden by the boundary (`id >= revert.messageID`, with partID partial-turn handling), applied both to newly-arriving messages and on `RevertChanged`. +- Follow the AGENTS.md "Adding a New Event" checklist (event subclass → model field + mutation + resets → controller handling → `-> Unit` stubs in exhaustive `when` blocks). + +### 6. Frontend views + +- New `session/ui/RevertBanner.kt` Swing view, built once and mutated via `update()` per retained-Swing rules. Listens to `RevertChanged` + `DiffUpdated`. Contents: + - arrow-left icon + count label (localized count.one/other), + - per-file diff rows reusing `ai.kilocode.client.ui.DiffStatBadge` fed from `model.diff` (`DiffFileDto.additions/deletions`), + - Redo button → `controller.redo()`, Redo All (only when count > 1) → `controller.redoAll()`, + - hint label (same new wording as Part A). + - Placed in `session/ui/SessionView.kt` at the bottom of the transcript, above the prompt input; shown only when `revert != null`. +- Per-user-message rollback control in `session/views/MessageView.kt`: user prompt bubbles already carry a hover `promptToolbar` (`MessageToolbar` with copy) + `promptHover` state. Add a rollback `HoverIcon` (arrow-left) alongside copy for user messages, wired through `SessionView → controller.revert(msgId)`. Disable/hide while the agent is busy or when there are no assistant messages after it, with the `revert.disabled.agentBusy` tooltip — mirroring `VscodeSessionTurn` `onRevert` + `data-revert-disabled`. Extend `MessageToolbar.kt` to host the extra action (or add a sibling control). +- Apply transcript hiding: `SessionView`/`TurnView` toggle `MessageView` visibility on `RevertChanged` (prefer visibility toggle over rebuild, per Swing lifecycle guidance). + +### 7. i18n (`frontend/src/main/resources/messages/KiloBundle*.properties`) + +- Add to base `KiloBundle.properties`: `revert.banner.count.one`, `revert.banner.count.other`, `revert.banner.redo`, `revert.banner.redo.all`, `revert.banner.hint` (new wording), `revert.disabled.agentBusy`. +- Other locale bundles fall back to base until translated; do not add stale translations. + +### 8. Tests + +- Backend: extend `KiloSessionRpcApiImplTest` / a `KiloBackendChatManager` test against `MockCliServer` asserting the exact HTTP path + JSON body for `revert`/`unrevert`, and that a session carrying `revert` deserializes into `SessionRevertDto`. +- Frontend controller (`SessionControllerTestBase` + `FakeSessionRpcApi`, add revert/unrevert call tracking): `revert()`/`unrevert()`/`redo()`/`redoAll()` hit the right RPC; `RevertChanged` fires; count logic; reverted messages hidden and restored; prompt-text restore on revert / clear on full redo. +- Frontend views (`BasePlatformTestCase`): `RevertBanner` renders count + per-file `DiffStatBadge` + redo buttons (Redo All only when count > 1); `MessageView` shows the rollback control on user messages and disables it while busy; retained-component assertions (`assertSame`, bounded count) per Swing lifecycle rules. + +## Deliverables + +| Area | Files | +|---|---| +| VS Code string | `webview-ui/src/i18n/en.ts` + 19 non-English locale hints reset | +| Shared | `SessionDto.kt` (+`SessionRevertDto`), `KiloSessionRpcApi.kt` | +| Backend | `KiloBackendChatManager.kt`, `KiloCliDataParser`, `KiloSessionRpcApiImpl.kt` | +| Frontend service/controller/model | `KiloSessionService.kt`, `SessionController.kt`, `SessionModel.kt`, `SessionModelEvent.kt` | +| Frontend views | new `RevertBanner.kt` (reusing `DiffStatBadge`), `MessageView.kt`, `MessageToolbar.kt`, `SessionView.kt`, `PromptPanel.kt` | +| i18n | `KiloBundle.properties` | +| Tests | backend RPC/chat-manager, frontend controller + view tests, `FakeSessionRpcApi` | + +## Validation + +- VS Code string change: `bun run typecheck` from repo root (or from `packages/kilo-vscode/`). +- JetBrains: `./gradlew typecheck` and `./gradlew test` from `packages/kilo-jetbrains/` (Java 21). +- Manual: `./gradlew runIde` (or split mode) to exercise rollback → redo → redo-all and reverted-transcript hiding. + +## Risks / Notes + +- Confirm no `kilocode_change` markers are needed for the VS Code i18n edits (Kilo-owned webview path — none required). +- Reset (not delete) is chosen for VS Code locale hints, so the fallback path is not relied upon; flag the 19 files for re-translation. +- Keep new Kotlin identifiers single-word per repo style; annotate EDT/model-touching methods with `@RequiresEdt`; wrap frontend RPC in `durable {}`. diff --git a/.kilo/plans/1783550396285-jetbrains-rollback-hardening.md b/.kilo/plans/1783550396285-jetbrains-rollback-hardening.md new file mode 100644 index 00000000000..fe9efd49150 --- /dev/null +++ b/.kilo/plans/1783550396285-jetbrains-rollback-hardening.md @@ -0,0 +1,199 @@ +# JetBrains Rollback Feature — Hardening Plan + +Harden the new JetBrains rollback/redo feature: (1) communicate when only messages were +reverted because snapshots are disabled, and (2–4) close the code-duplication, testing, +end-to-end, and AGENTS-conformance gaps. + +All work is inside `packages/kilo-jetbrains/`. No shared `packages/opencode/` files change, +so no `kilocode_change` markers are needed. + +## Context an implementer needs + +- Revert flow: user clicks the per-message Rollback button (`MessageToolbar`/`MessageView`) + → `SessionController.revert()` aborts a busy turn then POSTs `/session/:id/revert` + → CLI sets a revert marker → `SessionUpdated` event → `SessionModel.setRevert()` → + `RevertBanner` shows + reverted messages hidden. `redo()`/`redoAll()` drive + `/session/:id/revert` (next user msg) and `/session/:id/unrevert`. +- Snapshot signal: `SessionRevertDto.snapshot` (`shared/.../rpc/dto/SessionDto.kt:22`) is + `null` exactly when the server did NOT restore files (snapshots disabled: `config.snapshot + === false`, non-git worktree, or ACP). It is already parsed and reaches the frontend via + `SessionModel.revert()`. When `snapshot == null` the diff is also empty, so the banner shows + no file rows today. This is the reliable, no-new-API signal for "history only". +- Test harnesses already exist: frontend `SessionControllerTestBase` + `FakeSessionRpcApi` + (already tracks `rpc.reverts` and `rpc.unreverts`); backend `MockCliServer` + + `KiloBackendChatManagerTest`. Banner tests instantiate `RevertBanner(model, redo, redoAll)` + and read components via `components(banner)`. + +## Key files + +- `frontend/.../session/ui/RevertBanner.kt` +- `frontend/.../session/controller/SessionController.kt` (`revert`, `unrevert`, `redo`, `redoAll`, `capture`, `sessionProps`) +- `frontend/.../session/model/SessionModel.kt` (`revert()`, `revertedCount()`, `isRevertedMessage()`) +- `frontend/src/main/resources/messages/KiloBundle.properties` (+ translated `KiloBundle_*.properties`) +- `backend/.../app/KiloBackendChatManager.kt` (`revert`/`unrevert`), `backend/.../cli/KiloCliDataParser.kt` (`buildRevertJson`) +- `backend/.../app/KiloBackendSessionManager.kt` (`revertDto` duplication) +- Tests: `frontend/.../session/controller/TurnLifecycleTest.kt` (or new `RevertFlowTest.kt`), + `frontend/.../session/ui/SessionMessageListPanelTest.kt`, + `frontend/.../session/model/SessionModelTest.kt`, + `backend/.../testing/MockCliServer.kt`, `backend/.../app/KiloBackendChatManagerTest.kt`, + `backend/.../cli/KiloCliDataParserTest.kt` + +## Resolved decisions + +- **Do NOT hide/disable the Rollback button when snapshots are off.** This matches VS Code and + keeps history-only revert usable. We only add messaging. +- **Detect "files not restored" via `model.revert()?.snapshot == null`.** No new config/API, + no shared-code change. +- **Message it in the `RevertBanner`** with a distinct localized line, shown only when + `snapshot == null`. +- Proactively warning on the button before a revert (needs `config.snapshot`/`vcs` exposed to + the frontend) is **out of scope**. + +--- + +## Phase 1 — Communicate history-only revert (snapshots disabled) + +1. Add bundle key in `KiloBundle.properties` (place near the other `revert.banner.*` keys): + - `revert.banner.filesNotRestored=Snapshots are off — only the conversation was reverted; your files were not changed.` + - Add the same key to every translated `KiloBundle_*.properties` file. If translations are + not available, copy the English value (mirror how the other `revert.banner.*` keys were + seeded across bundles) so no key is missing. +2. In `RevertBanner.kt`: + - Add a retained `JBLabel` field `notice` styled like `hint` (`JBFont.small()`), created + once in `init` and added to `body` after `hint`. + - In `update()`, after computing `revert`, set + `notice.isVisible = revert.snapshot == null` and its text to + `KiloBundle.message("revert.banner.filesNotRestored")`. + - Color it via a theme API in `applyStyle` (use `UIUtil.getContextHelpForeground()` to match + the secondary `hint`; do not hardcode a color). Re-evaluate in `applyStyle` alongside + `hint.foreground`. + - Keep the existing empty-diff behavior (no file rows) — the notice replaces the missing + information rather than adding chrome. +3. Remove the redundant `isOpaque = false` on the `body` and `files` `Stack`s (see Phase 4); + do this while editing the file. + +## Phase 2 — Close code-duplication gaps + +4. In `KiloBackendSessionManager.kt`, collapse the two identical `revertDto(...)` overloads + (`SessionRevert` and `GlobalSessionRevert`) into a single private helper that maps the four + fields (`messageID`, `partID`, `snapshot`, `diff`) from a small shared shape. Simplest + approach: one private `fun revertDto(messageID: String?, partID: String?, snapshot: String?, + diff: String?)` returning `SessionRevertDto?` (null when `messageID` is null), and have both + call sites extract fields from their respective model type. Keep behavior identical; this is + a pure refactor covered by existing `KiloBackendSessionManagerTest` parsing assertions. + +## Phase 3 — Fill testing + end-to-end gaps + +### Frontend controller/model logic + +5. `SessionModelTest.kt` — add direct model tests: + - `revertedCount()` returns the number of user messages from the marker to the end + (build entries `u1,a1,u2,a2`; marker at `u1` → 2; marker at `u2` → 1; marker id absent → 0). + - `isRevertedMessage()` true for the marker message and everything after, false before. + - `snapshot`-null marker still yields a non-zero `revertedCount()` (history-only case). +6. `TurnLifecycleTest.kt` (or new `RevertFlowTest.kt`) — add controller tests using `prompted()`: + - `redo()` reverts to the NEXT user message: seed `u1,a1,u2,a2`, apply marker at `u1`, + `edt { m.redo() }`, flush → assert `rpc.reverts` last call targets `u2`. + - `redo()` at the last user message calls unrevert: marker at `u2` → `redo()` → assert + `rpc.unreverts` recorded and no new revert call. + - `redoAll()` calls unrevert: assert `rpc.unreverts` contains `("ses_test","/test")`. + - `unrevert()` clears via RPC: assert `rpc.unreverts` populated. + +### Frontend banner → controller wiring (end-to-end within the UI layer) + +7. `SessionMessageListPanelTest.kt` — add wiring + history-only tests: + - Redo button click invokes the controller: build `RevertBanner(model, redo = { redo++ }, + redoAll = { all++ })`, set a marker, `banner...first { text == redo }.doClick()` → assert + the redo lambda fired; same for Redo All when `revertedCount > 1`. + - History-only notice: marker with `SessionRevertDto("u1", snapshot = null)` → assert the + `revert.banner.filesNotRestored` label is visible and no file rows are present. + - Snapshot-present: marker with `snapshot = "snap1"` → assert the notice label is hidden. + +### Full round-trip end-to-end (frontend) + +8. Add one `RevertFlowTest` case that exercises the whole loop through `SessionControllerTestBase`: + - Seed a multi-turn session, `edt { m.revert(userMsgId) }`, flush → assert busy-abort + ordering + `rpc.reverts`. + - `emit(SessionUpdated(..., revert = SessionRevertDto(userMsgId, snapshot = "s1")))` → + assert `model.isRevertedMessage(userMsgId)` and reverted views hidden. + - Drive Redo All via `m.redoAll()`; `emit(SessionUpdated(..., revert = null))` → assert + messages restored (`isRevertedMessage` false) and marker cleared. + Use existing `emit`/`flush` helpers; no timeouts. + +### Backend HTTP contract + +9. `MockCliServer.kt` — add routing + capture fields (the current generic `/session/ses_*` + regexes are full-match and won't catch the `/revert` suffix, so new cases are safe): + - Fields: `lastRevertPath`, `lastRevertBody`, `lastUnrevertPath`, `lastUnrevertBody` + (`@Volatile var ... : String? = null`). + - Cases (place beside the `summarize`/`prompt_async` cases): + - `bare.matches(Regex("/session/ses_[^/]+/revert")) && method == "POST"` → store path+body, + `respond(output, 200, sessionCreate)`. + - `bare.matches(Regex("/session/ses_[^/]+/unrevert")) && method == "POST"` → store path+body, + `respond(output, 200, sessionCreate)`. +10. `KiloBackendChatManagerTest.kt` — mirror the existing `compact posts summarize request` test: + - `revert posts messageID and partID to revert endpoint`: call + `chat.revert("ses_abc", "/test/project", "msg1", "prt1")`; assert + `requestCount("/session/ses_abc/revert") == 1`, path starts with + `/session/ses_abc/revert?directory=`, body == `{"messageID":"msg1","partID":"prt1"}`. + - `revert omits partID when null`: body == `{"messageID":"msg1"}`. + - `unrevert posts empty body to unrevert endpoint`: body == `{}` and path prefix asserted. +11. `KiloCliDataParserTest.kt` — add `buildRevertJson` unit tests: + - `buildRevertJson("m1", null) == {"messageID":"m1"}`. + - `buildRevertJson("m1", "p1") == {"messageID":"m1","partID":"p1"}`. + - Escaping: an id containing a quote/backslash is JSON-escaped (reuse the parser's `escape`). + +## Phase 4 — AGENTS conformance cleanup + +12. Remove the unused bundle key `revert.disabled.agentBusy` from `KiloBundle.properties` and all + translated bundles (grep confirms it is referenced nowhere in Kotlin). Alternatively, if the + team wants VS Code parity, wire it as a disabled-state tooltip on the Rollback button — but + the recommended, lower-risk action is removal, since JetBrains intentionally aborts the busy + turn instead of disabling the button. +13. Remove redundant `isOpaque = false` on the transparent `Stack`s in `RevertBanner.kt` + (`body`, `files`) — `Stack` is already a no-color panel. Keep `isOpaque = false` on the + `BorderLayoutPanel` root only if it visibly overlays the session surface; otherwise remove it + too and rely on the default. +14. Add usage metrics for the new user-facing actions in `SessionController.kt`, using the + existing `capture(event, props)` + `sessionProps(id)` helpers: + - In `revert()` (on the success path, after `sessions.revert`): `capture("Session Rollback", + sessionProps(id))`. + - In `redo()`/`redoAll()`/`unrevert()`: `capture("Session Redo", sessionProps(id))` / + `capture("Session Redo All", ...)` as appropriate. Keep names consistent with existing + event naming; do not add PII. + +## Out of scope + +- Exposing `config.snapshot`/`vcs` to the frontend to proactively warn before a revert. +- Changing the retained-vs-rebuild strategy of `RevertBanner.files` (low-frequency update; + acceptable as-is). +- Replacing the full-project `ACTION_SYNCHRONIZE` in `synchronizeFromDisk` with a scoped VFS + refresh (only revisit if it shows up as a perf problem). +- Any VS Code webview parity changes beyond the shared i18n strings already present. + +## Validation + +Run from `packages/kilo-jetbrains/`: + +- `./gradlew typecheck` — compiles all Kotlin including generated client. +- `./gradlew test` — runs frontend + backend suites, including the new tests. Requires Java 21; + only check Java if Gradle reports a Java error. +- Targeted while iterating: run the touched test classes (`TurnLifecycleTest`/`RevertFlowTest`, + `SessionMessageListPanelTest`, `SessionModelTest`, `KiloBackendChatManagerTest`, + `KiloCliDataParserTest`). + +Manual smoke (optional, `./gradlew runIde`): trigger a rollback in a non-git directory (or with +`snapshot: false`) and confirm the banner shows the "files not changed" notice with no file rows; +trigger one in a git repo and confirm the notice is hidden and file rows appear. + +## Changeset + +Add `.changeset/.md` (`"@kilocode/kilo-jetbrains": patch`) describing the user-facing +change, e.g. "Clarify in JetBrains rollback that only the conversation was reverted when +snapshots are disabled." Test-only and refactor tasks do not need their own changeset. + +## Suggested order + +Phase 3 backend tests (9–11) and model tests (5) are independent and can be done first to lock +behavior. Then Phase 1 (user-facing messaging) with its banner tests (7). Then Phase 2 refactor, +Phase 4 cleanups, controller tests (6, 8), and the changeset last. diff --git a/.kilo/plans/1784057744472-jetbrains-custom-provider-dialog.md b/.kilo/plans/1784057744472-jetbrains-custom-provider-dialog.md new file mode 100644 index 00000000000..e3d9eee31a4 --- /dev/null +++ b/.kilo/plans/1784057744472-jetbrains-custom-provider-dialog.md @@ -0,0 +1,341 @@ +# JetBrains custom provider: close on OK, select, edit, edit/disconnect buttons + +## Goal + +Fix the JetBrains "Add custom provider" flow and add edit support: + +1. Confirming the dialog closes it (OK label "Add" for add, "Save" for edit). +2. The added/edited provider becomes the selected row in the list. +3. Double-clicking an editable custom provider opens it for editing instead of disconnecting it. +4. Editable custom providers show both **Edit** and **Disconnect** action buttons. + +All work is in `packages/kilo-jetbrains/frontend/`. No backend, RPC, or DTO changes. `saveCustom` already upserts, and `ProviderSettingsDto.config[id]` already carries the fields needed to prefill an edit. No `kilocode_change` markers are needed (all files are under Kilo-owned `packages/kilo-jetbrains/`). + +## Root causes (verified in code) + +- **Dialog does not close**: `CustomProviderDialog.syncActions()` sets `isOKActionEnabled = !saving && !fetching && modelIds().isNotEmpty()` (ProvidersSettingsUi.kt:957). On save success `doOKAction()` sets `outcome` and calls `closeOk()` → `super.doOKAction()` (line 966), which only closes when `getOKAction().isEnabled()` is true. `saving` stays `true`, so OK is disabled and nothing closes even though the log prints "closing dialog". +- **Double-click deletes**: `SettingsListView.mouseClicked` fires `primary(item)` on non-button double-clicks (SettingsListView.kt:65-72), and `primary` runs the first enabled cell. A configured custom provider only exposes `DISCONNECT` (ProviderListRows.kt:90), so double-click disconnects. +- **Edit feasibility**: `KiloBackendProviderSettingsManager.saveCustom` patches config as an upsert and only touches auth when a key/env is supplied (lines 154-166); a blank key on edit preserves the stored secret. Prefill source is `state.config[id]`: `name`, `options["baseURL"]`, `env` (first entry), `models` (model ids). The API key is a secret and is never returned, so the key field starts blank on edit. +- OK label is already `settings.providers.customAdd=Add`; edit mode needs a new "Save" label. + +## Exact edits + +### 1. `packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties` + +Add `settings.providers.edit=Edit` right after line 456 (`settings.providers.disconnect=Disconnect`): + +``` +settings.providers.edit=Edit +``` + +Add two keys in the custom block (after line 470 `customTitle` / line 471 `customAdd`): + +``` +settings.providers.customEditTitle=Edit OpenAI-Compatible Provider +settings.providers.customSave=Save +``` + +Only edit the base `KiloBundle.properties`; other locale files fall back to base for missing keys. + +### 2. `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProviderCatalog.kt` + +Add a helper at the end of the file (after `configured(...)`, line 99): + +```kotlin +internal fun customEditable(provider: ProviderSettingsProviderDto, state: ProviderSettingsDto) = + state.config[provider.id]?.npm == CUSTOM_PROVIDER_PACKAGE +``` + +`CUSTOM_PROVIDER_PACKAGE` (`"@ai-sdk/openai-compatible"`) is already defined at line 19 and is the same signal the backend disconnect path uses. + +### 3. `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProviderListRows.kt` + +Add `EDIT` to the enum (line 12-17): + +```kotlin +internal enum class ProviderListAction { + CONNECT, + OAUTH, + EDIT, + DISCONNECT, + ENABLE, +} +``` + +Update `cells` (lines 35-43) to mark EDIT primary and keep both EDIT and DISCONNECT visible when connected: + +```kotlin + override val cells: List + get() = actions.map { action -> + SettingsListCell( + action.name, + providerListActionText(action), + enabled(action), + alwaysVisible = (action == ProviderListAction.DISCONNECT || action == ProviderListAction.EDIT) && connected, + primary = action == ProviderListAction.EDIT, + ) + } +``` + +`enabled(action)` (line 45) already returns true for EDIT (`action != DISCONNECT`), no change. + +Add the EDIT label to `providerListActionText` (lines 48-53): + +```kotlin + ProviderListAction.EDIT -> KiloBundle.message("settings.providers.edit") +``` + +Update `providerActions` (lines 83-96) so configured editable custom providers expose EDIT + DISCONNECT: + +```kotlin +internal fun providerActions( + provider: ProviderSettingsProviderDto, + state: ProviderSettingsDto, + disabled: Set = state.disabled.toSet(), +): List { + if (provider.id in disabled) return listOf(ProviderListAction.ENABLE) + if (provider.id == KILO_PROVIDER_ID && configured(provider, state, state.connected.toSet())) return emptyList() + if (configured(provider, state, state.connected.toSet())) { + return if (customEditable(provider, state)) { + listOf(ProviderListAction.EDIT, ProviderListAction.DISCONNECT) + } else { + listOf(ProviderListAction.DISCONNECT) + } + } + val methods = providerMethods(provider, state) + return buildList { + if (methods.any { it.type == "oauth" }) add(ProviderListAction.OAUTH) + if (methods.any { it.type == "api" }) add(ProviderListAction.CONNECT) + } +} +``` + +### 4. `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUi.kt` + +**4a. Import.** Add to the settings.base imports (near line 7): + +```kotlin +import ai.kilocode.client.settings.base.SettingsListSelection +``` + +**4b. `view` field (line 196).** Pass the new edit callback: + +```kotlin + private val view = ProvidersContent(::connect, ::oauth, ::disconnect, ::enable, ::edit) +``` + +**4c. Replace `custom()` (lines 314-330)** with add + edit + shared open: + +```kotlin + @RequiresEdt + private fun custom() { + checkEdt() + openCustomDialog(null) + } + + @RequiresEdt + private fun edit(provider: ProviderSettingsProviderDto) { + checkEdt() + val cfg = state.config[provider.id] ?: return + openCustomDialog( + CustomProviderEdit( + id = provider.id, + name = cfg.name ?: provider.name, + baseUrl = cfg.options["baseURL"].orEmpty(), + envVar = cfg.env.firstOrNull(), + models = cfg.models.values.map { it.id }, + ), + ) + } + + // The dialog performs the save itself so failures can be shown inline and the user can + // correct their input without re-typing. It only closes on a verified success. + @RequiresEdt + private fun openCustomDialog(existing: CustomProviderEdit?) { + checkEdt() + val dialog = CustomProviderDialog( + cs, + directory, + { service().fetchCustomModels(it) }, + { service().saveCustom(it) }, + existing, + ) + if (!dialog.showAndGet()) return + val next = dialog.outcome ?: return + state = next + view.update(next, dialog.savedId) + clearProgress() + } +``` + +**4d. `ProvidersContent` (lines 520-591).** Add the `edit` callback and handle EDIT + selection. + +Constructor (lines 520-525): + +```kotlin +internal class ProvidersContent( + private val connect: (ProviderSettingsProviderDto) -> Unit, + private val oauth: (ProviderSettingsProviderDto) -> Unit, + private val disconnect: (ProviderSettingsProviderDto) -> Unit, + private val enable: (ProviderSettingsProviderDto) -> Unit, + private val edit: (ProviderSettingsProviderDto) -> Unit, +) : BaseContentPanel() { +``` + +`update` (lines 536-545) — add optional `select`: + +```kotlin + @RequiresEdt + fun update(state: ProviderSettingsDto, select: String? = null) { + checkEdt() + val notes = state.providers.count { providerDescription(it).isNotBlank() } + ProvidersSettingsUi.LOG.info("provider settings content update: start providers=${state.providers.size} connected=${state.connected.size} disabled=${state.disabled.size} descriptions=$notes") + this.state = state + val rows = providerListRows(state, "", disabledRows = busy) + if (select != null) view.update(rows, SettingsListSelection.Key(select)) else view.update(rows) + ProvidersSettingsUi.LOG.info("provider settings content update: completed rows=${rows.size}") + } +``` + +`activate` `when` block (lines 580-585) — add EDIT: + +```kotlin + when (action) { + ProviderListAction.CONNECT -> connect(row.provider) + ProviderListAction.OAUTH -> oauth(row.provider) + ProviderListAction.DISCONNECT -> disconnect(row.provider) + ProviderListAction.ENABLE -> enable(row.provider) + ProviderListAction.EDIT -> edit(row.provider) + } +``` + +**4e. `CustomProviderDialog`.** Add the edit-prefill data class, constructor param, prefill, `savedId`, edit-aware title/button, and the close fix. + +Add near the dialog (e.g. above `internal class CustomProviderDialog`): + +```kotlin +internal data class CustomProviderEdit( + val id: String, + val name: String, + val baseUrl: String, + val envVar: String?, + val models: List, +) +``` + +Constructor (lines 645-650) — add `existing` as the last param (keeps existing positional test calls valid): + +```kotlin +internal class CustomProviderDialog( + private val cs: CoroutineScope, + private val directory: String, + private val fetch: suspend (CustomModelFetchDto) -> CustomModelFetchResultDto, + private val save: suspend (CustomProviderSaveDto) -> ProviderActionResultDto, + private val existing: CustomProviderEdit? = null, +) : DialogWrapper(true) { +``` + +Add the `savedId` property next to `outcome` (lines 667-669): + +```kotlin + // Set once the save succeeds; the panel reads it after the dialog closes to update the list. + var outcome: ProviderSettingsDto? = null + private set + + // Id of the provider the save persisted; used to select the row after the dialog closes. + var savedId: String? = null + private set +``` + +`init` (lines 671-686) — edit-aware title/button and prefill: + +```kotlin + init { + title = if (existing != null) { + KiloBundle.message("settings.providers.customEditTitle") + } else { + KiloBundle.message("settings.providers.customTitle") + } + setOKButtonText( + if (existing != null) KiloBundle.message("settings.providers.customSave") + else KiloBundle.message("settings.providers.customAdd"), + ) + init() + initValidation() + existing?.let { prefill(it) } + models.document.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + syncActions() + } + }) + pick.addActionListener { + if (fetching) cancelFetch() + else selectModels() + } + syncActions() + } + + @RequiresEdt + private fun prefill(edit: CustomProviderEdit) { + id.text = edit.id + id.isEditable = false + name.text = edit.name + url.text = edit.baseUrl + env.text = edit.envVar.orEmpty() + models.text = edit.models.joinToString(", ") + } +``` + +Save-success branch in `doOKAction` (lines 767-769) — reset `saving`, re-enable OK, record id, close unconditionally: + +```kotlin + ProvidersSettingsUi.LOG.info("custom provider add: save succeeded id='${input.id}', closing dialog") + outcome = result.state + savedId = input.id + saving = false + syncActions() + close(OK_EXIT_CODE) +``` + +Remove the now-unused `closeOk()` helper (line 966), or replace its body with `close(OK_EXIT_CODE)`; do not leave `super.doOKAction()` in the success path. `OK_EXIT_CODE` is a `DialogWrapper` constant available to the subclass without an import. + +## Tests — `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUiTest.kt` + +Run against the real EDT/component tree with `FakeProviderRpcApi` (no mocks). + +**Update existing tests broken by the new EDIT action / callback:** + +- `content()` helper (line 965): change to `edt { ProvidersContent({}, {}, {}, {}, {}) }` (5 lambdas). +- `test configured custom provider exposes only disconnect` (lines 306-324): rename to reflect edit + disconnect and assert `assertEquals(listOf(ProviderListAction.EDIT, ProviderListAction.DISCONNECT), row.actions)`. +- `test source custom catalog providers remain visible while configured custom providers are connected` (line 412): change the `rows[0].actions` assertion to `listOf(ProviderListAction.EDIT, ProviderListAction.DISCONNECT)`. + +**Add new tests:** + +- Dialog closes on save success: reuse the `submit` + `flushUntil { edt { dialog.outcome != null } }` pattern; after success assert `dialog.savedId == "my-openai"` and `edt { dialog.isOKActionEnabled }` is `true` (proves the disabled-OK regression is fixed). +- Edit prefill: build `CustomProviderDialog(cs, "/tmp", { CustomModelFetchResultDto(listOf("gpt-4o")) }, { ProviderActionResultDto(...) }, CustomProviderEdit("my-openai", "My OpenAI", "https://example.com/v1", null, listOf("gpt-4o")))`. Assert the text fields (indices 0/1/2/4/5 for id/name/url/env/models) are populated, the key field (index 3) is blank, `id` field `isEditable == false`, and the OK button text equals `KiloBundle.message("settings.providers.customSave")`. (Field order via `filterIsInstance()`: 0=id, 1=name, 2=url, 3=key, 4=env, 5=models — `JBPasswordField` is a `JTextField`.) +- `providerActions` editable: given a provider with `config[id].npm == "@ai-sdk/openai-compatible"` and configured, assert `[EDIT, DISCONNECT]`, and that the EDIT cell has `primary == true` (via `row.cells.first { it.id == "EDIT" }.primary`). +- Non-editable connected provider unchanged: a connected non-custom provider (e.g. `anthropic`, connected) still yields `[DISCONNECT]`. +- Edit callback fires instead of disconnect on primary: construct `ProvidersContent` with recording lambdas (capture which provider each callback got), update with a configured editable provider, trigger the primary path, and assert the edit lambda ran and disconnect did not. +- Selection after update: `content.update(state, select = "local-openai")` then assert the list's selected row key is `"local-openai"`. + +## Validation + +Run from `packages/kilo-jetbrains/`: + +- `bun run typecheck` (or `./gradlew typecheck`). +- `./gradlew test` (target `ProvidersSettingsUiTest` first if iterating). +- Manual smoke via `./gradlew runIde`: add a custom provider → dialog closes and the new row is selected; double-click the row → edit dialog opens prefilled with the id field locked and "Save" button; the connected custom row shows both Edit and Disconnect. + +Java 21 is required for Gradle; only check `java -version` if a Gradle command fails with a Java error. + +## Notes / risks + +- API key is intentionally not prefilled on edit (secret). Blank key preserves the stored key; a new value updates it — matches `saveCustom` behavior. +- The id field is locked on edit because the id is the config key; renaming would create a second provider. +- Double-click on a visible action button (Edit/Disconnect) still routes through the single-cell click path, not `primary`; only double-clicks on the non-button row area trigger the primary (EDIT) action. +- Keep new identifiers single-word where clear (`existing`, `cfg`, `select`, `edit`, `prefill`, `savedId`). + +## Out of scope + +- Editing headers, per-model capabilities (reasoning), or config scope (global vs workspace) beyond the current add dialog. +- Any changes to shared `packages/opencode/` code, RPC contracts, or DTOs. diff --git a/.kilo/plans/1784058434633-consolidate-jetbrains-model-pickers.md b/.kilo/plans/1784058434633-consolidate-jetbrains-model-pickers.md new file mode 100644 index 00000000000..a807f0ed63a --- /dev/null +++ b/.kilo/plans/1784058434633-consolidate-jetbrains-model-pickers.md @@ -0,0 +1,198 @@ +# Consolidate JetBrains model pickers into a shared PickerPopup base + +## Goal + +Remove duplication between the two list-picker popups in the JetBrains frontend by +extracting a generic base, and make both consume it: + +1. Prompt model picker — `session/ui/model/ModelPicker.kt` (`showPopup`) — single-select + commit, search field, expand/collapse details panel, sections, per-row favorite (star) + toggle. +2. Custom provider model picker — `settings/providers/ProvidersSettingsUi.kt` + (`showModelPopup` inside `CustomProviderDialog`) — multi-select toggle, no search, no + details, selection stored in the `models` text field. + +The base must expose configurable options; the provider picker enables: multi-select, no +expanded/details view, and extra header toolbar buttons ("Select All" / "Deselect All"). + +All work is under `packages/kilo-jetbrains/frontend/`. No backend/RPC/DTO changes. Files are +Kilo-owned (no `kilocode_change` markers). + +## Resolved decisions + +- **Approach**: Extract a generic `PickerPopup` base; both pickers become thin configs. +- **Selection state**: Caller owns it. Base is selection-stateless: it calls `checked(row)` + to render check state and `onPrimary(row)` on activation. Provider keeps the `models` text + field as source of truth; prompt keeps its favorites callbacks. +- **Select All / Deselect All**: Two caller-supplied header toolbar buttons. Remove the + in-list `CustomModelRow.selectAll` row entirely. +- **Renderer**: Extract a base renderer skeleton too; `ModelPickerRenderer` and the provider + renderer extend it. Generalize the favorite hit-zone into a trailing click-zone helper. +- **Location**: New package `ai/kilocode/client/ui/picker/` with `PickerPopup` and + `PickerListRenderer`. `PickerRow` and `ModelSearch` stay where they are and are reused. + +## Base design + +### `ui/picker/PickerPopup.kt` — generic popup + +Owns: `JBList` + `CollectionListModel`, `createComponentPopupBuilder` with the shared +flags (requestFocus/focusable/cancelOnClickOutside/cancelKeyEnabled/cancelOnWindowDeactivation/ +locateWithinScreenBounds, non-resizable, non-movable), `popupBackground` helper (move the +duplicated `NewUI -> Popup.BACKGROUND else getListBackground()` here), header assembly, +mouse hit-testing, keyboard bindings, auto-select-on-move + scrolling install, and size +computation. + +Config (constructor params / vars, defaults match current behavior): + +- `anchor: JComponent`, `placement: Placement` (`ABOVE` / `BELOW` / `UNDERNEATH`). Prompt uses + `PopupShowOptions.aboveComponent` / `showUnderneathOf`; provider uses `showUnderneathOf(pick)`. +- `rows: (query: String) -> List` — rebuilt when the search text changes. Provider passes a + function that ignores `query`. +- `renderer: PickerListRenderer`. +- `checked: (T) -> Boolean` — drives the row check icon (single: `row.key == active`; multi: + `isSelected(row)`). +- `sectionTitle: (rows: List, index: Int) -> String?` — optional; prompt supplies + `modelPickerSectionTitle`, provider passes `{ _, _ -> null }`. +- `mode: Mode` = `Single` | `Multi`. + - Single: on primary click/ENTER call `onPrimary(row)` then `popup.closeOk(null)`. + - Multi: on primary click/SPACE/ENTER call `onPrimary(row)`, repaint, stay open. +- `onPrimary: (T) -> Unit`. (Prompt decides activate-vs-clear inside based on `row.item == null`; + provider toggles membership in the text field.) +- Optional trailing toggle: `trailingHit: (list, bounds, point) -> Boolean` and + `onTrailing: (T) -> Unit`. Prompt supplies favorite hit-zone + favorite toggle; provider omits. + Keyboard: in Single mode `Shift+SPACE` triggers trailing when present. +- `search: Boolean` — show `SearchTextField` in header CENTER. Prompt true; provider false. +- `toolbar: List` — extra header buttons placed in header WEST. Provider passes + Select All / Deselect All; prompt passes empty. +- Optional details/expand: `details: JComponent?` + `onPreview: (T?) -> Unit` + + `expandStateKey: String?`. When non-null, base shows the expand `HoverIcon`, EAST details + panel, persists expanded state via `PropertiesComponent`, reserves details width when + expanded, and `Disposer.register(popup, details)` if `details is Disposable`. Provider passes + null (no expand toggle, no details). +- Sizing: `minWidth`, `maxWidth`, `maxVisibleRows`, `emptyListHeight` (defaults = current + ModelPicker constants 420/760/10/120). Provider passes `minWidth = 320` and a `maxWidth` + that preserves today's look. Reuse the existing `computeInitialPopupSize` / + `computeListPreferredWidth` / `computeListPreferredHeight` logic, moved into the base and + parameterized by these constants + optional details width. + +Header layout: `BorderLayout` with `toolbar` row WEST, `search` CENTER (empty when +`search=false`), expand `HoverIcon` EAST (only when details present). Preserve +`AbstractPopup.customizeSearchFieldLook` and background wiring. + +Keyboard (registered on both the list and, when present, the search editor): + +- `UP`/`DOWN` → move selection (search field only; list uses `ScrollingUtil`). +- `ENTER` → primary on selected row. +- `ESC` → `popup.cancel()`. +- Single + trailing present → `Shift+SPACE` → trailing on selected row. +- Multi → `SPACE` → primary (toggle) on selected row. + +Mouse (`mouseReleased`, `UIUtil.isActionClick`): resolve row via `locationToIndex` + +`getCellBounds`/`contains`; if trailing present and `trailingHit` → `onTrailing` + consume; +else `onPrimary` (Single closes, Multi consumes + repaints). + +### `ui/picker/PickerListRenderer.kt` — renderer skeleton + +Abstract `ListCellRenderer` base: `PickerRow` wrap + optional top `GroupHeaderSeparator` +(driven by `sectionTitle`) + `[check icon | content | trailing]` layout with the shared +transparent row + `UiStyle.Gap.md/lg/md/pad` insets. Provides: + +- `check` icon column (`AllIcons.Actions.Checked` / `EmptyIcon`), set from `checked(row)`. +- protected `content: JComponent` slot and optional `trailing: JComponent` slot. +- section separator top panel wiring (`top`/`sep` from `ModelPickerRenderer`). +- a companion `trailingClickZone(list, bounds, point, width)` generalizing + `ModelPickerRenderer.isFavoriteClick` (keep `FAVORITE_CLICK_AREA_WIDTH` behavior). + +## Ordered implementation tasks + +1. **Create `ui/picker/PickerListRenderer.kt`**: base renderer skeleton with check column, + `PickerRow` wrap, top section separator, content/trailing slots, transparent row + insets, + and the `trailingClickZone` helper (moved/generalized from `ModelPickerRenderer.isFavoriteClick` + / `favoriteInset`). +2. **Create `ui/picker/PickerPopup.kt`**: generic popup per the design above, including the + moved `popupBackground` helper and the sizing helpers (parameterized). +3. **Refactor `ModelPickerRenderer`** to extend `PickerListRenderer`: content = title + (`SimpleColoredComponent`) + warn + free/BYOK badges + provider label; trailing = favorite + star; check via `checked`. Keep existing internal test accessors (`starIcon`, + `badgeVisible`, `badgeText`, `byokVisible`, `warningVisible`, `warningTooltip`) and the + `DATA_COLLECTED`/`checked`/`empty` companion members so `ModelPickerTest` compiles unchanged. +4. **Refactor `ModelPicker.showPopup`** to build a `PickerPopup` configured + as: Single mode; `rows = { q -> modelPickerRows(items, favorites(), q, allowEmpty, emptyText, includeSmall) }`; + `checked = { it.key == selected?.key }`; `sectionTitle = ::modelPickerSectionTitle`; + `onPrimary = { row -> row.item?.let(::activate) ?: clear() }`; trailing = favorite hit-zone + + `onFavoriteToggle`; `search = true`; details = `ModelDetailsPanel` with `expandStateKey = + MODEL_PICKER_EXPANDED_KEY`; placement from `placement`; sizing constants = current values. + Keep `ModelPicker`'s public API (`setItems`, `select`, `clearSelection`, `open`, callbacks, + `Placement`, `Item`) and test hooks (`selectedForTest`, `selectionKeyForTest`, + `expandedForTest`) unchanged so `PromptPanel`, `settings/models`, and `settings/agents` + consumers are unaffected. +5. **Add provider picker renderer** (small subclass of `PickerListRenderer` in + `settings/providers/`, or a shared simple text renderer): content = a `JBLabel` showing the + model id; no trailing; check via `checked`. +6. **Rewrite `CustomProviderDialog.showModelPopup(ids)`** to build a `PickerPopup`: + Multi mode; `rows = { _ -> ids }` (raw model ids, no select-all row); `checked = { it in modelIds().toSet() }`; + `onPrimary = { toggleModel(it) }`; `sectionTitle = { _, _ -> null }`; `search = false`; + `toolbar = listOf(selectAllButton, deselectAllButton)`; details = null; `minWidth = 320`; + anchor = `pick`, placement `UNDERNEATH`. +7. **Extract provider selection mutations into testable helpers** on/near `CustomProviderDialog` + operating on the `models` text field: `toggleModel(id)`, `selectAllModels(ids)`, + `clearModels()` (reuse existing `modelIds()` / `setModelIds()`). Wire the two toolbar buttons + to `selectAllModels(ids)` and `clearModels()`. +8. **Remove dead code**: `CustomModelRow`, `customModelRows`, `CustomModelRowRenderer`, + `CUSTOM_MODEL_POPUP_MIN_WIDTH`/`CUSTOM_MODEL_POPUP_MAX_ROWS` (fold into base config), and the + now-duplicated inline `popupBackground` in `ProvidersSettingsUi.kt`. +9. **Strings**: add a "Deselect All" key to `KiloBundle.properties` (e.g. + `settings.providers.customModelsDeselectAll=Deselect All`); reuse existing + `settings.providers.customModelsSelectAll` for "Select All". + +## Tests + +- **Keep passing unchanged**: `ModelPickerTest.kt` (row builder + renderer + `ModelSearch` + + favorite hit-zone). Because `ModelPickerRenderer` keeps its accessors and companion members and + `modelPickerRows`/`ModelPickerRow` are untouched, these should compile and pass as-is. Verify + the favorite hit-zone tests still reference a working symbol (either keep + `ModelPickerRenderer.isFavoriteClick` delegating to `PickerListRenderer.trailingClickZone`, or + update the 3 call sites in `ModelPickerTest.kt` to the new helper). +- **Update** `ProvidersSettingsUiTest.kt`: + - Replace `test custom model rows start with select all` (there is no select-all row anymore) + with tests for the extracted helpers: `toggleModel` adds/removes an id in the text field; + `selectAllModels(ids)` sets all; `clearModels()` empties it. + - Keep existing add/edit/delete and dialog-close tests working. +- **New**: a small `PickerListRenderer`/`PickerPopup` unit test where feasible without a live + popup — e.g. `trailingClickZone` geometry (mirroring the current `isFavoriteClick` tests) and + the provider text renderer check-state. Avoid asserting live-popup internals; follow the + existing pattern of testing renderers/row-builders/pure helpers directly (per package AGENTS + "test the real implementation, no mocks"). + +## Validation + +From `packages/kilo-jetbrains/`: + +- `./gradlew typecheck` +- `./gradlew test` (iterate with `--tests ai.kilocode.client.session.ui.model.ModelPickerTest` + and `--tests ai.kilocode.client.settings.providers.ProvidersSettingsUiTest`). +- Manual smoke via `./gradlew runIde`: + - Prompt model picker: search, arrow/enter select, favorite star toggle, expand/collapse + details, placement — all unchanged. + - Add custom provider → fetch models → popup shows model list with "Select All"/"Deselect All" + toolbar buttons, per-row multi-select toggles, no details/expand, selection reflected in the + Model IDs field; Save persists selected models. + +## Risks / notes + +- `ModelPicker`'s public API and test hooks must stay stable — several settings screens depend on + it. Only the popup internals move. +- The details/expand panel is `Disposable`; the base must register it with the popup exactly as + today to avoid leaks. +- Sizing differs (prompt 420/760 dynamic + details; provider fixed 320). Parameterize rather than + hard-code; keep provider width visually equivalent to current. +- Popups are hard to unit test; keep coverage on pure helpers/renderers, not live-popup behavior. +- Keyboard/mouse semantics differ by mode (single-commit-close vs multi-toggle-stay-open) and by + presence of a trailing toggle — encode these in the base mode, don't special-case callers. + +## Out of scope + +- The other picker popups (`ModePicker`, `ReasoningPicker`, `SessionAccountOverlay`) — not part + of this consolidation. +- Adding search to the provider picker (kept off to match current behavior; base supports it). +- Any backend/RPC/DTO or CLI changes. diff --git a/.kilo/plans/1786990849107-jetbrains-devcontainer-unsupported-notice.md b/.kilo/plans/1786990849107-jetbrains-devcontainer-unsupported-notice.md new file mode 100644 index 00000000000..e740e52a0d3 --- /dev/null +++ b/.kilo/plans/1786990849107-jetbrains-devcontainer-unsupported-notice.md @@ -0,0 +1,103 @@ +# JetBrains: graceful "Dev Container not supported" workspace notice + +## Goal + +When a JetBrains project is opened so that its directory is a **local-IDE + virtual (IJent) path** — e.g. `/$devcontainer.ij/@…podman.sock/…` (the "Model 2" case) — the host-side Kilo CLI cannot resolve the directory, so agent resolution returns HTTP 500 and the workspace fails to load. Today this surfaces as a generic red "Workspace loading failed" banner with a futile "Try again". + +Replace that with a clear, non-error notice that: +- explains Kilo can't access the project because it's opened through a Dev Container / remote virtual filesystem, and +- recommends running Kilo **inside the container** via JetBrains Remote Development (backend-in-container, the validated "Model 1" flow) as the preferred way, and +- offers a "Learn more" link. + +Detection short-circuits workspace load **before** the 3× `/agent` 500 retries. + +## Background (verified in code) + +- Backend workspace load: `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspace.kt` (`load()` fetches agents/providers/commands/skills; failure → `KiloWorkspaceState.Error`). +- Backend state model: `.../backend/workspace/KiloWorkspaceState.kt` (`Pending/Loading/Ready/Error`). +- RPC DTO: `packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/KiloWorkspaceStateDto.kt` (`KiloWorkspaceStatusDto = PENDING/LOADING/READY/ERROR`). +- DTO mapping (exhaustive `when` over sealed state): `.../backend/rpc/KiloWorkspaceRpcApiImpl.kt` `dto(state)` (~line 493). Directory resolution `resolveProjectDirectory` returns `project.basePath`; `localConfig` (~line 343) already throws `InvalidPathException` for IJent paths. +- Frontend mapping to UI: `.../frontend/.../session/controller/SessionController.kt` `resolveConnectionState()` (~line 2270) maps `workspace.status == ERROR` → `ConnectionChanged.ShowError`; `retryConnection` reloads the workspace when status is ERROR (~line 622). +- Connection banner UI: `.../frontend/.../session/ui/ConnectionPanel.kt` (red/warning label + expandable details + "Try again" `ActionLink`). Events defined in `.../session/controller/SessionControllerEvent.kt` (`ConnectionChanged.{Hide,ShowConnecting,ShowDownloading,ShowError,ShowWarning}`). +- Strings: `.../frontend/src/main/resources/messages/KiloBundle.properties` (`session.connection.*`). Only the base bundle needs new keys; other locales fall back. +- This builds on the committed fix (`fix(jetbrains): don't surface workspace fetch failures as IDE errors`); genuine 500s from *real* directories keep the existing Error path. + +## Design decisions + +1. **Representation: dedicated non-error state (recommended).** Add a new workspace status rather than reusing `ERROR`, so the UI is informational (not red), retry is suppressed, and there is no 500/log/retry spam. +2. **Detection lives in the backend workspace load path**, keyed on the directory string, run before any fetch. Global app load stays `READY` (providers/config are global and unaffected). +3. **Detection signals (low false-positive):** + - Directory contains the marker `/$devcontainer.ij/`, or + - starts with a WSL root `\\wsl$\` or `\\wsl.localhost\`, or + - `java.nio.file.Path.of(directory)` (or normalize) throws `InvalidPathException`. + Do **not** trigger solely on "path doesn't exist" (avoids false positives on transient FS states / real paths). Real container paths (Model 1, e.g. `/workspaces/podman`) and normal local paths never match. +4. **Retry:** hidden for the unsupported state (deterministic; reload would re-detect). The workspace re-evaluates naturally when the directory changes (new workspace instance). +5. **Link:** a single "Learn more" hyperlink. Default target `https://kilo.ai/docs/jetbrains/dev-containers` (see Open items — confirm/replace). Keep the URL as one constant so it's trivial to change. +6. **Localization:** add keys to base `KiloBundle.properties` only. + +## Implementation tasks (ordered) + +### 1. Shared DTO +- `shared/.../rpc/dto/KiloWorkspaceStateDto.kt`: add `UNSUPPORTED` to `KiloWorkspaceStatusDto`. Reuse the existing `error: String?` field to carry a short reason code/message (no new field required); keep `errors` empty for this state. + +### 2. Backend state + detection +- `backend/.../workspace/KiloWorkspaceState.kt`: add `data class Unsupported(val reason: String) : KiloWorkspaceState()`. +- Add a small pure, unit-testable helper (single-word-friendly names), e.g. `RemoteDirectory.detect(directory: String): String?` returning a reason string when the directory is an unsupported virtual/IJent path, else `null`. Place it in `backend/.../workspace/` (backend-owned; no `kilocode_change` marker needed — new Kilo file in the Kilo plugin). +- `backend/.../workspace/KiloBackendWorkspace.kt`: at the very start of `load()`, if `RemoteDirectory.detect(directory) != null`, set `_state.value = KiloWorkspaceState.Unsupported(reason)`, log a single `info`/`warn` line (not `error`), and return without fetching. Ensure no fetch/retry runs. + +### 3. Backend RPC mapping +- `backend/.../rpc/KiloWorkspaceRpcApiImpl.kt` `dto(state)`: add branch `is KiloWorkspaceState.Unsupported -> KiloWorkspaceStateDto(status = UNSUPPORTED, error = state.reason)`. + +### 4. Frontend event + controller +- `frontend/.../session/controller/SessionControllerEvent.kt`: add `data class ShowNotice(val summary: String, val detail: String?, val learnMoreUrl: String? = null) : ConnectionChanged()`. +- `frontend/.../session/controller/SessionController.kt`: + - `resolveConnectionState()`: add a branch for `workspace.status == KiloWorkspaceStatusDto.UNSUPPORTED` (place before the generic ERROR branch) returning `ShowNotice(summary=…, detail=…, learnMoreUrl=…)` using new bundle keys. + - `retryConnection()` / retry path (~line 622): do **not** call `workspace.reload()` when status is `UNSUPPORTED`. + - Confirm `WorkspaceChanged` handling (~line 933) already no-ops for non-READY (it does). + +### 5. Frontend rendering +- `frontend/.../session/ui/ConnectionPanel.kt`: handle `ConnectionChanged.ShowNotice`: + - Info styling (secondary/label foreground, not `errorLabelForeground`). + - Show the guidance text; keep `detail` in the expandable area if used. + - Render a "Learn more" link via platform `HyperlinkLabel`/`ActionLink` that opens `learnMoreUrl` with `BrowserUtil.browse(url)`. + - Hide the "Try again" retry link for this event. + +### 6. Strings +- `frontend/.../messages/KiloBundle.properties`: add keys, e.g.: + - `session.connection.notice.devcontainer.summary=Kilo can't access this Dev Container project` + - `session.connection.notice.devcontainer.detail=This project is opened through a Dev Container/remote virtual filesystem that the Kilo runtime on your machine can't reach. Run Kilo inside the container using JetBrains Remote Development (the IDE backend runs in the container) — that's the recommended way. Local projects also work.` + - `session.connection.notice.learnMore=Learn more` +- Store the docs URL as a Kotlin constant referenced by the controller (single source), not in the bundle. + +### 7. Tests +- Backend `backend/src/test/.../workspace/KiloBackendWorkspaceTest.kt`: + - Given directory `/$devcontainer.ij/abc@…podman.sock/IdeaProjects/x`, workspace state becomes `Unsupported`; assert **no** `/agent` request hit the mock CLI (via `MockCliServer` request log), no retries, and no `ERROR`/500 log lines. + - Add a `\\wsl$\Ubuntu\home\x` case and an `InvalidPathException`-triggering case. + - Negative: a normal local dir and a real `/workspaces/...`-style dir still load normally (existing tests cover normal load). +- Add/extend a mapping assertion: `Unsupported` → `KiloWorkspaceStateDto(status=UNSUPPORTED, error=reason)`. +- Frontend `frontend/src/test/.../session/ui/ConnectionPanelTest.kt`: `ShowNotice` renders info label (not error color), shows the summary, exposes the "Learn more" link, and hides retry. +- Frontend controller test (`session/controller/…`): `KiloWorkspaceStatusDto.UNSUPPORTED` state produces a `ConnectionChanged.ShowNotice`, and `retryConnection()` does not call `workspace.reload()` for UNSUPPORTED. + +### 8. Changeset +- Add `.changeset/.md` (`"@kilocode/kilo-jetbrains": patch`) describing the user-facing behavior: "Show a clear notice (with guidance to run in a Dev Container) instead of a generic error when a project is opened through a Dev Container/remote virtual filesystem Kilo can't access." + +## Out of scope +- Actually supporting Model 2 (running the CLI in-container via Eel/IJent, port forwarding, path translation). This plan only adds graceful communication. +- Hardening every directory-scoped RPC (`models`, file search, git) for virtual paths; they already fail soft. The banner communicates the root cause. + +## Failure modes / edge cases +- **False positive** on a legitimate directory: mitigated by marker-only + `InvalidPathException` detection (not "not exists"). +- **Model 1 unaffected:** backend runs in the container, directory is real (`/workspaces/...`), markers don't match → normal load. +- **App stays READY:** only the workspace is Unsupported; global providers/config still load, so the rest of the UI (settings, providers) remains usable. +- **New enum value:** update the exhaustive `when` in backend `dto()` (compile-enforced). Frontend status checks are equality-based; add the UNSUPPORTED branch in `resolveConnectionState` and confirm no other exhaustive `when(status)` needs a branch (grep `KiloWorkspaceStatusDto.`). + +## Validation +- From `packages/kilo-jetbrains/`: `./gradlew :backend:test --tests ai.kilocode.backend.workspace.KiloBackendWorkspaceTest` and the new frontend tests (`./gradlew :frontend:test --tests …ConnectionPanelTest` and the controller test). +- From `packages/kilo-jetbrains/`: `bun run typecheck` (or `./gradlew typecheck`) and `./gradlew test`. +- Run inspection "Plugin DevKit | Code | Frontend and Backend API Usage" since split-mode code (shared DTO + frontend event) changes. +- Manual (optional): reproduce Model 2 on Linux+rootless Podman (local IDE + IJent path) and confirm the info banner + link appears instead of the red error, with no IDE internal-error popup. + +## Open items (non-blocking; recommended defaults chosen) +1. **Docs URL** for "Learn more": default `https://kilo.ai/docs/jetbrains/dev-containers`. Confirm the final path or point to an existing page; may require creating that docs page (in `packages/kilo-docs/`) separately. If source URLs under `packages/kilo-vscode`/`opencode` change this doesn't apply, but if a docs page is added, run `bun run script/extract-source-links.ts` only if a tracked source URL changes. +2. **Optional telemetry:** capture a "Dev Container Unsupported Shown" event via the existing `capture(...)` pattern in `SessionController` when the notice is first shown. Recommended: include it; low cost. +3. **Copy review:** finalize the exact wording of the summary/detail strings. diff --git a/.kilo/plans/1787073472111-jetbrains-devcontainer-outcome-card.md b/.kilo/plans/1787073472111-jetbrains-devcontainer-outcome-card.md new file mode 100644 index 00000000000..d54ad3b48e1 --- /dev/null +++ b/.kilo/plans/1787073472111-jetbrains-devcontainer-outcome-card.md @@ -0,0 +1,177 @@ +# JetBrains: surface "Dev Container unsupported" via the existing turn‑outcome card + +## Goal + +When a JetBrains project is opened through a local‑IDE + virtual (IJent) path (Model 2 — +`/$devcontainer.ij/…podman.sock/…`, `\\wsl$\…`, or an `InvalidPathException` path), the host CLI +can't resolve the directory and the workspace can't load. Communicate this **using the existing +in‑chat outcome card** (`SessionOutcomeView`), shown **immediately** on open — not with a new +ConnectionPanel banner. Extract the outcome card so it renders both inside a session transcript +(existing use) and standalone for this workspace‑level condition. + +Decision from planning: **show immediately when the workspace is `UNSUPPORTED`, and extract the +common outcome‑card UI so it is reused in the session context and outside it.** + +## Precondition (blocking) + +- The outcome UI (`model/TurnOutcome.kt`, `views/SessionOutcomeView.kt`, `SessionState.TurnEnded`, + `SessionMessageListPanel.outcome` wiring, `SessionUi.outcome`) exists on **`origin/main`** (commits + `b1a8893f14`, `0116b63641`, `58b6edd04f`, `5ad8db8a6d`) but **NOT** on this branch + (`investigate-podman-container-crash`, ~176 commits behind main). +- **Task 1 must be merging/rebasing `origin/main` into this branch** so the outcome UI is present. + Cherry‑picking just the four commits is a fallback but rebase/merge is required before the PR + merges anyway. Do this first; reconcile the existing partial work (below) during the merge. + +## What already exists on this branch (from earlier work) and how to reconcile + +- **Backend (KEEP):** `KiloWorkspaceStatusDto.UNSUPPORTED`, `KiloWorkspaceState.Unsupported(reason)`, + `RemoteDirectory.detect(...)` short‑circuit at the top of `KiloBackendWorkspace.load()`, `dto()` + mapping branch, the `kilo.dev.forceUnsupportedWorkspace` dev flag, and their tests. No change. +- **Frontend (REPLACE):** the `ConnectionChanged.ShowNotice` event + `ConnectionPanel.showNotice`/ + "Learn more" link + `resolveConnectionState` UNSUPPORTED→ShowNotice branch + `session.connection.notice.*` + bundle keys were the "new way to communicate" the user rejected. These must be removed/repurposed. + +## Design + +1. **Backend stays the source of truth.** Workspace load short‑circuits to `Unsupported(reason)` + before any fetch (already implemented); app stays `READY`; DTO carries `status = UNSUPPORTED`, + `error = reason` (`devcontainer_virtual_filesystem` | `wsl_virtual_filesystem` | `invalid_virtual_path`). +2. **Reuse the outcome card, don't invent UI.** `SessionOutcomeView` (a `DialogView`/`SessionView` + showing header icon + title + description, plus an optional scrollable error body and a + DialogView action footer) is the single card class. It is already constructed in `SessionUi` and + injected into `SessionMessageListPanel`; it does not depend on the transcript, so it can be reused + standalone. Add one small method for the informational/notice case. +3. **Show immediately, no session required.** The transcript body only renders when + `model.showSession == true`. For the UNSUPPORTED case there is no session, so route a dedicated + body (a standalone `SessionOutcomeView`) as the session content the moment the workspace is + UNSUPPORTED, taking precedence over the empty/recents view. +4. **No connection banner for this case.** `resolveConnectionState()` returns `Hide` for UNSUPPORTED + (connection is healthy); retry ignores UNSUPPORTED. + +## Implementation tasks (ordered) + +### 1. Bring in the outcome UI +- Merge/rebase `origin/main` into this branch. Verify `SessionOutcomeView`, `TurnOutcome`, + `SessionState.TurnEnded`, and the `SessionMessageListPanel.outcome`/`SessionUi.outcome` wiring are + present. Keep backend UNSUPPORTED work; drop the ConnectionPanel notice work (tasks 2–3). + +### 2. Remove the ConnectionPanel notice approach +- `frontend/.../session/controller/SessionControllerEvent.kt`: delete `ConnectionChanged.ShowNotice`. +- `frontend/.../session/ui/ConnectionPanel.kt`: remove `showNotice`, the `learn` ActionLink, the + `actions` `Stack`, the `url` field, and `learnVisible()/learnText()`; restore `retry` directly at + `BorderLayout.EAST`; remove the `ShowNotice` branch in `onEvent` and the `BrowserUtil`/`Stack` imports. +- `frontend/.../session/controller/SessionController.kt`: + - `resolveConnectionState()`: change the `workspace.status == UNSUPPORTED` branch to return + `ConnectionChanged.Hide` (place before the READY/warning branches). This prevents the perpetual + "Loading…" that would otherwise occur because `workspace != READY`. + - `retryConnection()`: keep the guard that returns early (no `workspace.reload()`) when + `workspace.status == UNSUPPORTED`. + - `setConnectionTargetState()`: remove the `ShowNotice` immediate‑state branch. +- `frontend/.../messages/KiloBundle.properties`: remove `session.connection.notice.*` keys. + +### 3. Extract/extend the reusable outcome card +- `frontend/.../session/views/SessionOutcomeView.kt`: add an EDT method that reuses the existing + card rendering for an informational notice, e.g.: + `fun showNotice(title: String, description: String, tone: OutcomeTone, actions: List = emptyList())` + — sets header icon (WARNING for informational), `setHeader(title, description)`, `setContent(null)`, + `setActions(actions)`, `isVisible = true`, `refresh()`. This reuses the same visual card the + transcript uses; it is not a new surface. Confirm `DialogView` exposes `setActions`/`Action` (it + does — used by `LoginRequiredView`/`RevertBanner`). +- Do not fork the card; the same `SessionOutcomeView` class is used in the transcript and standalone. + +### 4. Route the standalone card in `SessionUi` +- `frontend/.../session/SessionUi.kt`: + - Own a standalone `SessionOutcomeView` for the workspace notice (separate instance from the + transcript's `outcome`), plus a body container following the `blankBody`/`progressBody` pattern, + e.g. `unsupportedBody` hosting the standalone view. Register it with `applyStyle`. + - `body(state)`: at the top, if `controller.model.workspace.status == KiloWorkspaceStatusDto.UNSUPPORTED`, + return `unsupportedBody` (and populate the card via `showNotice(...)` mapping the workspace `error` + reason to copy). This wins over `showSession`/empty/progress. + - React to workspace changes: in the `SessionControllerEvent.WorkspaceChanged` handler (and on + the new `ShowUnsupported` view event, task 5), re‑evaluate the body and populate the card. Keep + `prompt.setReady(controller.model.isReady())` — prompt stays disabled since `isReady()` is false + when workspace ≠ READY, so the card explains why input is unavailable. + +### 5. Controller view routing for the unsupported case +- `frontend/.../session/controller/SessionControllerEvent.kt`: add + `data class ShowUnsupported(val reason: String) : ViewChanged()` with a stable `toString()`. +- `frontend/.../session/controller/SessionController.kt`: + - Add a precedence check so that when `model.workspace.status == UNSUPPORTED` (app READY), the + controller emits `ViewChanged.ShowUnsupported(reason)` via `setControllerViewState(...)` instead + of `ShowRecents`/`ShowSession`. Gate `canUseRecents()` (and the recents refresh at ~line 992 / + `refreshRecents`) to return false when UNSUPPORTED so recents don't clobber the notice. + - Handle `ShowUnsupported` in `setControllerViewState` similarly to `ShowRecents` + (e.g. `hideAccountOverlay()` or leave account overlay untouched; do not set `model.showSession`). +- `SessionUi` handles `ViewChanged.ShowUnsupported` by clearing `empty`, populating the standalone + `SessionOutcomeView` from `reason`, and `scroll.show(unsupportedBody)`. + +### 6. Copy / content +- `frontend/.../messages/KiloBundle.properties`: add notice copy reused for all virtual‑fs reasons + (single title + description is sufficient; the recommendation is the same), e.g.: + - `session.unsupported.devcontainer.title=Kilo can't access this Dev Container project` + - `session.unsupported.devcontainer.description=This project is opened through a Dev Container or remote virtual filesystem that the Kilo runtime on your machine can't reach. Run Kilo inside the container using JetBrains Remote Development, where the IDE backend runs in the container. Local projects also work.` + - Optional: `session.unsupported.learnMore=Learn more` + - Note: no‑argument bundle values must use a single apostrophe (`can't`), not `''` (MessageFormat + only collapses `''` when args are passed). +- Keep the docs URL as one Kotlin constant (reuse the existing `DEVCONTAINER_URL`, + default `https://kilo.ai/docs/jetbrains/dev-containers`). If a "Learn more" action is included, + wire it as a `DialogView.Action` that calls `BrowserUtil.browse(url)`. +- Reason→copy mapping: map all three reasons to the same devcontainer copy for now (a `when(reason)` + helper), so wsl/invalid paths also get a clear message. Tone = `OutcomeTone.WARNING` (informational). + +### 7. Tests +- Backend (unchanged, keep passing where the env allows): `RemoteDirectoryTest` (pure, incl. forced + flag), `KiloBackendWorkspaceTest` UNSUPPORTED cases (no `/agent` fetch), `KiloWorkspaceRpcApiImplTest` + mapping. Note: the full backend suite can't run in this worktree because the fake‑CLI `connect()` + helper times out here (environmental); `RemoteDirectoryTest` runs fine and should be the primary + backend guard. +- Frontend: + - Remove the obsolete `ConnectionPanelTest` notice test and the `ConnectionDelayTest` + unsupported‑notice/retry tests. Replace with: UNSUPPORTED makes `resolveConnectionState` resolve + to `Hide` (no connection banner), and `retryConnection()` does not call `projectRpc.reload`. + - `SessionOutcomeViewTest`: add a case for the new `showNotice(...)` — asserts header icon/title/ + description render and (if included) the Learn‑more action is present. + - Controller test: workspace `UNSUPPORTED` emits `ViewChanged.ShowUnsupported(reason)` and does + **not** emit `ShowRecents`/`ShowSession`; `canUseRecents()` is false. + - SessionUi/body test (or `SessionMessageListPanel`‑style test): when workspace is UNSUPPORTED the + standalone outcome card body is shown with the devcontainer title/description. + +### 8. Changeset +- Update the existing `.changeset/jetbrains-devcontainer-notice.md` (`"@kilocode/kilo-jetbrains": patch`) + to describe the final behavior: "When a JetBrains project is opened through a Dev Container / remote + virtual filesystem Kilo can't access, show a clear in‑chat notice (with guidance to run Kilo inside + the container) instead of a generic loading/error state." + +## Out of scope +- Actually supporting Model 2 (running the CLI in‑container via Eel/IJent, path translation). This is + communication‑only. +- Reworking the transcript's existing failed/interrupted turn outcomes. + +## Risks / edge cases +- **Merge scope:** rebasing ~176 commits may conflict on `SessionController.kt`/`ConnectionPanel.kt`/ + `SessionUi.kt`. Reverting the ConnectionPanel notice (task 2) reduces overlap with main's outcome + changes; do task 2 as part of resolving conflicts. +- **State‑machine hygiene:** drive the notice from **workspace status** (view routing), not by + faking a per‑session `SessionState`, to avoid corrupting the session state machine when there is no + session. +- **Body precedence:** ensure UNSUPPORTED beats empty/recents/progress and that recents refresh does + not overwrite it (`canUseRecents()` guard). +- **App stays READY:** global providers/config still load; only the workspace is Unsupported, so the + rest of the UI (settings/providers) remains usable and no red connection error appears. +- **Manual repro:** `-Pkilo.dev.forceUnsupportedWorkspace=true` on a dev IDE run forces every + workspace into UNSUPPORTED to exercise the card without a real IJent path. + +## Validation +- From `packages/kilo-jetbrains/`: `./gradlew :backend:test --tests ai.kilocode.backend.workspace.RemoteDirectoryTest` + and the frontend tests `./gradlew :frontend:test --tests …SessionOutcomeViewTest --tests …ConnectionDelayTest` + plus the new controller/body tests. (Use module‑scoped `:backend:`/`:frontend:` task filters.) +- `./gradlew typecheck` and, where the env permits, `./gradlew test`. +- Run inspection "Plugin DevKit | Code | Frontend and Backend API Usage" (shared DTO + new frontend + view event). +- Manual: launch split mode with `-Pkilo.dev.forceUnsupportedWorkspace=true`; confirm the in‑chat + outcome card appears immediately, the prompt is disabled, and no red connection banner shows. + +## Open items +- Confirm/replace the "Learn more" docs URL (`https://kilo.ai/docs/jetbrains/dev-containers`) and + whether to include the Learn‑more action at all (recommended: include it as a `DialogView.Action`). +- Final copy review for the notice title/description. diff --git a/.kilo/plans/1787183917922-jetbrains-unsupported-workspace-banner.md b/.kilo/plans/1787183917922-jetbrains-unsupported-workspace-banner.md new file mode 100644 index 00000000000..2c908112efc --- /dev/null +++ b/.kilo/plans/1787183917922-jetbrains-unsupported-workspace-banner.md @@ -0,0 +1,149 @@ +# Show unsupported workspace via the standard session error banner + fit-to-transcript error height (JetBrains) + +## Goal + +1. When a JetBrains workspace directory is unsupported for the host-side CLI + runtime (devcontainer / WSL / invalid virtual path), surface it through the + existing in-session connection banner (`ConnectionPanel`) instead of silently + showing "Loading…" forever. It gets the **same** recovery options as other + connection errors (Try again → Retry / Restart / Reinstall Core). +2. Change the expanded error/detail area so it **fits the whole detail text**, + capped to the **available height of the session transcript area** (instead of + the current fixed 10-line cap). This becomes the default behavior for **all** + connection error/warning banners, not just unsupported. + +## Background / Current State + +- Detection already works end to end. `RemoteDirectory.detect()` returns a reason + code and `KiloBackendWorkspace` sets `KiloWorkspaceState.Unsupported(reason)`, + mapped to `KiloWorkspaceStateDto(status = UNSUPPORTED, error = reason)` + (`KiloWorkspaceRpcApiImpl.kt:507`). Reason string is in the DTO `error` field. + Reason codes: `devcontainer_virtual_filesystem`, `wsl_virtual_filesystem`, + `invalid_virtual_path`. +- The DTO reaches `SessionModel.workspace`; `syncConnectionState()` already runs + on every `WorkspaceChanged` (`SessionController.kt:982-984`). +- **Gap:** `SessionController.resolveConnectionState()` (`SessionController.kt:2353-2393`) + has no `UNSUPPORTED` branch, so it falls through to `ShowConnecting` + (line 2392) → banner reads "Loading…" indefinitely. +- Banner renderer is `ConnectionPanel` (`session/ui/ConnectionPanel.kt`), driven + by `ConnectionChanged.ShowError/ShowWarning/…`. Its "Try again" link opens a + popup with Retry / Restart / Reinstall (`recoveryGroup()`). +- Height cap today: `DETAILS_LINES = 10` (line 45). `scrollHeight()` coerces the + row count to `1..DETAILS_LINES` (line 304), `getPreferredSize()` uses it + (line 295-301), and `maxExpandedHeight()` (line 345) exposes the fixed cap. +- Overlay placement: `SessionUi.kt:442-452` anchors the banner just above the + prompt using `child.preferredSize.height` as the banner height; bottom edge is + `promptTop - gap`, and it grows upward. + +All touched files are Kilo-owned JetBrains frontend paths — no `kilocode_change` +markers required. No backend / shared DTO changes needed. + +## Design Decisions + +- **Unsupported reuses the standard `ShowError` path unchanged** — same red + banner, same "Try again" popup (Retry / Restart / Reinstall). No new event + type and no `retry` flag. (Reverses the earlier "hide retry" idea per user.) +- **Detail area fits the full text, capped to available transcript height.** + Remove the fixed 10-line cap in `ConnectionPanel` so preferred height reflects + the whole detail text; clamp the rendered banner height to the transcript space + above the prompt in the overlay layout (where pane geometry is known). The + existing internal `JBScrollPane` (`VERTICAL_SCROLLBAR_AS_NEEDED`) scrolls the + overflow. This applies to every error/warning banner uniformly. + +## Implementation Tasks + +1. **`SessionController.resolveConnectionState()`** (`SessionController.kt:2353`) + - Add a branch for `workspace.status == KiloWorkspaceStatusDto.UNSUPPORTED` + (place it next to the workspace `ERROR` branch, before the READY branches): + - summary = `KiloBundle.message("session.connection.unsupported")` + - detail = reason mapped to a localized string (helper below), falling back + to the raw `workspace.error` + - `source = "workspace"` (retry link shows by default; no flag change) + - Add a private helper (single-word name, e.g. `unsupported`) mapping the + `workspace.error` reason code to a bundle string: + - `devcontainer_virtual_filesystem` → `session.connection.unsupported.devcontainer` + - `wsl_virtual_filesystem` → `session.connection.unsupported.wsl` + - `invalid_virtual_path` → `session.connection.unsupported.invalid` + - else → `session.connection.unsupported.unknown` (or raw reason) + +2. **`KiloBundle.properties`** (`resources/messages/`, after line 17) + - Add (finalize exact copy during implementation): + - `session.connection.unsupported=Workspace not supported` + - `session.connection.unsupported.devcontainer=Dev Container virtual filesystem paths can't be reached by the host-side Kilo runtime.` + - `session.connection.unsupported.wsl=WSL virtual filesystem paths aren't supported by the host-side Kilo runtime.` + - `session.connection.unsupported.invalid=This workspace path can't be resolved on the local filesystem.` + - `session.connection.unsupported.unknown=This workspace isn't supported by the host-side Kilo runtime.` + +3. **`ConnectionPanel.kt`** (`session/ui/`) — remove the fixed cap so details fit + the whole text: + - Delete `DETAILS_LINES` (line 45) usage in `scrollHeight()` (line 303-306): + compute rows from the full logical line count (`coerceAtLeast(1)`), no upper + bound. `getPreferredSize()` (line 295-301) then reports the full detail + height when expanded. + - Remove `maxExpandedHeight()` (line 345) or repurpose it; it encodes the + 10-line cap and is only used by the outgoing test. + - Leave the `JBScrollPane` policies as-is so overflow scrolls when the overlay + clamps the banner shorter than preferred. + - Note (known limitation, keep behavior parity): row count uses logical lines, + not wrapped visual lines, so a wrapped long line may under-estimate height; + the scrollbar still covers overflow. Optional follow-up only. + +4. **`SessionUi.kt`** overlay layout for `connection` (lines 442-452) — clamp the + banner height to the transcript area above the prompt: + - `full = child.preferredSize.height` + - `avail = (point.y - gap).coerceAtLeast(0)` (space from pane top to just above + the prompt) + - `h = full.coerceAtMost(avail)` + - Rectangle: `x = point.x + gap`, `y = point.y - h - gap`, + `width = (prompt.width - gap*2).coerceAtLeast(0)`, `height = h` + - This keeps the bottom anchored at `promptTop - gap` (unchanged) while + preventing the top from overflowing above the transcript region; the panel's + internal scroll pane handles the remainder. Applies to every banner state. + +## Tests + +- **`ConnectionDelayTest.kt`** (`session/controller/`): add a test mirroring + `test persistent workspace error is delayed` — set + `projectRpc.state.value = KiloWorkspaceStateDto(status = UNSUPPORTED, error = "wsl_virtual_filesystem")`, + assert a `ShowError` with summary "Workspace not supported", the mapped WSL + detail, and `source == "workspace"`; assert it no longer resolves to + `ShowConnecting`. +- **`ConnectionPanelTest.kt`** (`session/ui/`): + - Replace `test expanded details height is capped at ten lines` (lines 140-150) + with a test asserting the expanded preferred height grows with the full text + (e.g. 30 lines yields a preferred height clearly larger than the old + 10-line height / a computed full-text height), i.e. no fixed cap. + - Optionally add a small test that an unsupported-style `ShowError` still shows + the retry link and uses the Core recovery group (parity with existing + `test retry popup group uses core recovery actions`). +- **`SessionUiLayoutTest.kt`**: add a test that with a large detail body and a + constrained root/pane height, the expanded banner is clamped to the transcript + area — `connection.y >= 0` (does not overflow above the transcript), bottom + still anchored at `promptTop - gap`, `detailsVisible()` true, and the internal + `JBScrollPane` shows/needs its vertical scrollbar. Reuse the anchoring + assertions from `test expanded connection panel remains anchored above prompt` + (lines 261-276). + +## Validation + +From `packages/kilo-jetbrains/`: +- `./gradlew typecheck` +- `./gradlew test` (or targeted: `ConnectionPanelTest`, `ConnectionDelayTest`, + `SessionUiLayoutTest`) + +Requires Java 21; only check Java if Gradle fails with a Java-version error. + +## Risks / Notes + +- Removing the fixed cap changes sizing for **all** connection banners; the + overlay clamp is what bounds it, so verify very long errors scroll rather than + push the banner off the top of the transcript. +- No backend / shared DTO changes; `UNSUPPORTED` + `error=reason` already reach + the frontend. +- Reason codes live only in `RemoteDirectory.kt` today; the new bundle keys are + the first human-readable mapping. New reason codes fall back to `unknown`. + +## Open Questions + +- Exact user-facing wording for the five `session.connection.unsupported*` + strings (placeholder copy above). diff --git a/.kilo/plans/agent-manager-multi-project-configuration.md b/.kilo/plans/agent-manager-multi-project-configuration.md new file mode 100644 index 00000000000..340082fbb63 --- /dev/null +++ b/.kilo/plans/agent-manager-multi-project-configuration.md @@ -0,0 +1,262 @@ +# Agent Manager multi-project configuration architecture + +**Status:** Blocking architecture for multi-project release + +**Date:** 2026-07-22 + +This document is the canonical configuration specification for Agent Manager multi-project support. The main UI/runtime plan references it and must not duplicate or contradict it. + +The executable sequence from the current branch is [`agent-manager-multi-project-implementation-handoff.md`](./agent-manager-multi-project-implementation-handoff.md). + +## Decision + +Keep the useful current split between user and project settings, but make every Settings read and write target explicit, immutable, revisioned, and independent from Agent Manager activation. + +Multi-project must not ship broadly while Settings can load a draft for project A and resolve its save target from the mutable active project B. + +## Current behavior + +Kilo has four configuration stores: + +| Store | Example | Owner | +|---|---|---| +| VS Code preferences | VS Code `settings.json` | This VS Code user/installation | +| Kilo user config | `~/.config/kilo/kilo.json` | User defaults across projects and Kilo clients | +| Kilo project config | `/.kilo/kilo.jsonc` | Repository behavior and overrides | +| Runtime/session state | In-memory, directory-qualified | One project, worktree, or session | + +The shared Settings save currently calls `splitConfigByScope()`: + +- `commit_message` is written to project config; +- `indexing.enabled` is written to project config; +- all other generic Settings fields are written to user config. + +The Indexing tab also has an explicit Global/Project selector and can write the entire `indexing` object to either layer. This is too broad because project config can receive provider/model/vector-store credentials and infrastructure settings. + +Several controls are VS Code preferences and bypass Kilo config entirely, including autocomplete UI, browser automation, notifications, max auto-approve cost, commit-message output language, and indexing button visibility. + +## Confirmed blocking failure + +The current protocol does not bind a draft to the config target it was loaded from: + +1. `KiloProvider.fetchAndSendConfig()` resolves a mutable current directory and sends unqualified `configLoaded` state. +2. The webview owns one global/project/effective draft. +3. Agent Manager changes the active project from A to B. +4. The webview sends an unqualified `updateConfig`. +5. `KiloProvider.handleUpdateConfig()` resolves the current directory again at save time and may write A's draft to B. + +Reads are directory-scoped, but writes are not bound to the read target. This is the release blocker. + +The backend also lacks an expected target/revision precondition, so external editors or another window can overwrite a config between read and save. + +## Ownership policy + +### VS Code preferences + +These remain in VS Code settings and do not participate in project config: + +- extension language and presentation preferences; +- autocomplete enablement, keybindings, provider, and model; +- browser automation enablement/system Chrome/headless mode; +- notification enablement and sound; +- maximum automatic approval cost; +- commit-message output language; +- indexing button visibility while indexing is disabled; +- multi-project feature enablement. + +### Kilo user config + +These are personal defaults or security policy and should be edited in User scope: + +- default, small, and subagent models and variants; +- provider enablement, custom providers, credentials, and endpoints; +- user/global agents and default agent; +- permission defaults and user tool defaults; +- sandbox policy, network access, writable paths, and allowed hosts; +- compaction, checkpoint/snapshot, and tool-output defaults; +- username and display behavior; +- sharing, remote control, telemetry, and experimental features; +- user/global formatter, LSP, MCP, skills, instructions, commands, and workflows; +- indexing provider, model, credentials, vector storage, and global tuning defaults. + +A trusted project may override many of these at runtime, but editing User scope never writes those overrides. + +### Kilo project config + +These describe repository behavior and are valid project settings: + +- commit-message prompt; +- repository indexing file extensions, include/ignore rules, and deliberate project tuning overrides; +- repository instructions; +- repository skill paths; +- repository commands/workflows; +- trusted project agents; +- trusted project MCP servers; +- repository formatter/LSP overrides; +- repository watcher ignores; +- repository-specific tool restrictions and permission requests. + +Project configuration may override user model/agent/tool defaults, but provider credentials and security-policy weakening must not be silently authored into a repository. + +### Machine-local project consent + +Indexing enablement is privacy consent, not repository configuration. Store it outside the repository, keyed by canonical `ProjectId` in machine-local extension state: + +- newly observed projects default to indexing disabled; +- users explicitly enable indexing for one project on this machine; +- repository config cannot enable indexing; +- repository config may describe what to index, but consent gates whether indexing starts; +- canonical project identity prevents a symlink or alternate path from bypassing consent. + +Effective indexing requires both valid user-global indexing configuration and machine-local consent for that project. + +### Current tabs + +| Settings tab | Correct editable target | +|---|---| +| Models | User by default; explicit Project scope may override models/agents | +| Providers | User only for credentials/endpoints; project-provided entries are source-labelled | +| Agent Behaviour | User or explicit trusted Project scope | +| Auto Approve | User Kilo config; max cost remains VS Code preference | +| Browser | VS Code preferences | +| Checkpoints | User default or explicit Project override | +| Display | User config | +| Autocomplete | VS Code preferences | +| Notifications | VS Code preferences | +| Context | User defaults; repository watcher/instruction rules in explicit Project scope | +| Commit Message | Project scope for prompt; language remains VS Code preference | +| Indexing | User for provider/model/credentials/storage; machine-local project consent for enablement; Project for repository rules | +| Experimental | User config; multi-project also mirrors to VS Code preference | +| Sandboxing | User config only | +| Language | VS Code preference | +| MCP/Commands/Skills | User defaults or explicit trusted Project scope | + +No field silently chooses a file during save. The UI must display its scope. + +## Settings UX + +Use explicit scope and project controls: + +```text +Scope: User | Project + +Project: backend +Target: /projects/backend/.kilo/kilo.jsonc +``` + +- User scope always targets user config. +- Project scope requires an explicit trusted project selector. +- The Settings selector is separate from Agent Manager's active project. +- Opening Settings may initialize the project selector once from the current project, but later Agent Manager switches never change it. +- A dirty project draft cannot move to another project. Selector changes require Save, Discard, or Stay. +- Inherited values show source badges such as User, Project: backend, and Managed. +- Project scope offers Override and Reset to inherited. +- Project-sourced providers cannot be silently deleted from project config through User scope. + +Runtime config still follows the exact session directory. Settings Project scope targets the registered project root, not the active worktree. Worktree-config editing is a separate future feature requiring an explicit `WorktreeRef`. + +## Immutable binding contract + +A Settings read returns an opaque binding: + +```ts +interface SettingsBinding { + id: string + connectionGeneration: number + scope: "global" | "project" + project?: { + projectId: string + root: string + generation: number + } + directory: string + target: { + scope: "global" | "project" + path: string + revision: string + exists: boolean + writable: boolean + } +} +``` + +The write contains only the opaque binding and patch: + +```ts +interface WriteSettingsConfig { + type: "settingsConfig.write" + requestId: string + bindingId: string + set: Record + unset: string[][] +} +``` + +The extension stores the authoritative binding. On write it must: + +1. reject unknown/expired bindings; +2. verify project existence, generation, and trust; +3. capture the binding before the first await; +4. use the binding's stored directory and scope; +5. never call `getWorkspaceDirectory()`, `contexts.active()`, or use a worktree/session fallback; +6. clear a draft only from the matching `{ requestId, bindingId }` response. + +Bindings expire after save, reconnect, trust revocation, project removal, or context generation change. + +## Backend revision contract + +`GET /config/overlay` must return the exact global/project target path, parsed raw target config, effective config/source metadata, and a revision. + +The revision is a SHA-256 fingerprint of canonical target path, existence marker, and exact file bytes. This catches content changes, JSONC comment-only edits, and target changes. + +`PATCH /config/overlay` accepts one scope and requires: + +```ts +{ + scope: "global" | "project" + set: Record + unset: string[][] + expected: { + path: string + revision: string + } +} +``` + +The backend must re-resolve the authoritative target, verify path/revision under a target lock, patch the raw target layer, validate it, atomically replace the file, and return a fresh snapshot. It never accepts an arbitrary client path. + +Expected failures include expired binding, unknown/untrusted project, changed target, revision conflict, invalid config, non-writable target, and I/O failure. Every failure preserves the draft. + +## Required implementation + +1. Add revisioned target descriptors and compare-and-swap writes to the Kilo config overlay API. +2. Split activation-bound runtime config state from binding-keyed Settings editor state. +3. Replace unqualified `configLoaded`/`updateConfig` with settings read/write messages carrying request and binding IDs. +4. Replace hidden `splitConfigByScope` saves with explicit scope on every editable control. +5. Restrict Indexing Project scope to repository rules; keep provider/model/credentials/storage in User scope. +6. Move indexing enablement from project config to machine-local consent keyed by canonical `ProjectId`, default off. +7. Audit direct config mutators outside the save bar, especially provider disconnect, imports/resets, custom providers, work styles, permission rules, and indexing actions. +8. Make Open Project Config take a `ProjectRef`, resolve the immutable registered root, and verify trust. +9. Partition config caches/events by scope, directory, target, revision, and activation generation. + +## Blocking tests + +- Load Settings for A, switch Agent Manager to B, save: only A's bound target changes. +- Same test while selecting A/B worktrees and sessions. +- User-scope save always changes only user config. +- Project-scope save requires the explicit trusted project and changes only its registered root config. +- Dirty drafts survive Agent Manager switches and cannot migrate between Settings projects. +- Out-of-order reads and writes update only the matching request/binding. +- External file change causes a revision conflict without losing the draft. +- A changed config target path causes a target conflict. +- Project removal, generation change, or trust revocation expires its binding. +- Indexing provider/model/credentials/storage never enter project config through the form. +- A repository file containing `indexing.enabled: true` cannot grant indexing consent. +- New projects default to indexing disabled until explicitly enabled on this machine. +- Consent follows canonical project identity across symlink/path aliases and never leaks to another project. +- `commit_message.prompt` and repository indexing rules still support explicit project writes. +- Runtime worktree config uses the worktree directory while Project Settings remains bound to the registered project root. + +## Release gate + +Keep multi-project disabled by default until the immutable binding/revision contract and the blocking tests above are implemented. The useful existing project-local behavior should be preserved, not removed; its write target must become explicit and immutable. diff --git a/.kilo/plans/agent-manager-multi-project-runtime.md b/.kilo/plans/agent-manager-multi-project-runtime.md new file mode 100644 index 00000000000..25bb0d9e07b --- /dev/null +++ b/.kilo/plans/agent-manager-multi-project-runtime.md @@ -0,0 +1,351 @@ +# Agent Manager multi-project runtime + +> UI architecture update: the active-body plus background-summary model in this document is superseded by `agent-manager-multi-project-uniform-ui.md`. The runtime findings remain useful, but the final UI renders one permanent real interactive body for every expanded project and requires strict project-qualified routing before background actions are enabled. + +**Status:** Runtime foundation implemented (this worktree); accordion UI pending + +## Implementation status (2026-07-20) + +Landed in this worktree, all covered by unit tests (`tests/unit/agent-project-*.test.ts`): + +- `src/agent-manager/project-paths.ts` — canonical root resolution (`realpath` + `normalizePath`), case-aware `samePath`, deterministic `projectIdFor(root)` (sha1, stable across restarts), `resolveGitRoot`. +- `src/agent-manager/project-registry.ts` — versioned global catalog of **additional** projects (storage-injected, corrupt-safe, dedupe by id, trust/label/order). The pinned workspace project is never persisted. +- `src/agent-manager/project-context.ts` — immutable `ProjectContext` (owns WorktreeStateManager/WorktreeManager/SetupScriptService/stale set per canonical root, lazy creation, peek accessors) and `ProjectContexts` coordinator (pinned derivation, active/expanded lifecycle, trust+flag gating, `syncPinned()` for workspace changes, webview `ProjectSnapshot`s). +- `src/agent-manager/project-messages.ts` — vscode-free handlers: `requestProjects`, `addProject` (folder picker → git root resolve → dedupe), `removeProject`, `selectProject`, `setProjectExpanded`, `trustProject`. All fail closed when the flag is off. +- `src/agent-manager/project-wiring.ts` — factory assembling registry/contexts/deps/host listeners (extracted to keep `AgentManagerProvider.ts` under its 2000-line cap). +- `AgentManagerProvider` — fields `state/worktrees/setupScript/staleWorktreeIds` replaced by context-backed getters; every existing handler now operates on the active project with zero per-handler changes. `onMessage` consumes project messages first and drops messages whose `projectId` mismatches the active project (stale-pane protection). `activateProject` re-runs `initializeState` for the new context; pollers/importer/run/terminal follow the active context through the existing getters. `agentManager.state` now carries `projectId`. +- Protocol — `agentManager.projects` out-message (`multiProject` flag + `ProjectSnapshot[]`); new in-messages listed above; `projectId?` on `agentManager.state` and `createWorktree`. Webview type mirrors updated (`webview-ui/src/types/messages/{extension,webview}-messages.ts`). +- Host — `pickFolder`, `multiProject`, `readProjects`/`writeProjects` (globalState key `agentManager.projects`), `onDidChangeWorkspaceFolders`, `onDidChangeMultiProject`. +- Setting — `kilo-code.new.agentManager.multiProject` (boolean, default false, application scope). +- Changeset — `.changeset/agent-manager-multi-project.md`. + +Behavior with flag off: only the pinned workspace project exists; snapshots contain exactly one project; all registry data is preserved but hidden; no UI change. + +## Remaining for the UI session (accordion) + +- Project accordion rendering from `agentManager.projects`: header-only rows for collapsed/uninitialized projects (registry metadata only), full existing Local/worktree body for the active project, expanded non-active bodies from per-context state (extension must push per-context state with that context's `projectId` — currently `pushState` only pushes the active context). +- Add-project entry point, trust CTA (sends `agentManager.trustProject` then expand/select), remove/rename in project menu. +- `selectProject` before any repo action in a non-active project; send `projectId` on `createWorktree` (extension drops mismatches). +- New Worktree dialog project selector (defaults to active project). +- Extension follow-ups: hidden-cadence polling for expanded projects; session-owner index so Local sessions of project B open/share correctly even while A is active (currently Local sessions resolve through the active root — acceptable only while the UI activates before opening). +- Implemented: per-context `pushState` for expanded non-active projects; per-project git stats/PR pollers (`ProjectPollers` in `project-pollers.ts`) so every expanded accordion receives live stats/PR data tagged with its projectId; webview `project-live.ts` store routing per-project payloads into per-project summary stores; `activate()` keeps the previously active project expanded. +- Known boundary: `KiloProvider.projectID` and session refresh remain single-project; cross-project SSE filtering by session→directory owner is a follow-up (see "Verified findings" below). + +## Objective (original) + +Build the project-aware Agent Manager runtime before changing the UI. The eventual UI is a pinned current-project view plus lazily initialized additional project accordions, with one shared tab/detail area. The first backend slice must be mergeable with the flag disabled and must route today's single project through the same project-aware contracts that secondary projects will use. + +The implementation must preserve these invariants: + +- The first project is always the canonical Git root of the first VS Code workspace folder in the current window. It is derived at runtime, is always first, and cannot be removed or replaced by persisted state. +- Global extension storage contains only the catalog of additional projects and project-list UI metadata. Each repository remains authoritative for its own `.kilo/agent-manager.json`. +- A collapsed, never-opened additional project causes no `.kilo/agent-manager.json` read, Git mutation, setup/run initialization, terminal creation, or poller subscription. +- Every session/worktree/draft operation has a project identity internally. Once secondary-project activation is enabled and more than one project is exposed, missing or mismatched identity is rejected rather than routed to the current workspace. +- The feature flag gates catalog exposure, secondary activation, and multi-project wire/UI behavior. It does not select a separate legacy implementation. + +## Verified findings + +- `AgentManagerProvider` currently owns the panel and all repository-bound resources in one 1,941-line singleton. Its root is always `Host.workspacePath()` and its state/managers are lazily cached against that root (`packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts:56-212`, `1654-1695`). The architecture test caps this file at 2,000 lines and explicitly forbids raising the cap (`packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts:793-854`). +- Opening the panel immediately initializes the current repository, performs `ensureGitExclude`, loads `.kilo/agent-manager.json`, discovers/restores worktrees, registers sessions, starts state-dependent behavior, and recovers prompts (`AgentManagerProvider.ts:267-360`). This whole sequence cannot run for a collapsed catalog entry. +- `WorktreeStateManager` is correctly repository-scoped by constructor root and writes only `/.kilo/agent-manager.json` (`WorktreeStateManager.ts:100-124`, `632-656`, `797-847`). Its save queue and `flush()` are suitable for a context shutdown barrier, but it has no cancellation/dispose primitive (`755-795`). +- The embedded Agent Manager `KiloProvider` is one provider attached to the one panel/webview (`agent-manager/vscode-host.ts:67-169`). It already routes many SDK calls through a session-to-directory map, but local sessions are allowed to fall back to `workspaceFolders[0]` (`KiloProvider.ts:4343-4378`; `kilo-provider-utils.ts:319-345`). That fallback becomes a cross-project write risk. +- `KiloProvider` has singleton project state (`projectID`, `currentSession`, project-scoped caches, one active project directory) and current session refresh treats all extra directories as worktrees of one canonical project (`KiloProvider.ts:344-359`, `1984-2029`; `kilo-provider-utils.ts:230-295`). It is not multi-project-correct without refactoring. +- The shared backend and generated SDK support an explicit `directory` on session share and unshare (`packages/sdk/js/src/v2/gen/sdk.gen.ts:4733-4795`). Sharing must therefore use the same project/session route as prompts and transcript reads. +- Backend session IDs are high-entropy process-global IDs (`packages/opencode/src/id/id.ts:1-17`, `23-33`, `42-49`). Raw IDs can remain the SDK/server identifier, but the UI/runtime identity still needs a project-qualified reference, and route registration must reject an observed cross-project collision. +- `session.status` currently discards the SSE directory before invoking branch naming (`AgentManagerProvider.ts:204-220`). The orchestration bridge subscribes globally and rejects requests outside its root (`orchestration-bridge.ts:175-203`). Creating one unchanged bridge per project would let the wrong bridge reject a valid request before the correct project handles it. +- Multiple `SessionTerminalManager` instances would each register the same global VS Code commands and terminal listeners (`SessionTerminalManager.ts:35-81`). It must remain panel/coordinator-owned, with project-qualified keys and explicit CWDs. Also, `TerminalRouter` currently falls back from an unknown worktree ID to the repository root (`terminal-routing.ts:120-130`), which must be removed in strict routing. +- `VscodeHost.workspacePath()` uses `workspaceFolders[0]` (`vscode-host.ts:172-174`; `review-utils.ts:12-15`). It loses URI scheme/authority, and `openFolder` recreates paths with `Uri.file`. A catalog containing only path strings would be unsafe across local, SSH, container, and other remote extension hosts. +- Existing Kilo experimental config is CLI/project config, not an application feature-flag service (`features.ts`; `webview-ui/src/types/messages/config.ts`). A multi-project bootstrap flag cannot be project-scoped because choosing which project config to read is the behavior being gated. + +## Architecture decisions + +### 1. Project identity and registry semantics + +Add a VS Code-free `ProjectRegistry` backed by an injected storage and resolver interface. `vscode-host.ts` implements those interfaces using `ExtensionContext.globalState`, workspace/open-dialog URIs, filesystem canonicalization, and Git. + +Use these core types in `src/agent-manager/project.ts`: + +```ts +type ProjectId = string + +interface ProjectLocation { + uri: string + scheme: string + authority: string + root: string + commonGitDir: string +} + +interface ProjectDescriptor extends ProjectLocation { + id: ProjectId + name: string + pinned: boolean + collapsed: boolean + order: number + status: "unverified" | "ready" | "missing" | "notGit" | "wrongAuthority" +} +``` + +`root` is the canonical filesystem Git toplevel. Resolve it only when deriving the pinned project, adding a project, or expanding an unverified project: + +1. Start from the selected workspace/folder URI. +2. Reject a URI whose scheme/authority is not the current extension-host scope. +3. Run `git -C rev-parse --show-toplevel` through the existing hidden-window process wrapper. +4. Resolve the result with `fs.promises.realpath`; normalize separators, trailing separators, and Windows case for identity comparison. +5. Resolve and canonicalize `git rev-parse --path-format=absolute --git-common-dir` as `commonGitDir`. +6. Derive `ProjectId` deterministically as a versioned SHA-256/base64url hash of `scheme + NUL + authority + NUL + normalized canonical root`. Do not expose a path-concatenated ID on the wire. + +Different linked worktree roots have different project IDs, but the first iteration must reject adding a project whose canonical `commonGitDir` matches any project already exposed in the window. Two Agent Manager contexts mutating the same Git common directory would bypass the current root-keyed worktree lock and could manage the same branches twice. This is a product-valid restriction to distinct Git projects, not a path workaround. Also change `WorktreeManager`'s write-lock key to canonical `commonGitDir` so future relaxation does not reintroduce `index.lock` races. + +Persist one unsynced global-state value under `kilo.agentManager.projects.v1`: + +```ts +interface StoredProjectCatalogV1 { + version: 1 + projects: Array<{ + id: ProjectId + uri: string + scheme: string + authority: string + root: string + commonGitDir: string + name?: string + collapsed: boolean + order: number + addedAt: string + }> +} +``` + +Do not call `setKeysForSync`; machine/remote paths must not sync to other machines. Serialize mutations in-process. Each mutation re-reads the latest value, validates it, applies add/remove/reorder/metadata changes, and performs one `globalState.update`. + +Registry behavior is exact: + +- `snapshot(flag)` derives the pinned current project fresh. It never reads a persisted active project. +- The pinned descriptor is first, has `pinned: true`, and defaults to expanded. It is not written into the catalog. +- Persisted entries matching the pinned project ID are suppressed, not deleted. They reappear as additional projects when another window has a different pinned project. +- Additional entries are filtered to the current URI scheme/authority before exposure. Entries for other authorities remain untouched in storage. +- Reading the catalog does not stat, realpath, run Git, or open per-repository state. Persisted additional entries start as `unverified`. +- Adding validates/canonicalizes the chosen folder, rejects the pinned project, duplicate IDs, duplicate common Git directories, and authority mismatches, then persists it. A newly added project may be marked expanded and explicitly activated by the caller; listing alone never initializes it. +- A missing or no-longer-Git root is marked unavailable only when expansion validates it. Keep the entry and its metadata. Do not prune it automatically and do not create a `ProjectContext`. +- Removing is allowed only for non-pinned projects. It removes only the catalog entry after the context is idle/disposed; it never deletes `.kilo`, worktrees, sessions, branches, or shares. +- On every extension/panel restart, the active selection is the pinned project's Local context. Persisted additional projects and collapse/order/name metadata survive, but a stale secondary active session does not override the current VS Code window. + +For a multi-root VS Code workspace, preserve current behavior by deriving the pinned project from `workspaceFolders[0]`, then canonicalize that folder to its Git toplevel. Do not silently select another workspace folder if the first is not a Git repository. + +### 2. Coordinator and immutable lazy ProjectContext + +Turn `AgentManagerProvider` into the panel-level coordinator and extract repository behavior into `project-context.ts`. Do not instantiate one current `AgentManagerProvider` per project. + +The coordinator owns: + +- the one panel and embedded `KiloProvider`/`SessionProvider`; +- `ProjectRegistry`, active `ProjectId`, exposed descriptors, and `Map`; +- a shared `Semaphore(3)` for Git/network polling across all projects; +- the one project/session router; +- one project-aware `AgentManagerVisiblePresence`; +- one `SessionTerminalManager` and one project-aware `TerminalRouter`; +- one global orchestration ingress and the global SSE/tool/status subscriptions; +- project and activation generations used to discard stale async results. + +Each `ProjectContext` is constructed once with an immutable descriptor/root/common Git directory and owns only repository-bound resources: + +- `WorktreeStateManager`, `WorktreeManager`, `SetupScriptService`, and repository `GitOps`; +- local diff functions, `WorktreeDiffController`, `WorktreeImporter`, and `RunController`; +- `GitStatsPoller`, `PRStatusBridge`/poller, and `BranchNamingController`; +- per-project cached state/stats/stale IDs, managed/open session refs, and initialization promise; +- project-specific message handlers currently embedded in `AgentManagerProvider`. + +It does not own a webview, `KiloProvider`, VS Code terminal command registrations, global connection subscriptions, or a global orchestration subscription. + +Lifecycle: + +1. `cold`: descriptor only. No state manager, manager, poller, watcher, or per-repo read exists. +2. `initializing`: one shared promise validates the location, constructs resources, performs the existing `ensureGitExclude -> state.load -> recoverWorktrees -> register routes` sequence, and captures a context generation. +3. `ready/expanded`: posts project-stamped state and permits project actions. Pollers run only when both the panel is visible and this project is expanded. Multiple expanded contexts may be ready concurrently. +4. `suspended/collapsed`: if active, the coordinator first activates pinned Local (or another explicit target). Stop stats/PR/diff polling and watchers, stop accepting new project mutations, wait for the current mutation barrier, flush state, and detach project-specific event side effects such as automatic branch naming. Keep validated session routes/tracking so a running backend session can still surface status and permission/question events without a repository read. Do not abort agents merely because an accordion collapsed. +5. `disposed`: increment generation first, stop/dispose resources, await terminal-router/run/mutation cleanup and state flush, detach routes, then remove the exact context object from the map. Late completions may neither post nor mutate a replacement context. + +Expansion of a suspended context resumes its existing in-memory state and pollers; expansion of a cold context initializes. Concurrent expansions share the same promise. Collapse during initialization records the desired collapsed state, allows non-cancellable Git/state work to finish, suppresses stale posts, then immediately suspends. + +The coordinator routes global events by canonical event directory before invoking a context. `session.status` must retain its directory. A single orchestration ingress determines the one owning context and delegates there; it rejects an unknown directory once, globally. It must never create one rejecting bridge per project. A collapsed project does not auto-initialize in response to an unsolicited orchestration event; return a typed `project_inactive` result. + +Panel close keeps existing semantics for sessions actually opened/created by that panel, but aggregates their project-qualified routes before aborting. Project collapse and flag-off transitions do not abort them. Removing a project is refused while one of its tracked sessions or Agent Manager run/mutation is busy, rather than orphaning an interaction prompt. + +### 3. One embedded KiloProvider, made project-aware + +Use one embedded `KiloProvider`. Multiple providers are not safe or useful here: + +- there is one webview and one message handler; +- each provider would own conflicting current-session/cached UI state and duplicate SSE handling; +- multiple terminal/visibility registrations would be ambiguous; +- it would not solve session identity at the shared webview boundary. + +Refactor its Agent Manager adapter around an injected project/session route service while leaving sidebar and standalone providers unchanged. The route service supplies: + +```ts +interface ProjectSessionRef { + projectId: ProjectId + sessionId: string +} + +interface SessionRoute extends ProjectSessionRef { + directory: string + generation: number +} +``` + +Internally key UI maps by `projectId + NUL + sessionId`; never use that concatenation on the wire. Keep the raw `sessionId` for SDK calls and backend presence. Maintain a reverse raw-ID index only to process backend events. If the same raw ID is observed in two projects, mark it ambiguous and reject/drop operations and events that cannot also be disambiguated by event directory. + +Required routing changes: + +- Register an explicit route for every Agent Manager session, including Local sessions at a project root. Do not omit a map entry merely because it equals the first workspace root. +- New/draft operations resolve from the outer project envelope and an explicit Local/worktree context. Unknown worktree IDs are errors; remove `TerminalRouter`'s fallback to root. +- Existing session operations resolve only from `ProjectSessionRef`. In strict multi-project mode, an unknown session never falls back to `workspaceFolders[0]` or the currently active project. +- Child/subagent sessions inherit the parent's project and directory. A directory-bearing event must also pass the owning-context check before registration. +- Session list refresh runs per project root plus that project's worktree directories. It emits sessions stamped with that project; it does not infer one canonical `projectID` from the first root and merge all projects into it. +- Replace singleton Agent Manager use of `KiloProvider.projectID` with the route table/directory ownership check. Keep existing project filtering for non-Agent-Manager provider instances. +- On active-project/session change, call one `activateProject(projectId, root)` path. It updates `projectDirectory`, invalidates/refetches project-scoped config, providers, agents, commands, skills, MCP, indexing, sandbox, and Git status for that root. Guard every async refresh with activation generation so project A cannot populate caches after switching to B. +- Every session create, continuation, prompt, permission/question response, terminal operation, diff operation, file/context operation, and share/unshare SDK call receives the resolved explicit directory. + +This picks up each secondary project's config naturally through the CLI's directory-scoped instance/config resolution. Do not copy provider/config/settings data into global storage. + +### 4. Project-qualified protocol and share semantics + +Define an internal/wire envelope in `agent-manager/types.ts` and mirror it in `webview-ui/src/types/messages/agent-manager.ts`: + +```ts +interface ProjectStamp { + projectId: ProjectId + generation: number +} + +interface ProjectEnvelope { + type: "agentManager.projectMessage" + project: ProjectStamp + payload: T +} +``` + +Use project-qualified references for sessions, worktrees, and drafts: + +```ts +interface SessionRef { projectId: ProjectId; sessionId: string } +interface WorktreeRef { projectId: ProjectId; worktreeId: string } +interface DraftRef { projectId: ProjectId; draftId: string } +``` + +Raw IDs remain in each repository's existing state file. The repository/context supplies the project identity when state is loaded. Do not migrate session IDs into global state and do not permit moving an existing session record between projects. A future cross-project continuation must explicitly fork/create in the target project; relabeling a raw session is invalid. + +Protocol rules: + +- The coordinator is the only component that unwraps inbound envelopes and wraps context output. +- Global catalog/route errors are global messages. Repository state, stats, session metadata, diff, run, PR, terminal, and chat messages are project envelopes. +- Preserve `requestId` inside the payload. Return a typed route error (`project_required`, `project_unknown`, `project_inactive`, `project_mismatch`, `session_unknown`, or `session_ambiguous`) without forwarding the original message. +- Validate the envelope project against the context, persisted session/worktree ownership, session route, and event/request directory. A valid raw ID in the wrong envelope is still rejected. +- Outbound generation lets the webview/coordinator discard messages from a collapsed/disposed/replaced context. +- While the flag is off, or while only the pinned project is exposed, a compatibility adapter wraps today's unqualified webview messages with the pinned project and unwraps pinned output to the existing shape. The core handlers still receive envelopes. +- Once the flag is on and a second project is exposed, unqualified project/session/worktree messages fail closed. The multi-project UI must send/consume envelopes and use composite refs as tab/store keys. + +Sharing is an operation on `SessionRef`, not on a bare session ID: + +- Manual share calls `client.session.share({ sessionID, directory })`; unshare uses the exact same resolved route. +- Automatic sharing remains a project config behavior because session creation is sent with the target project's directory. +- Store no share URL or share state in the project catalog. Session list/get/update data remains authoritative and is enriched with `projectId` on output. +- Removing/collapsing a project never unshares its sessions. A share URL is externally usable, but reopening or mutating the local session still requires its local project route. +- A mismatched or missing project cannot share/unshare, even if the raw session ID happens to resolve elsewhere. + +### 5. Feature flag, restart, and migration behavior + +Contribute an application-scoped VS Code setting, default false, for example `kilo-code.new.agentManager.multiProject`, with `scope: "application"`. Read it in the host/coordinator; do not put it in CLI `experimental` config. + +- Flag off: derive and initialize only pinned current project, use the compatibility wire adapter, and preserve the exact current view. Read but do not expose, validate, initialize, rewrite, or delete additional catalog entries. +- Flag on: publish lightweight descriptors. Only explicit expansion initializes an additional project. Selecting Local/worktree also expands and initializes its owner before activation. +- Runtime on -> off: increment activation generation, activate pinned Local, suspend all secondary contexts, switch to compatibility protocol, and preserve catalog/per-repo state. Do not abort backend sessions. +- Runtime off -> on: expose stored descriptors as cold/unverified. Do not eagerly restore a persisted secondary active project. + +No `.kilo/agent-manager.json` schema migration is needed. Existing state is loaded unchanged by the pinned context, and additional contexts load their own existing files only when expanded. An absent catalog means no additional projects. Invalid catalog entries are ignored in the snapshot and logged, but a successful write must preserve valid entries from other authorities. Never scan the filesystem to discover projects as migration. + +On restored legacy panel/webview state, unqualified tabs are accepted only in compatibility single-project mode. When multi-project mode exposes a second project, send a protocol reset and rebuild tabs/session lists from project envelopes instead of guessing which project owns a restored raw session ID. + +Remote behavior: + +- Scope catalog exposure and identity by full URI scheme + authority, not `vscode.env.remoteName` alone. +- Run Git and filesystem operations in the current extension host against `fsPath`; preserve the original URI for `openFolder` and picker operations. +- Reject selection from another authority. Keep such stored entries for their matching window/host. +- Unsupported virtual filesystems, missing roots, permission failures, and non-Git directories become unavailable descriptors after explicit validation. They do not fall back to the pinned root. + +## Ordered implementation plan + +### Slice A: mergeable routing foundation before multi-project UI + +This is the narrow first PR. It produces no secondary-project UI, but the current pinned project runs through the durable identity/routing contracts and the registry is fully tested. + +1. Add `src/agent-manager/project.ts` with IDs, locations, descriptors, stamps, envelopes, and ref types. Add `src/agent-manager/project-registry.ts` with injected storage/resolver, v1 schema validation, pinned merge, authority filtering, dedupe, and serialized mutations. +2. Extend `Host` in `src/agent-manager/host.ts`; implement global-state storage, canonical Git/URI resolution, pinned project resolution, and the application flag in `src/agent-manager/vscode-host.ts`. Preserve URIs in `openFolder`. Add the setting contribution in `packages/kilo-vscode/package.json`. +3. Add `src/agent-manager/project-session-router.ts`. It must register Local and worktree routes, resolve refs/directories, inherit child routes, validate directory ownership, detect raw-ID ambiguity, and implement compatibility versus strict mode. +4. Add envelope/route errors to `src/agent-manager/types.ts` and the mirrored webview type file. Add a coordinator adapter in `AgentManagerProvider` that derives the pinned project at panel attach, wraps all legacy inbound messages, stamps context output, and switches strictness based on exposed-project count. In this slice only the pinned context is activated, so rendered behavior remains unchanged. +5. Extend the `SessionProvider` adapter (`host.ts`, `vscode-host.ts`) with project-aware `setSessionRoute`, `trackSession`, `getSessionInfo`, `loadMessages`, `share`, and `unshare` methods. Retain temporary legacy methods only as single-project adapters, not as a second implementation. +6. Modify `KiloProvider`, `kilo-provider/options.ts`, and the extracted helpers in `kilo-provider-utils.ts` so Agent Manager Local sessions also have explicit routes, unknown strict routes fail closed, child routes inherit project identity, and project activation invalidates project-scoped cache work by generation. Keep non-Agent-Manager provider behavior unchanged. +7. Update `extension.ts` auto-approve/session-directory lookup to ask the router/coordinator for the active or unique `SessionRef`; do not merge raw ID maps and choose the first match. +8. Remove unknown-worktree-to-root fallback in `terminal-routing.ts`, and inject the pinned project root into `createTerminalHost` instead of reading `workspaceFolders[0]` internally. + +Slice A is complete when the flag-disabled Agent Manager is visually and behaviorally unchanged, every current project session has an explicit project route, and unit tests prove a second registered route cannot be accidentally handled through pinned-root fallback. Do not expose Add Project until Slice B is complete. + +### Slice B: lazy multi-context runtime, still before UI rendering + +1. Add `src/agent-manager/project-context.ts` and move root-bound state/managers/handlers out of `AgentManagerProvider`. Keep context code VS Code-free. Reduce the provider cap to the new rounded-up line count instead of raising it. +2. Make `AgentManagerProvider` the coordinator described above. Add cold/initializing/ready/suspended/disposed state, generation guards, expansion/activation APIs, shared semaphore, and per-project message dispatch. +3. Refactor status/tool/orchestration subscriptions so the coordinator routes once by project/directory. Do not retain per-context global subscriptions that can reject each other's requests. +4. Make terminal management panel-global and key mappings by composite session/worktree refs. A context supplies only an already-validated CWD. Ensure context collapse cannot switch to another project's terminal by raw ID. +5. Refactor session refresh to accept one project at a time and emit project-stamped results. Register routes for discovered Local sessions before they can be opened. Preserve failed-directory session IDs only within the affected project. +6. Change `WorktreeManager`'s mutation lock key to canonical common Git directory and reject adding another exposed context with the same common directory. +7. Add backend catalog commands (`requestProjects`, `addProject`, `removeProject`, `expandProject`, `collapseProject`, `activateProject`) behind the flag. These are testable protocol endpoints but need not yet have visible controls. + +### Slice C: first usable flagged UI + +After A and B are green, implement project accordions and the project selector in the New Worktree modal. Keep the current markup/layout when only the pinned project exists. Use composite refs in webview stores/tabs, one shared tab bar/detail pane, and clearly show the project name on cross-project session tabs and share actions. This UI slice is intentionally outside the backend-first scope. + +## Exact tests + +Add or update these focused tests under `packages/kilo-vscode/tests/unit/`: + +- `agent-manager-project-registry.test.ts`: pinned project is always first/fresh; persisted active cannot replace it; additional roots survive restart; pinned duplicate is suppressed; add canonicalizes subdirectories/symlinks; duplicate root/common Git dir and wrong authority are rejected; flag-off snapshot exposes only pinned without mutating catalog; catalog listing performs no resolver/filesystem calls; missing entries remain stored. +- `agent-manager-project-router.test.ts`: every Local/worktree session has a route; matching refs resolve; wrong project, unknown session, unknown worktree, missing project, and raw-ID collision fail closed; child inherits parent project/directory; compatibility inference works only for one exposed project; share and unshare use the same explicit directory. +- `agent-manager-project-context.test.ts`: collapsed cold projects construct/read nothing; concurrent expand initializes once; two expanded contexts use separate state roots and share the concurrency gate; collapse stops pollers and flushes; collapse during init suppresses posts; disposed-generation results cannot update a replacement context; remove is blocked while busy. +- Extend `kilo-provider-session-refresh.test.ts`: lists per project instead of merging project roots; outputs project stamps; failed worktree listing preserves only that project's sessions; activation generation drops stale config/session results. +- Extend `kilo-provider-worktree-context.test.ts`: strict unknown session/worktree has no workspace fallback; Local routes explicitly resolve to their own project root; a secondary Local session cannot resolve to pinned root. +- Extend `AgentManagerProvider.spec.ts`: single-project legacy messages are wrapped pinned; two-project unqualified messages return `project_required`; mismatched session/project returns `project_mismatch`; status/tool events dispatch by directory; restart selects pinned Local. +- Add terminal routing coverage: unknown project/worktree produces an error and no PTY/VS Code terminal; identical raw session IDs in different projects cannot select the wrong terminal. +- Extend `agent-manager-arch.test.ts`: new domain modules do not import `vscode`; lower `AgentManagerProvider.ts` maxLines after extraction. + +Run from `packages/kilo-vscode/` after each slice: + +```sh +bun test tests/unit/agent-manager-project-registry.test.ts tests/unit/agent-manager-project-router.test.ts tests/unit/kilo-provider-session-refresh.test.ts tests/unit/kilo-provider-worktree-context.test.ts +bun run typecheck +bun run lint +bun run test:unit +bun run knip +bun run check-kilocode-change +``` + +For Slice B, add the context/provider/terminal tests to the focused first command. No CLI/SDK regeneration is required because this architecture uses existing explicit-directory SDK parameters and adds only extension/webview protocol types. + +## Manual validation after the UI slice + +1. With the flag off and a non-empty stored catalog, restart VS Code. Agent Manager must look and behave exactly as today and use only the current window's Git root. +2. Turn the flag on, add a second repository, restart, and verify its header persists while the pinned current project remains first and active. Confirm the collapsed second project creates no output/state access/poll traffic until expanded. +3. Expand both projects, open Local and worktree sessions from each, switch rapidly, send prompts, answer permission/question requests, open terminals/diffs, and share then unshare. Verify every operation affects the displayed project's repository and config. +4. Disable the flag while a secondary project is active. Verify pinned Local becomes active, secondary state remains on disk/catalog, and re-enabling restores the descriptor without eager initialization. +5. Rename/remove a cataloged directory and restart. Verify the project remains as unavailable, cannot route actions to pinned root, and can be removed without touching its repository state. +6. Repeat in a remote window. Verify local/other-authority catalog entries are hidden but preserved and folder opening retains the remote URI authority. + +## Risks and rollback + +- The largest correctness risk is a hidden workspace-root fallback in `KiloProvider` or a helper. Treat every Agent Manager method that accepts `sessionID`, `worktreeId`, `draftID`, `requestID`, or directory as part of the routing audit. Tests should fail if strict mode calls `workspaceFolders[0]` for an identified operation. +- Project-scoped KiloProvider caches can show stale settings after a rapid project switch. Activation generation checks are required before posting and before replacing caches; merely cancelling transcript load is insufficient. +- Global event consumers can race. There must be exactly one ownership decision for orchestration/tool events, and directory must be retained for status/permission/question handling. +- Multiple contexts increase polling/process pressure. Share the concurrency semaphore, gate all pollers by panel visibility and expansion, and suspend on collapse/flag-off. +- Global-state updates from separate VS Code windows do not provide a transactional CAS. The first iteration should re-read before each serialized mutation and tolerate another window appearing after reload; do not build a filesystem lock or custom database for this feature. +- Rollback is safe: turn off the application flag. The pinned path and existing `.kilo/agent-manager.json` format remain intact, while the unused global catalog is preserved for a later re-enable. diff --git a/.kilo/plans/agent-manager-multi-project-sidebar-density.md b/.kilo/plans/agent-manager-multi-project-sidebar-density.md new file mode 100644 index 00000000000..28790c6a3bf --- /dev/null +++ b/.kilo/plans/agent-manager-multi-project-sidebar-density.md @@ -0,0 +1,437 @@ +# Plan: Agent Manager sidebar — shortcut badge fix + reclaim title width + +Worktree: `/Users/marius/Documents/git/kilocode/.kilo/worktrees/mewing-profit` +All paths below are relative to `packages/kilo-vscode/`. + +Rules: + +- Do exactly these edits. Do not refactor anything else. +- Solid.js, not React: `class=`, not `className=`. +- Do **not** add `kilocode_change` markers. This package is Kilo-owned and CI fails if you do. +- Do **not** add new i18n strings. None are needed. +- Out of scope: the per-project `SESSIONS` list in the tree (tracked in + https://github.com/Kilo-Org/kilocode/issues/12928). Do not touch `UnassignedSessionsSection.tsx`. + +Do task A, verify, then task B, verify, then task C. + +## Task A — Shortcut badge on the right, hidden until hover or ⌘ held + +The `⌘1` badge on "local" rows is always visible, and in multi-project mode it renders on the +*left*, between the icon and the label. Worktree rows already behave correctly. Cause: +`.am-shortcut-badge` has no default `opacity: 0` — worktree badges are only hidden because their +container `.am-wt-hover-actions` is hidden, and local rows never got that container. + +### A1 — `webview-ui/agent-manager/ProjectSidebarBody.tsx` + +Replace the whole ` +``` + +The `−` in `am-stat-deletions` is U+2212 MINUS SIGN, not a hyphen. Copy it verbatim. + +### A2 — `webview-ui/agent-manager/SidebarBody.tsx` + +Same bug in legacy single-project mode, but the badge is already last, so only the wrapper is +missing. + +Insert **before** line **127** (``): + +```tsx +
+``` + +Replace line **182**: + +```tsx + {isMac ? "⌘" : "Ctrl+"}1 +``` + +with: + +```tsx +
+ {isMac ? "⌘" : "Ctrl+"}1 +
+
+``` + +Result: one `am-wt-actions-cell` div containing the skeleton ``, the stats ``, and the +hover-actions div, closing before ``. Prettier fixes indentation in task D. + +### A3 — `webview-ui/agent-manager/agent-manager.css` + +Delete lines **569-575** and replace with the rule that actually works: + +```css +.am-local-item .am-shortcut-badge { + right: 8px; +} + +.am-local-item:hover .am-shortcut-badge { + opacity: 1; +} +``` + +becomes: + +```css +.am-local-item:hover .am-wt-hover-actions { + opacity: 1; + visibility: visible; +} +``` + +(`right: 8px` never applied — the badge is `position: static`. The `opacity: 1` never did anything +because nothing set `opacity: 0` first.) + +### A4 — same file, delete lines **609-611** entirely, add nothing: + +```css +.am-show-shortcuts .am-local-item .am-shortcut-badge { + opacity: 1; +} +``` + +The rule at lines 597-600 is not scoped to worktree items, so after A1/A2 it already covers local +rows. + +### A5 — same file, lines **986-989**, add `visibility: hidden` to match worktree behaviour: + +```css +.am-local-item:hover .am-worktree-stats, +.am-local-item:hover .am-worktree-stats-skeleton { + opacity: 0; + visibility: hidden; +} +``` + +### Verify A + +```bash +cd packages/kilo-vscode && bun run typecheck && bun run lint +``` + +## Task B — Stop reserving width for invisible content + +`.am-wt-actions-cell` is a grid whose children all stack in cell 1/1, so the cell is permanently as +wide as its widest child. `visibility: hidden` does not remove layout, so the invisible hover +actions (~42px) and the loading skeleton (~48px) reserve width on every row forever. That is why +titles truncate ~40px before the right edge. + +All edits in `webview-ui/agent-manager/agent-manager.css`. + +### B1 — Take hover actions out of grid flow + +Lines **475-482**. Add the four `position` lines at the top, keep everything else: + +```css +.am-wt-hover-actions { + position: absolute; + top: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 2px; + opacity: 0; + visibility: hidden; +} +``` + +Absolutely positioned children never size grid tracks. `.am-wt-actions-cell` is already +`position: relative` (line 464). Do not edit the `.am-wt-actions-cell > *` rule at lines 469-472. + +### B2 — Same for the loading skeleton + +Add immediately after the rule from B1: + +```css +/* Skeleton is a placeholder, not real content — it must not reserve title width. */ +.am-wt-actions-cell > .am-worktree-stats-skeleton { + position: absolute; + top: 0; + right: 0; + bottom: 0; +} +``` + +Do not edit the base `.am-worktree-stats-skeleton` rule (lines 744-748) or `.am-pr-badge-skeleton`. + +After B1+B2 the cell is sized only by `.am-worktree-stats` and `.am-worktree-delete-hint`, which +are the only things that should reserve space. + +### B3 — Fade the title under the overlaying actions + +The actions now overlay the row instead of sitting in reserved space, so a long title would render +behind them. Same fix the codebase already uses for the local branch at lines 577-580. Add after +B2: + +```css +.am-worktree-item:hover .am-worktree-branch { + mask-image: linear-gradient(to right, black calc(100% - 48px), transparent 100%); + -webkit-mask-image: linear-gradient(to right, black calc(100% - 48px), transparent 100%); +} +``` + +### B4 — Remove one layer of nested padding + +Lines **365-370**, change `padding: 0 6px;` to `padding: 0;`: + +```css +.am-project-body { + display: flex; + flex-direction: column; + min-height: 0; + padding: 0; +} +``` + +Gains 6px per side on every row in a project. Rows keep their own 10px inset +(`.am-local-item` line 106, `.am-worktree-item` line 425) so they stay indented under the project +header. Do not change those, and leave `.am-project-body .am-section-header` (lines 273-275) alone. + +### B5 — Remove the remaining outer list gutters in multi-project mode + +The sidebar keeps 8px horizontal padding for the header controls, but the project list should use +the full width up to its scrollbar. In the same CSS file, extend `.am-projects-list` with: + +```css +.am-projects-list { + display: flex; + flex-direction: column; + min-height: 0; + overflow-y: auto; + margin-inline: -8px; +} +``` + +Do not remove padding from `.am-sidebar` itself. This makes project cards and rows reach the +scrollbar without moving the `PROJECTS` header and its controls to the edge. + +### Verify B + +```bash +cd packages/kilo-vscode && bun run typecheck && bun run lint +``` + +## Task C — Show where a palette session lives + +In the ⌘F search palette, multi-project session results show only the project name, so you cannot +tell whether a session is in a worktree or at the project root. + +File: `webview-ui/agent-manager/ProjectList.tsx`. In the session loop (lines **90-106**), add the +two `const` lines and change `meta` and `search`: + +```tsx + for (const session of props.sessions[project.id] ?? []) { + const wt = session.worktreeId ? state.worktrees.find((item) => item.id === session.worktreeId) : undefined + const where = wt ? wt.label || wt.branch : props.t("agentManager.local") + items.push({ + key: `${project.id}:session:${session.id}`, + projectId: project.id, + kind: "session", + group: "sessions", + title: session.title || props.t("agentManager.session.untitled"), + meta: [project.label, where], + search: [project.label, where, wt?.branch, session.title, session.id].filter(Boolean).join(" "), + updatedAt: session.updatedAt, + state: "idle", + visible: project.expanded, + sessionId: session.id, + location: session.worktreeId ? "worktree" : "local", + worktreeId: session.worktreeId ?? undefined, + }) + } +``` + +`state` is already in scope (line 55) and already null-checked (line 56). `meta` is joined with +` · ` by the renderer (`SidebarSearchMenu.tsx:143`), so no separator work is needed. + +## Task E — Align the project heading with the row icons + +Every row in the projects tree used a different left inset, which made the sidebar look busy and +indented for no reason. Measured against a full-bleed `.am-projects-list` (starting at x=0): + +| Element | Before | After | +|---|---|---| +| `PROJECTS` label | 16px | 10px | +| Project chevron | 12px | 10px | +| `WORKTREES` / `SESSIONS` label | 6px | 10px | +| `.am-local-item` icon | 10px | 10px | +| `.am-worktree-item` icon | 10px | 10px | + +Changes in `agent-manager.css`: + +- `.am-local-item` padding `8px 10px` → `8px 6px` +- `.am-worktree-item` padding `6px 10px` → `6px 6px` +- `.am-project-item` padding `6px 8px 6px 12px` → `6px 6px` +- `.am-project-body .am-section-header` padding-left `6px` → unchanged at `6px` +- `.am-projects > .am-section-header` gets `padding-left: 2px`, because that heading sits inside + `.am-sidebar`'s own 8px padding rather than in the pulled-out list + +The leading columns carry no padding of their own (`.am-sidebar-header-toggle` and +`.am-sidebar-header-chevron` are bare 16px boxes), so row padding is the only lever. + +### Symmetric gutter + +`.am-projects-list` uses `margin-left: -4px; margin-right: -4px`, pulling out of `.am-sidebar`'s +8px padding to an even 4px gutter. An earlier attempt used `-8px` on the left for true full bleed, +but that is wrong twice over: the resize handle's inner half then covered every card, so card +clicks started a resize, and a selected row's `border-radius: var(--radius-sm)` background clipped +flat against the window edge while still being inset on the right, which read as a bar bleeding off +the sidebar rather than a card. + +4px is also the most reclaimable on the right. The handle's hit area is 8px wide centered on the +border, reaching 5px back into the content area (255-263 in a 260px sidebar). At this gutter the +row's hover actions end at 249, leaving 6px of clearance; anything tighter puts row buttons under +the handle and turns clicks into resize drags. + +### Two tab stops + +Reducing the insets exposed that labels sat at four different offsets: the non-collapsible +`WORKTREES` heading at 6px (it passes no `onToggle`, so `SidebarSectionHeader` renders no chevron), +collapsible headings at 28px, worktree card text at 30px, and local card text at 32px because +`.am-local-icon` was 18px wide while `.am-wt-icon` held a 16px glyph. + +Normalized to one 16px leading column with an 8px gap, giving exactly two tab stops: + +- `.am-sidebar-header-main` gap `6px` → `8px`, matching the card icon gap +- new `.am-project-body .am-sidebar-header:not(.am-sidebar-header-toggleable) .am-sidebar-header-main { padding-left: 24px }` + so a heading with no chevron still reserves the column +- `.am-local-icon` `18px` → `16px` +- `.am-wt-icon` gains `width: 16px` and `justify-content: center` + +Measured result at 260px: + +| Tab stop | Elements | +|---|---| +| 10px | `PROJECTS` label, project chevron, local icon, worktree icon, `SESSIONS` chevron | +| 34px | project name, `WORKTREES` label, `SESSIONS` label, local text, worktree text | + +``` +cardLeftGap=4 | cardRightGap=4 | actionsRight=249 | handleZoneFrom=255 +``` + +`PROJECTS` intentionally stays at the 10px glyph stop rather than being pushed to 34px: it is the +root heading, so indenting it further than the projects beneath it would invert the hierarchy. + +### Rejected: a per-project icon in the heading + +Superset shows a GitHub **owner** avatar per project (`https://github.com/{owner}.png?size=64`, +falling back to the project's initial). Our webview CSP already allows `https:` images, so it was +feasible, but it does not fit this repo set: of the local projects, ~17 are `Kilo-Org` remotes and +would share one identical Kilo logo, and ~20 have no git remote at all and would show nothing. +An owner avatar answers "who owns this", which is not the question the sidebar needs answered. +Superset's repo-file scanner (`favicon-discovery.ts`) is dead code there, so it was not an option +worth copying either. Left out entirely as out of scope. + +## Task D — Final checks + +From `packages/kilo-vscode`: + +```bash +bun run format +bun run typecheck +bun run lint +bun run test:unit +bun run check-kilocode-change +bun run compile +``` + +Then create `.changeset/agent-manager-sidebar-density.md`: + +```md +--- +"kilo-code": patch +--- + +Fix the Agent Manager sidebar keyboard shortcut badge so it appears on the right edge of local rows and only while hovered or holding the jump modifier, give worktree titles more room by no longer reserving space for hidden row actions, and show which worktree a session belongs to in the search palette. +``` + +## Visual regression baselines + +Task A and B change existing snapshots: `WorktreeItemDefault`, `WorktreeItemActive`, +`WorktreeItemPendingDelete`, `WorktreeItemStale`, `WorktreeItemWithStats`, `WorktreeItemGrouped`, +`SidebarSearchOpen`, `MultiProjectSidebar` in `webview-ui/src/stories/agent-manager.stories.tsx`. + +**Do not run or update visual regression tests.** `tests/visual-regression.spec.ts` is skipped on +macOS, so you cannot produce valid Linux baselines locally. CI regenerates them and may push a +baseline commit. If a push is then rejected, do **not** `git pull --rebase`; run +`git fetch && git push --force-with-lease`. + +## Manual test + +Enable `kilo-code.new.experimental.multiProject` in Kilo Settings → Experimental, add a second +project, open Agent Manager (`Cmd+Shift+M`). + +1. Nothing hovered: no `⌘N` badge visible anywhere. +2. Hover a `local` row: badge appears at the **right** edge, git stats fade out. +3. Hold ⌘: badges appear on all local and worktree rows, all right-aligned. Release: all gone. +4. Worktree rows with no git changes show noticeably longer titles than before. Hover one with a + long title: it fades under the badge and trash button instead of colliding. +5. ⌘F: session results read ` · `. +6. Turn the experimental setting off: the `local` row's `⌘1` badge is hidden until hover. + +## Known issue, not fixed here + +`⌘1`–`⌘9` index the full nav order, which includes session rows that render no badge +(`navigate.ts:214-218` + `section-helpers.ts:147-153`). So with 4 worktrees you see `⌘1`–`⌘5` and +`⌘6`–`⌘9` silently land on unlabelled session rows. Deliberately left alone: fixing it means +changing jump semantics and rewriting `tests/unit/navigate.test.ts:745-757`, and it becomes moot +once #12928 moves sessions out of the tree. diff --git a/.kilo/plans/agent-manager-multi-project-uniform-ui.md b/.kilo/plans/agent-manager-multi-project-uniform-ui.md new file mode 100644 index 00000000000..5beb0e3ef22 --- /dev/null +++ b/.kilo/plans/agent-manager-multi-project-uniform-ui.md @@ -0,0 +1,740 @@ +# Agent Manager multi-project uniform UI architecture + +**Status:** Ready, corrected architecture; implementation pending + +**Date:** 2026-07-22 + +This plan supersedes the active-body plus background-summary UI described in `agent-manager-multi-project-runtime.md`. The existing project registry, immutable project-root concept, per-project state payloads, and project-tagged stats work are useful foundations. The current read-only `ProjectSummary` and active-project dynamic getter routing are not the target architecture. + +Configuration reads, writes, scopes, drafts, trust, revisions, and release-blocking tests are specified canonically in [`agent-manager-multi-project-configuration.md`](./agent-manager-multi-project-configuration.md). If this plan and that document differ on configuration behavior, the configuration document wins. + +The step-by-step implementation sequence from the current branch is [`agent-manager-multi-project-implementation-handoff.md`](./agent-manager-multi-project-implementation-handoff.md). Implementers should follow that handoff slice by slice rather than treating this architecture document as an unordered task list. + +## Product requirement + +Every expanded project permanently renders the real existing Agent Manager sidebar UI: + +- the real Local card; +- the real worktree cards, including their current menus and actions; +- the real session list, which may initially use a simplified layout but must contain real live session data; +- live git stats, PR state, run/setup state, stale state, and session state owned by that project. + +Selecting or interacting with a project may change the shared detail pane and top toolbar. It must not remove, replace, downgrade, or remount any expanded project body. There is no active-body versus background-summary rendering mode. + +The shared detail pane remains singular. The sidebar displays all expanded projects concurrently; the detail pane displays one selected project/local/worktree/session target at a time. + +## Architecture verdict on the current worktree + +The current implementation is a valid partial runtime foundation, but it is not safe to extend directly into fully interactive background cards. + +Keep and evolve: + +- `ProjectRegistry` and the pinned-workspace project concept; +- immutable root ownership in `ProjectContext`; +- `initContextState` extraction; +- project-stamped state/stats/PR messages; +- per-project webview stores; +- reusable `WorktreeItem`, `SectionHeader`, `WorktreeSectionActions`, and `UnassignedSessionsSection` components; +- the application-scoped feature flag. + +Replace or refactor before exposing interactive multi-project UI: + +- delete `ProjectSummary`; do not add more behavior to it; +- remove the rule that only `project.active` renders `renderBody()`; +- stop routing normal actions through `contexts.active()` and optional `projectId`; +- replace raw session/worktree/local UI keys with project-qualified references; +- replace the active singleton poller plus background poller split with uniform context-owned polling; +- make context initialization, mutation, suspension, and disposal explicit and generation-guarded; +- make the one embedded `KiloProvider` project/session-route aware. +- separate activation-bound runtime config from the Settings editor's immutable read/write binding. + +## Core model + +### Project identity + +A project is one canonical Git repository root in the current extension host. + +```ts +type ProjectId = string + +interface ProjectRef { + projectId: ProjectId +} + +interface WorktreeRef extends ProjectRef { + worktreeId: string +} + +interface SessionRef extends ProjectRef { + sessionId: string +} + +type SidebarTarget = + | { projectId: ProjectId; kind: "local" } + | { projectId: ProjectId; kind: "worktree"; worktreeId: string } + | { projectId: ProjectId; kind: "session"; sessionId: string } +``` + +Raw backend and repository-local IDs remain unchanged. Composite references are required at every panel/runtime boundary so identical local sentinels, worktree IDs, section IDs, or session IDs cannot cross projects. + +Project identity must include the extension-host URI scheme and authority in addition to the canonical root. Adding a project must also resolve canonical `git-common-dir` and reject another exposed project sharing that common Git directory. Linked worktrees of one repository cannot be registered as independent projects in the first release. + +### Active project means detail selection only + +`activeProjectId` is not a rendering mode and not an implicit mutation target. + +It means: + +- which project owns the current shared detail/chat/diff/terminal pane; +- which project the global top toolbar and global New Session/Run buttons target; +- which project/worktree directory supplies runtime-effective Kilo configuration in the one embedded `KiloProvider`. + +It never selects a Settings write target. Settings bindings are explicit and remain unchanged when detail selection changes. + +Project expansion controls rendering. Every `project.expanded` project renders one stable `ProjectSidebarBody` keyed by `projectId`, regardless of active status. + +### Atomic selection + +Do not send `selectProject` followed by an unqualified worktree/session action. VS Code message handlers are asynchronous and are not a transaction queue. + +Use one atomic selection intent: + +```ts +interface ActivateSelectionMessage { + type: "agentManager.activateSelection" + target: SidebarTarget +} +``` + +The coordinator validates the project and target, ensures the context is ready, activates the project-specific Kilo route, then emits one project-qualified activation result. The sidebar body stays mounted; only shared detail state changes. + +Mutations that do not need the detail pane, such as rename, delete, section edits, run, setup, open window, PR actions, or worktree creation, execute directly against their explicit `ProjectRef` or `WorktreeRef`. They do not activate another project as a side effect. + +## Configuration architecture + +See [`agent-manager-multi-project-configuration.md`](./agent-manager-multi-project-configuration.md) for the complete contract. + +### Verified blocking failure + +The current config protocol is unsafe for multi-project use: + +- `KiloProvider.fetchAndSendConfig()` resolves a mutable current directory and emits one unqualified `configLoaded` value (`packages/kilo-vscode/src/KiloProvider.ts:2513`). +- `ConfigProvider` owns one global/project/effective draft and sends an unqualified `updateConfig` (`packages/kilo-vscode/webview-ui/src/context/config.tsx:54`, `:243`). +- `KiloProvider.handleUpdateConfig()` resolves `getWorkspaceDirectory()` again at save time (`packages/kilo-vscode/src/KiloProvider.ts:2982`). A draft loaded for project A can therefore be written to project B after detail activation changes. +- The backend derives the project target from request directory and has no target or revision precondition (`packages/opencode/src/config/config.ts:944`, `packages/opencode/src/kilocode/config/config.ts:67`). Concurrent editors can overwrite one another, and a newly created higher-priority config file can redirect a pending write. +- Hidden scope splitting currently writes `commit_message` and `indexing.enabled` to the project while most fields go global (`packages/kilo-vscode/webview-ui/src/utils/config-scope.ts:3`). The UI does not make this target choice explicit. +- Project config writes also exist outside the save bar. In particular, provider disconnect can call `saveProject()` using the mutable provider workspace directory (`packages/kilo-vscode/src/provider-actions.ts:252`). Disabling only `handleUpdateConfig()` is insufficient. + +This is a release blocker, not a follow-up cleanup. + +### Product decision and staged order + +Preserve useful project-local editing such as `commit_message.prompt` and repository indexing rules. Indexing enablement is machine-local project consent, default off, and cannot be granted by repository config. Replace hidden active-directory targeting with explicit `User | Project` scope, a required Settings project selector for Project scope, an opaque immutable binding, and optimistic concurrency by target revision. + +Do not maintain one write model for flag-off mode and another for multi-project mode. Single-project mode uses the same explicit protocol, with a narrow adapter that injects the pinned `ProjectRef` for reads and open-file actions. + +### Separate runtime config from Settings editor config + +The current `ConfigProvider` conflates two different state machines. Split them: + +- **Runtime config** is read-only UI state for the selected detail/session route. It is keyed by `{ projectId, directory, activationGeneration }`, follows atomic detail activation, and drives chat, providers, agents, features, display, MCP, indexing, and sandbox behavior. +- **Settings editor config** is a scoped layer editor keyed by an immutable binding. It never follows detail activation and its draft is never applied optimistically to runtime config. + +Use distinct messages. Names may follow repository conventions, but the fields and invariants are mandatory: + +```ts +interface RuntimeConfigLoaded { + type: "runtimeConfigLoaded" + route: { + projectId: ProjectId + directory: string + activationGeneration: number + } + config: Config + features: FeatureFlags +} + +type ConfigScope = "global" | "project" + +interface ConfigTarget { + scope: ConfigScope + path: string + revision: string + exists: boolean + writable: boolean +} + +interface SettingsBinding { + id: string + connectionGeneration: number + scope: ConfigScope + project?: { + projectId: ProjectId + root: string + generation: number + } + directory: string + target: ConfigTarget +} + +interface ReadSettingsConfig { + type: "settingsConfig.read" + requestId: string + scope: ConfigScope + projectId?: ProjectId +} + +interface SettingsConfigSnapshot { + type: "settingsConfig.snapshot" + requestId: string + binding: SettingsBinding + preview?: { + projectId: ProjectId + root: string + generation: number + directory: string + } + targetConfig: Config + effective: Config + global: Config + project: Config + fields: Record + collections: Record +} + +interface WriteSettingsConfig { + type: "settingsConfig.write" + requestId: string + bindingId: string + set: Partial + unset: string[][] +} +``` + +For a global read, `projectId` is optional preview context used only to explain project shadowing. It never changes the global target. A project read requires `projectId` and always resolves the immutable `ProjectContext.root`, not the active worktree or session. + +For User scope, preview identity is presentation state, not part of the editable target identity. The controller keeps the global target binding and draft separate from the latest project preview. A clean preview change may replace both from one fresh snapshot. A dirty preview change updates only `preview`, effective/source metadata, and project raw data. It retains the original global target revision; if the fresh read reports a different global path or revision, mark the draft stale instead of silently rebasing it. + +`targetConfig` is the parsed raw content of the one file the API will patch. It is not the aggregate `global` or `project` layer. The form edits this raw target value so it never copies values inherited from another file into the target. The aggregate values remain in the snapshot solely for source, inheritance, and effective-preview UI. Existing JSONC comments remain server-side raw text and are preserved by patching; the webview does not round-trip serialized file contents. + +Parse `targetConfig` from raw JSONC without expanding `{env:}` or `{file:}` variables, so the Settings protocol never turns placeholders into secret values. If the exact target is syntactically or structurally invalid, return diagnostics and `writable: false` for form editing rather than treating it as `{}` and overwriting it. The recovery action is **Open file**. Source metadata must cover every path rendered by Settings, not only the current hand-maintained overlay field list. + +The extension owns an in-memory binding registry. Binding IDs are opaque, unpredictable, and scoped to one provider/webview lifecycle. The webview returns only `bindingId`, not a client-authoritative path or project envelope. On write the extension must: + +1. look up `bindingId` and reject unknown or expired bindings; +2. verify that the project still exists, has the same context generation, and remains trusted; +3. capture the stored binding before the first `await`; +4. send the stored directory and expected target to the backend without calling `getWorkspaceDirectory()`, `contexts.active()`, or any fallback; +5. route success or failure by `{ requestId, binding.id }` only. + +A binding expires after a successful save, backend reconnect, trust revocation, project removal, or context generation change. Success returns a new snapshot and binding. Cached snapshots are keyed by binding/scope/project identity; there is no singleton `cachedConfigMessage` for Settings. + +### Backend read/write and optimistic concurrency contract + +Evolve the Kilo-owned `/config/overlay` API rather than adding project identity to generic `Config.update`. The backend does not know Agent Manager `projectId`; it enforces the routed directory and filesystem target while the extension enforces project identity. + +`GET /config/overlay` continues to take explicit `directory` and `scope`, but returns: + +```ts +interface ConfigOverlaySnapshot { + context: { directory: string; worktree?: string } + scope: ConfigScope + targetConfig: Config + effective: Config + global: Config + project: Config + sources: ConfigSource[] + targets: { + global: ConfigTarget + project: ConfigTarget + active: ConfigTarget + } + fields: Record + collections: Record +} +``` + +The revision is a SHA-256 fingerprint of scope, canonical target path, existence marker, and exact file bytes. It is not an mtime. Exact bytes are required so comment-only JSONC edits and delete/recreate cycles with different content cause conflicts. The missing-file revision includes the intended canonical path. An ABA delete/recreate with identical bytes may retain the same revision; that state is content-equivalent and does not lose a user edit. + +`PATCH /config/overlay?directory=...` accepts exactly one scope per request: + +```ts +interface ConfigOverlayWrite { + scope: ConfigScope + set?: Record + unset?: string[][] + expected: { + path: string + revision: string + } +} + +interface ConfigOverlayWriteResult { + outcome: "applied" | "applied_but_overridden" + changed: boolean + overriddenPaths: string[][] + snapshot: ConfigOverlaySnapshot +} +``` + +The server performs the following transaction: + +1. Resolve the authoritative target from the routed instance directory and requested scope. Never accept `expected.path` as an arbitrary destination. +2. Canonicalize and compare the resolved and expected paths. Reject a changed target, including a newly created higher-priority config file. +3. Acquire a cross-process lock keyed by canonical target path. Project writes must use the same locking discipline already used for global config. +4. Inside the lock, re-resolve the target, read exact bytes, recompute the revision, and reject a mismatch. +5. Apply `set`/`unset` to the raw target layer, preserve JSONC comments where supported, validate the result, and atomically replace the target using a temporary file in the same directory. Do not expose a partially written config. +6. Invalidate the affected config instances, reload the overlay, and return the authoritative post-write snapshot. Do not fabricate optimistic config if refresh fails. +7. Emit a config event containing `scope`, routed `directory`, canonical `target`, and new `revision`. Global changes invalidate all runtime config caches; project changes invalidate only matching directory/project caches. + +`applied_but_overridden` is success. For each `set` leaf, the refreshed overlay identifies whether a higher layer still wins, such as project config shadowing a user value or managed/runtime config shadowing a project value. The UI keeps the saved value and explains why effective behavior did not change. + +Do not send global and project patches in one webview message or pretend that two files save atomically. User and Project scope maintain independent drafts and save independently. VS Code extension preferences such as autocomplete settings are a third, visibly separate store with their own request acknowledgements. + +Domain actions outside Settings must not perform client-side read-modify-write against a mutable effective config. Provider/custom-provider/work-style actions are User-global operations and should call either the same revision-aware User endpoint or a narrow backend mutation that merges named fields under the target lock. Permission Dock "always" rules remain a narrow global rule mutation tied to the exact pending request/session route; that session directory validates the request but does not select a project config target. All such mutations emit the revisioned config event, so a dirty Settings editor becomes stale rather than being overwritten. + +Expected typed failures are: + +- `unknown_project` or `stale_project_generation` from the extension; +- `untrusted_project` or `workspace_untrusted` from the extension; +- `binding_mismatch` or `binding_expired` from the extension; +- HTTP 409 `config_target_changed` with the current target descriptor; +- HTTP 409 `config_revision_conflict` with the current target descriptor and fresh snapshot when available; +- HTTP 403 `config_target_not_writable` or project target escaping its trusted root; +- HTTP 422 `config_invalid` with schema/parse details; +- an I/O failure with no success acknowledgement. + +Every failure preserves the draft and names the scope/project/path. A config event is never treated as confirmation of the user's save; only a matching write response may clear that draft. + +### Draft and project-switch behavior + +- Detail activation from project A to B does not change the Settings selector, binding, values, draft, save state, or target. +- A User Settings draft is global and survives all detail-project switches. +- Project scope uses an explicit trusted project selector that never follows detail activation automatically. +- On first open, the selector may initialize once from the pinned/selected project for convenience, but that choice is immediately captured as Settings state. +- Choosing another Settings project with a dirty draft prompts `Save`, `Discard`, or `Stay`. Drafts are never carried to a different project. +- Clean selector changes issue a new request and ignore out-of-order responses by `requestId`. +- A save captures one binding. Switching visible settings while it is in flight does not redirect it; the result updates only the original binding store and identifies that target in its notification. +- External config events refresh a clean binding. For a dirty or saving binding they mark it stale and show a changed-on-disk banner without replacing local edits. +- A 409 preserves the draft and disables blind retry. The first version offers `Reload`, `Open file`, and `Discard`; it does not auto-merge or force overwrite. A future three-way merge may use the original snapshot, fresh snapshot, and draft. + +### Scope UX + +The first release presents explicit `User | Project` scope, a required selector in Project scope, source badges, and separate binding-keyed drafts. The selected scope applies to the entire operation. Remove hidden `splitConfigByScope`; no setting silently chooses a different file. Inherited user values remain visible in Project scope with `Override`/`Reset to inherited` affordances backed by `set`/`unset`. + +### Project root, worktree, and trust semantics + +- A Project Settings binding targets the registered checkout root held by `ProjectContext.root`. It never targets the last selected Local/worktree/session card. +- Runtime config remains directory-correct: Local uses the project root, and a worktree session uses its exact routed worktree directory. Branch-specific config in a managed worktree can therefore differ at runtime. +- Editing a project's root config does not claim to update already-created worktree checkouts. Editing a worktree config from Settings is out of scope for the first Project Settings release. A future worktree editor must use an explicit `WorktreeRef` and its own immutable binding. +- The backend resolver may choose an existing root or `.kilo`/`.kilocode` config according to current precedence, but the expected target must remain within the trusted project checkout after canonical/symlink resolution. A symlink escape is read-only and must not be patched through the API. +- For an existing target, canonicalize the file with `realpath`. For a missing target, canonicalize its nearest existing ancestor and append the remaining path components before checking containment. Global writes must similarly remain under the canonical `Global.Path.config` root. Do not rely on a lexical prefix check. +- Additional Agent Manager projects must be registry-trusted before project overlay evaluation, opening/creating project config, or project writes. Global User Settings remain editable without trusting a project. +- The pinned project is trusted for config evaluation only when the VS Code workspace is trusted. Do not treat `pinned` as an unconditional trust grant. +- Trust is stored outside the repository in the extension registry/global state. A project config file cannot grant its own trust. Revoking trust invalidates all bindings and project runtime caches. + +### Concrete implementation sequence for config safety + +1. In `packages/opencode/src/kilocode/config/overlay.ts`, replace path-only targets with revisioned target descriptors and add generic changed-path source resolution. +2. Add a Kilo-owned compare-and-swap writer under `packages/opencode/src/kilocode/config/` that resolves, confines, locks, fingerprints, patches, validates, and atomically replaces one target. Keep shared `packages/opencode/src/config/config.ts` changes to minimal marked delegation/invalidation hooks. +3. Update `packages/opencode/src/kilocode/server/httpapi/groups/config-console.ts` and `handlers/config-console.ts` with the write precondition, result, and typed error contracts. Regenerate `packages/sdk/js/`. +4. Extract a config binding controller under `packages/kilo-vscode/src/kilo-provider/` and have `KiloProvider.ts` delegate reads/writes to it. The controller receives explicit project-route resolution; it never reads mutable active workspace state during a bound operation. +5. Replace `configLoaded`, `configUpdated`, and unqualified `updateConfig` in `webview-ui/src/types/messages/` with runtime and Settings message families. Keep a temporary single-project adapter only at the protocol edge. +6. Split `webview-ui/src/context/config.tsx` into activation-bound runtime state and binding-keyed Settings editor state. Settings components consume the editor context; non-settings consumers continue to use runtime-effective config. +7. Remove `webview-ui/src/utils/config-scope.ts` after migrating every control to explicit scope. Preserve explicit project editing for commit messages and repository indexing rules; move indexing enablement to machine-local project consent. +8. Audit all extension config mutators and route user-config imports, resets, custom providers, work-style writes, project writes, and save-bar writes through a revision-aware endpoint or backend atomic field-level mutation. A project-sourced item must never fall back to an active directory write. +9. Make open-file actions take `ProjectRef`, resolve `ProjectContext.root`, verify trust, and show the exact path. For a missing target, open an unsaved document at that explicit path or require an explicit create action; never create it merely by visiting Settings. +10. Update config events and cache invalidation so scope/directory/revision are retained end to end. + +Focused tests belong in: + +- `packages/opencode/test/kilocode/server/config-overlay.test.ts` for raw target snapshots, target changes, 409 revisions, source shadowing, locks, symlink confinement, and response outcomes; +- `packages/opencode/test/kilocode/project-config-update.test.ts` for target selection and atomic writes from repository roots/nested directories; +- a new Kilo-owned backend unit test beside those files only if the compare-and-swap writer needs direct fault-injection coverage; +- `packages/kilo-vscode/tests/unit/config-utils.test.ts`, replacing singleton `ConfigState` assumptions with binding/request-aware draft tests; +- `packages/kilo-vscode/tests/unit/config-scope.test.ts`, deleted when `splitConfigByScope` is removed and replaced by tests proving one explicit scope per save; +- Agent Manager route/context tests for immutable project-root resolution, trust revocation, removal, and generation changes; +- KiloProvider protocol tests for A-to-B activation during reads and writes, out-of-order responses, cache partitioning, and auditing non-save-bar mutators. + +## One `.kilo/agent-manager.json` per project + +Yes, every project has its own repository-local state file: + +```text +project-a/.kilo/agent-manager.json +project-b/.kilo/agent-manager.json +project-c/.kilo/agent-manager.json +``` + +This is already structurally supported because `WorktreeStateManager` derives its file from its immutable constructor root. The final architecture makes this ownership explicit and safe. + +### Ownership rules + +Each `ProjectContext` owns exactly one: + +- canonical repository root and canonical Git common directory; +- `WorktreeStateManager(root)` writing only `/.kilo/agent-manager.json`; +- `WorktreeManager(root)`; +- `SetupScriptService(root)` and project run service; +- diff/import/branch-naming services; +- git stats and PR pollers; +- worktree/session/section/order/run/stale caches; +- mutation queue and initialization promise. + +The global project registry stores descriptors only. It never stores worktrees, sections, sessions, tab order, or repository behavior. The pinned project is derived from the current workspace and remains outside the registry. + +### Initialization + +The first explicit expansion of a trusted project calls one single-flight `ensureReady()` promise: + +1. validate root and Git common directory; +2. construct project-owned services; +3. update that repository's local Git exclude; +4. load only that repository's `.kilo/agent-manager.json`; +5. discover and reconcile only that repository's worktrees; +6. register Local and worktree session routes for that project; +7. start context-owned polling if the project is expanded and the panel is visible; +8. post one generation-stamped project snapshot. + +Repeated expansion must reuse the same initialization promise. Collapse/removal during initialization records the desired lifecycle transition and prevents late completion from posting or mutating a replacement context. + +### Writes and disposal + +All mutations for one project execute through that context's serialized mutation queue. An operation captures the context and generation before its first await and never re-resolves through `contexts.active()` afterward. + +Collapse: + +- leaves the project registered; +- stops pollers/watchers after current operations reach a safe boundary; +- flushes its own state file; +- retains session routes required for already-running backend sessions; +- never aborts a session merely because the UI collapsed. + +Removal: + +- never deletes `.kilo/agent-manager.json`, worktrees, branches, or sessions; +- refuses removal while a project mutation/run requires ownership, unless an explicit coordinated shutdown is implemented; +- awaits context disposal and state flush before deleting the registry descriptor; +- invalidates context generation before awaits so late callbacks are ignored. + +Panel disposal awaits all initialized contexts before shared services are disposed. + +Two VS Code windows writing the same repository state file remain the same pre-existing concurrency boundary as today's single-project Agent Manager. This plan prevents one panel from creating two contexts for the same Git common directory; cross-window file coordination is not expanded in this feature. + +## ProjectContext lifecycle + +`ProjectContext` becomes a lifecycle owner rather than only a lazy service container. + +```ts +type ProjectLifecycle = + | "cold" + | "initializing" + | "ready" + | "suspended" + | "disposing" + | "disposed" + +interface ProjectContext { + readonly id: ProjectId + readonly root: string + readonly commonGitDir: string + readonly generation: number + ensureReady(): Promise + run(operation: (ctx: ReadyProjectContext) => Promise): Promise + suspend(): Promise + dispose(): Promise +} +``` + +Every async init, poll, mutation, diff, run, import, setup, PR update, branch naming, and session operation checks context identity and generation before committing state or posting to the webview. + +## Strict project-qualified protocol + +When multi-project mode exposes more than one project, every repository operation requires project identity. + +Required project qualification includes: + +- worktree create/delete/rename/import/order/section/open/PR actions; +- Local and worktree session create/open/close/fork/promote/share/unshare actions; +- prompt, abort, permission, question, command, sandbox, and context actions; +- terminal create/input/resize/close/show actions; +- diff/watch/apply/revert actions; +- run/setup/configuration actions; +- all state/session/diff/run/terminal/setup/PR output messages. + +Missing, unknown, mismatched, inactive-when-required, or ambiguous identities return a typed route error. They never fall back to the current project or `workspaceFolders[0]`. + +Single-project mode retains a narrow compatibility adapter that injects the pinned project identity. Core handlers always operate on explicit references. + +## One KiloProvider with a project/session route service + +Keep one embedded `KiloProvider`. Multiple providers would duplicate event handling, terminal registrations, caches, and current-session state for one webview. + +Add an Agent Manager route service: + +```ts +interface SessionRoute extends SessionRef { + directory: string + generation: number +} +``` + +It must: + +- register explicit routes for every Local session at its project root; +- register every worktree session at its worktree path; +- let child sessions inherit the parent route; +- validate directory ownership; +- detect ambiguous raw session IDs; +- supply the exact directory to every SDK operation, including share/unshare; +- remove current-root and workspace-root fallback for identified Agent Manager sessions; +- refresh sessions independently for every initialized project and emit real project-stamped `SessionInfo` records. + +The shared detail pane consumes the selected `SessionRef`. Every sidebar body consumes its project's real session store. Background session UI may initially be visually simpler, but it must not display fabricated titles or placeholder session objects. + +Project activation in `KiloProvider` must invalidate and refresh project-scoped config, providers, agents, commands, skills, MCP state, indexing, sandbox state, and Git status under an activation generation guard. + +## Global services with composite keys + +Some services remain panel-global to avoid duplicate VS Code registrations, but all associations use composite references. + +- terminal mappings: `ProjectId + SessionId` or `ProjectId + WorktreeId`; +- run state: keyed by `WorktreeRef`, including project Local; +- visible/presence state: keyed by `SessionRef`; +- tabs and sidebar selection: keyed by `SidebarTarget`; +- diff/apply/revert state: keyed by `ProjectRef` and target; +- pending setup/create/delete state: keyed by project-qualified resource refs. + +Remove unknown-worktree-to-current-root terminal fallback. A context supplies a validated CWD to the global terminal manager. + +Global backend events are routed once by event directory and registered session route. Status, tool, orchestration, permission, and question events retain their directory and enter exactly one owning project context. A cold/collapsed context is not initialized by an unsolicited event. + +## Uniform project sidebar UI + +### One body implementation + +Extract the existing real `renderBody()` from `AgentManagerApp.tsx` into `ProjectSidebarBody`. Do not maintain active and background implementations. + +`ProjectSidebarBody` receives a stable project-scoped store and project-qualified action callbacks. It reuses: + +- the existing Local card markup; +- `WorktreeSectionActions`; +- `WorktreeItem`; +- `SectionHeader` and current drag/drop behavior; +- `UnassignedSessionsSection`; +- existing rename/delete/open/PR/run/setup/session affordances. + +Render it for every expanded project: + +```tsx + + {(project) => ( + + + + + + )} + +``` + +The body is keyed by `projectId`, not active status. Changing detail selection cannot remount it. + +### Per-project webview stores + +Replace active-only shared sidebar signals with `Map` or an equivalent Solid store registry. + +Each store owns: + +- state/worktrees/sections/order/stale state; +- real `SessionInfo` and managed-session state; +- git stats and local stats; +- PR and run/setup state; +- pending delete/rename/create state; +- sidebar search/order/collapse state where repository-owned; +- generation for dropping stale output. + +Only shared detail/chat/terminal state remains global. The top toolbar reads the selected detail target's project store. + +`project-live.ts` may be evolved into this complete store registry. It must stop mirroring active payloads into a separate sidebar signal set. + +### Scrolling and layout + +The project list becomes the sidebar scroll container. Individual project bodies must not use a global `50vh` cap that makes two projects fight for viewport height. Section and project collapse state controls density; normal browser scrolling exposes all expanded worktrees. + +### UI behavior + +- Clicking a Local/worktree/session card emits one atomic project-qualified selection. +- Rename/delete/run/open/section/PR actions execute against the clicked card's project without changing sidebar structure. +- The active project may receive an emphasis style, but no different markup. +- Active project chevrons are not disabled; expansion and detail selection are separate concepts. +- Global New Session/Run buttons target the last selected detail project. +- New Worktree accepts an explicit target project, defaulting to the selected detail project. + +## Uniform polling and live state + +Every ready expanded context owns the same poller set. There is no active singleton poller plus background poller split. + +Pollers emit `{ projectId, generation }` and update only their owning context/store. Stop/suspend increments generation before clearing timers so in-flight Git/PR results are dropped. + +Context state is pushed after every project mutation, not only initial expansion or active-project changes. This includes worktree/session/section/order/run/setup changes in background projects. + +Panel visibility changes poll cadence uniformly across all expanded contexts. + +## Registry correctness + +Registry mutations use one in-process queue. Each mutation re-reads persisted storage, validates it, applies one mutation, writes once, then updates memory. Trust/remove persistence failures are user-visible and do not leave UI state ahead of storage. + +Registry descriptors include canonical root, canonical Git common directory, URI scheme/authority, order, label, trust, and added time. They do not contain Agent Manager repository state. + +## Implementation order + +### Slice 0: configuration safety blocker + +1. Introduce revisioned `/config/overlay` reads and compare-and-swap writes, first for User scope and with the same generic implementation covered for Project scope. +2. Separate runtime-effective config messages/state from immutable Settings bindings and drafts. +3. Add explicit `User | Project` Settings scope and an immutable trusted project selector/binding for Project writes. +4. Remove hidden `splitConfigByScope` persistence and audit every direct/indirect config mutation, including provider disconnect, import/reset, custom providers, work-style presets, Permission Dock, and indexing. +5. Make config events scope/directory/target/revision aware and generation-guard runtime activation refreshes. +6. Add target-switch, stale-binding, revision-conflict, trust, and draft-preservation tests. + +Do not enable multi-project broadly before this slice passes. + +### Slice 1: routing and identity blockers + +1. Add project-qualified refs and strict protocol types. +2. Add the project/session route service and explicit Local routes. +3. Remove Agent Manager current/workspace-root fallback for identified operations. +4. Add atomic project-qualified selection. +5. Convert panel-global maps to composite keys. +6. Add strict routing and raw-ID collision tests. + +Do not expose interactive background controls before this slice passes. + +### Slice 2: context lifecycle and service ownership + +1. Add single-flight initialization, lifecycle state, generation, and mutation queue to `ProjectContext`. +2. Move root-bound diff/import/run/setup/branch-naming/poller ownership behind context APIs. +3. Replace dynamic active getters inside async mutations with captured contexts. +4. Add suspend/remove/dispose coordination. +5. Reject duplicate Git common directories and serialize registry writes. +6. Route global backend events by directory. + +### Slice 3: complete per-project stores + +1. Refresh and store real sessions per project. +2. Emit all relevant output with project and generation. +3. Evolve `project-live.ts` into a complete project sidebar store registry. +4. Make polling uniform and generation-safe. +5. Add background mutation/state propagation tests. + +### Slice 4: uniform real UI + +1. Extract the current `renderBody()` intact into `ProjectSidebarBody`. +2. Parameterize it by one project store and project-qualified callbacks. +3. Render one body per expanded project. +4. Delete `ProjectSummary` and its CSS. +5. Separate project expansion from detail activation. +6. Add project-aware New Worktree targeting. +7. Add component/visual tests proving no body remount or structural change on selection. + +### Slice 5: validation and release gating + +1. Run focused lifecycle/router/context tests after each slice. +2. From `packages/opencode/`, run the focused config overlay/project-update tests and CLI typecheck. Run the opencode annotation and Promise-facade guards for shared-file hooks. +3. Regenerate the JS SDK after endpoint schema changes and verify generated consumers compile; never hand-edit generated sources. +4. From `packages/kilo-vscode/`, run typecheck, lint, focused/unit tests, knip, compile/package, and architecture guards. +5. Manually exercise two repositories with multiple worktrees and sessions in both, including a dirty User draft during project activation, an external config edit conflict, project trust revocation, project removal, and project-config open targeting. +6. Keep the feature flag default off until all acceptance criteria pass. + +## Blocking tests + +The feature is not complete until tests prove: + +1. Two concurrent expands initialize one context once. +2. Collapse/remove during initialization produces no late state or poller posts. +3. A project switch during create/import/setup/session/run/diff cannot change operation ownership. +4. Project B Local sessions never resolve through project A. +5. Missing/mismatched project references fail closed. +6. Poller/PR late results after stop or generation change are dropped. +7. Same raw worktree/session/local IDs in two projects do not collide in runtime or UI stores. +8. Two expanded projects render two permanent full `ProjectSidebarBody` instances. +9. Selecting a card changes only shared detail state and active emphasis, not body structure or mount identity. +10. Every worktree/session/terminal/diff/run/setup/section/PR action targets the clicked project's context. +11. Registry writes preserve concurrent add/remove/trust changes. +12. Roots sharing one Git common directory cannot both register. +13. Panel shutdown awaits all contexts and stops background activity. +14. Flag-off behavior remains the current single-project UI and state file. +15. A Settings draft loaded for project A cannot write project B after any activation, expansion, removal, trust, or worktree switch. +16. User Settings writes target the revisioned user file; explicit Project Settings writes target only the selected trusted project's revisioned root config. No key is silently rerouted. +17. A clean external config change refreshes Settings; a dirty or saving binding is marked stale without losing its draft. +18. Concurrent editors using the same revision produce one success and one 409 conflict, with no lost update or partial file. +19. A comment-only JSONC edit, file deletion/recreation with changed bytes, or newly created higher-priority config changes the revision/target and rejects the stale write. +20. A save response clears only the matching `{ requestId, binding.id }`; an SSE config event or out-of-order read cannot confirm a save. +21. Global and project writes are independently acknowledged and never presented as one atomic transaction. +22. Project config open/read/write actions reject unknown, untrusted, removed, generation-stale, symlink-escaping, or mismatched project targets. +23. Runtime config for a selected managed worktree uses that worktree directory, while Project Settings/open-file targets the immutable registered project root. +24. Project-sourced providers and other config collections cannot trigger an implicit active-project write from Settings actions. + +## Acceptance criteria + +- No `ProjectSummary` or alternate fake-card body exists. +- Every expanded project displays real live Local, worktree, and session UI concurrently. +- Every visible worktree/session action is usable and routes by explicit project identity. +- Switching detail selection causes no sidebar body remount, replacement, collapse, or data reset. +- Each project reads and writes only its own `.kilo/agent-manager.json`. +- Expanded projects remain live independently; collapsed projects suspend safely. +- No identified operation falls back to active/current/workspace root. +- Single-project behavior and persisted state remain backward-compatible when the flag is off. +- Shared Settings exposes explicit User and Project scope; Project writes require a trusted project selector and immutable binding. +- Runtime project activation and Settings editor binding are separate state machines. +- Every Settings config write has an immutable scope/target binding and revision precondition; narrow domain mutations merge named fields under the same target lock. No config mutation uses a save-time active-directory lookup. +- Dirty drafts survive external updates and conflicts and can never migrate between projects. +- Project Settings targets the explicit registered root, never the active Agent Manager project or active worktree. + +## Planned UI + +```text +┌─ Sidebar ─────────────────────────────────┬─ Shared detail pane ──────────┐ +│ ← selected project search calendar gear │ │ +│ │ Chat / diff / terminal │ +│ PROJECTS + │ for the selected │ +│ ▾ abalone-bactrosaurus │ project/worktree/session │ +│ ┌─────────────────────────────────┐ │ │ +│ │ Local main +3295 -548 ↓54 │ │ │ +│ └─────────────────────────────────┘ │ │ +│ WORKTREES actions │ │ +│ ┌─────────────────────────────────┐ │ │ +│ │ feature-x #1234 +120 -8 ↓2 │ │ │ +│ │ real menus, rename, run, open │ │ │ +│ └─────────────────────────────────┘ │ │ +│ ┌─────────────────────────────────┐ │ │ +│ │ bugfix-y +45 -12 │ │ │ +│ └─────────────────────────────────┘ │ │ +│ SESSIONS │ │ +│ • Fix login redirect running │ │ +│ • Refactor auth │ │ +│ │ │ +│ ▾ kilo-pi-provider │ │ +│ ┌─────────────────────────────────┐ │ │ +│ │ Local master +16 -6 ↓1 │ │ │ +│ └─────────────────────────────────┘ │ │ +│ WORKTREES actions │ │ +│ ┌─────────────────────────────────┐ │ │ +│ │ provider-z #88 +88 -3 │ │ │ +│ │ real menus, rename, run, open │ │ │ +│ └─────────────────────────────────┘ │ │ +│ SESSIONS │ │ +│ • Add provider tests │ │ +│ │ │ +│ ▸ docs-repo collapsed │ │ +│ │ │ +│ [New Session] [Run] │ │ +└───────────────────────────────────────────┴──────────────────────────────┘ + +Selecting any card: + +- keeps every project body mounted and pixel-stable; +- changes only the selected styling, top toolbar target, and shared detail pane; +- never swaps a real body for a summary or vice versa. +``` diff --git a/.kilo/plans/agent-manager-new-worktree-project-selector.md b/.kilo/plans/agent-manager-new-worktree-project-selector.md new file mode 100644 index 00000000000..5f98fd086f7 --- /dev/null +++ b/.kilo/plans/agent-manager-new-worktree-project-selector.md @@ -0,0 +1,834 @@ +# Agent Manager — New Worktree Project Selector + +Status: implemented 2026-08-06. The implementation is uncommitted. + +This is Slice 4 item 6 ("Add project-aware New Worktree targeting") from +`agent-manager-multi-project-uniform-ui.md`, the last unimplemented item of that slice. +`agent-manager-multi-project-runtime.md:29` deferred it out of the backend-first scope. +Everything the extension side needs already exists; this is almost entirely a webview +change. + +Implementation notes: + +- The project catalog is passed into the dialog as an accessor so the picker reflects + registry changes while it remains open. +- The per-project default-base resolver returns `undefined` when the project has no + configured/local branch, allowing the backend-detected branch response to remain the + fallback instead of being replaced by a hardcoded `main`. +- Branch, import-result, and worktree-ready messages carry `projectId` in multi-project + mode, which makes fast project changes and cross-project creation activation safe. +- The slash-command hook accepts optional caller-owned commands; `/project` is scoped to + this dialog and is hidden when multi-project mode is unavailable. + +--- + +## Problem + +With `kilo-code.new.experimental.multiProject` enabled, the New Worktree dialog has no +notion of which repository it targets, and the user cannot see or change it. + +`Cmd+N` opens the dialog with no project at all: + +```tsx +// AgentManagerApp.tsx:1870-1876 +const showNewWorktreeDialog = () => { + if (!loaded()) return + expandSidebar() + dialog.show(() => ( + dialog.close()} defaultBaseBranch={repoDefaultBranch()} /> + )) +} +``` + +`projectId` is `undefined`, so every message the dialog sends omits it +(`agentManager.requestBranches` at `NewWorktreeDialog.tsx:321`, +`agentManager.createMultiVersion` at `:373`, `agentManager.importFromPR` at `:566`, +`agentManager.importFromBranch` at `:575`). The extension then silently falls back to the +active project in `messageProject()` (`AgentManagerProvider.ts:474`) before running the +message inside `ProjectScope`. + +The result is correct but opaque: + +- The dialog never shows which repository the worktree lands in. +- The only way to target a specific project is the per-project `+` button + (`ProjectList.tsx:136-146`), which does pass `projectId` explicitly. +- `defaultBaseBranch` is resolved from the *active* project + (`AgentManagerApp.tsx:261,272`), so even Advanced options' base-branch list and default + badge are implicitly single-project. + +The desired behavior, per the original request: the dialog should show the assigned +project and let the user change it. Defaulting to the last selected project is fine as a +default; it just must be visible and overridable. + +--- + +## Design decisions + +### Placement: inline with the tab switcher + +The selector renders inside the New/Import pill row (`NewWorktreeDialog.tsx:620-699`), +right-aligned with a constrained width, so it is shared by both tabs without adding a +full-width form row. + +``` +┌─ New Worktree ──────────────────────────────┐ +│ [ New ] [ Import ] [ folder kilocode ▾ ] │ ← inline, multiProject only +│ [ Worktree name (optional) ] │ +│ [ prompt … ] Code ▾ GPT-5.6 ▾ None ▾ │ +│ › Advanced options │ +│ VERSIONS 1 2 3 4 ⧉ Compare Models │ +│ [ Create Worktree ] │ +└──────────────────────────────────────────────┘ +``` + +Rejected alternatives and why: + +- **Inside Advanced options.** Wrong category. Advanced options holds refinements of a + known target (branch name, base branch). The project *is* the target: changing it + invalidates the branch list, base branch, default-branch badge, and setup scripts. + Hiding it also fails the stated requirement of seeing which project is assigned. +- **A full-width row directly beneath the tabs.** It covered both tabs, but consumed + unnecessary vertical space and made the project control look like a primary form field. +- **In the dialog title** (`New Worktree in [kilocode ▾]`). Reads nicely but requires + widening `Dialog`'s `title` prop to accept JSX, which touches kilo-ui for one caller. + +### Visibility + +Render the row only when `multiProject` is true. With the flag off (the default) the +dialog stays byte-identical to today, so there is no regression surface for the +all-user path. When the flag is on but only the pinned project exists, still render it: +showing the target is informative and the requirement is explicitly about seeing the +assignment. + +### Default value + +`props.projectId ?? activeProjectId()`. `activeProjectId()` already exists at +`AgentManagerApp.tsx:268` (`projectList().find((p) => p.active)?.id ?? currentProjectId()`). + +No new persistence. The active project is already the durable "last selected" state +(persisted per project as `activeTarget` in each repo's `.kilo/agent-manager.json`, plus +the registry's ordering). Adding a separate "last dialog project" key would create a +second source of truth that can disagree with the sidebar. + +### Reuse, no new CSS + +The row uses the existing `am-advanced-field` + `am-nv-config-label` + +`am-selector-wrapper` + `am-selector-trigger` markup, i.e. exactly the structure the base +branch selector already uses at `NewWorktreeDialog.tsx:813-905`, with `DeferredPopover` +(already imported) instead of `BranchSelectPopover`. + +### Component extraction + +`NewWorktreeDialog.tsx` is already 1136 lines. The selector and its popover list go into a +new `webview-ui/agent-manager/ProjectSelect.tsx` (roughly `BranchSelect.tsx`'s role): +presentational, takes `projects`, `value`, `onSelect`, and labels, and owns nothing but +its own list rendering. The dialog keeps only the signal, the popover trigger, and the +effects. + +Note: `webview-ui/agent-manager/NewWorktreeDialog.tsx` is not under a `maxLines` cap +(`tests/unit/agent-manager-arch.test.ts` caps `src/agent-manager/*.ts` only), but the file +is on the arch test's watched list and the caps exist to discourage exactly this kind of +growth. + +--- + +## Changes + +### 1. `webview-ui/agent-manager/ProjectSelect.tsx` (new) + +Presentational popover body listing projects: + +- Row = project label + dimmed root path (tooltip on the full root, matching + `ProjectsSection.tsx:60`). +- Check mark on the selected project. +- Untrusted and missing projects are disabled and carry the same affordances the accordion + uses: `lock` icon + trust hint, `warning` icon + missing hint + (`ProjectsSection.tsx:67-75`). Selecting them is not possible; trust happens in the + sidebar, not in this dialog. Keeps the dialog free of trust-flow branching. +- There is intentionally no Add project action in this picker. Project registration and + trust management stay in the Agent Manager Projects toolbar. + +### 2. `webview-ui/agent-manager/NewWorktreeDialog.tsx` + +Props change: + +```ts +export const NewWorktreeDialog: Component<{ + onClose: () => void + projectId?: string // now: initial value, not fixed target + projects?: AgentProjectSnapshot[] // omitted / empty => single-project, row hidden + activeProjectId?: string + defaultBase?: (projectId: string) => string // replaces defaultBaseBranch?: string + mode: ModeRouter +}> +``` + +`defaultBaseBranch?: string` must become a per-project lookup because each project has its +own configured default and its own local branch. `ProjectList.tsx:142` already computes +that expression (`state?.defaultBaseBranch ?? props.local[projectId]?.branch`); hoist it +into the callback so both call sites share it. + +New state and derived values: + +```ts +const [project, setProject] = createSignal(props.projectId ?? props.activeProjectId) +const [projectOpen, setProjectOpen] = createSignal(false) +const selectable = () => (props.projects ?? []).filter((p) => p.trusted && !p.missing) +const showProject = () => (props.projects?.length ?? 0) > 0 +``` + +Every outbound message switches from `props.projectId` to `project()`: +`:321` `requestBranches`, `:373` `createMultiVersion`, `:566` `importFromPR`, +`:575` `importFromBranch`. + +Reload branch data on project change, replacing the one-shot `onMount` request at `:319-321`: + +```ts +createEffect( + on(project, (id) => { + setBranches([]) + setBranchSearch("") + setHighlightedIndex(0) + setBaseBranch(null) // custom base is project-specific + setDefaultBranch(props.defaultBase?.(id) ?? "main") + setBranchesLoading(true) + vscode.postMessage({ type: "agentManager.requestBranches", projectId: id }) + }), +) +``` + +Drop stale branch replies in the `agentManager.branches` handler (`:520-525`). +`AgentManagerBranchesMessage` **already declares an optional `projectId`** +(`extension-messages.ts:939-945`, `src/agent-manager/types.ts:293-298`); the field is +simply never populated or read today. Without this guard, switching projects twice quickly +can race a wrong branch list into the base-branch popover: + +```ts +if (ev.projectId && ev.projectId !== project()) return +``` + +Also replace the `if (!props.defaultBaseBranch) setDefaultBranch(ev.defaultBranch)` guard +at `:523` — with a per-project lookup, the guard must consult +`props.defaultBase?.(project())` instead of a fixed prop. + +Preserved across a project change (all project-agnostic): prompt text and its +`advancedDialogPrompt` persistence, images, name, agent, model, variant, versions, compare +allocations, sandbox override. + +Keyboard: add `project` to `WORKTREE_PROMPT_COMMANDS` so `/project` opens the popover, +consistent with mode/model/variant/sandbox already being reachable from the dialog's slash +menu (`:302-314`). Hide it from the list when `showProject()` is false, using the same +`hidden` set mechanism already used for `agents` / `variant` / `sandbox`. + +### 3. `webview-ui/agent-manager/AgentManagerApp.tsx` + +`showNewWorktreeDialog` (`:1870-1876`) passes the catalog and a per-project default +resolver instead of a single branch string: + +```tsx + dialog.close()} + projects={multiProject() ? projectList() : undefined} + activeProjectId={activeProjectId()} + defaultBase={defaultBase} +/> +``` + +where `defaultBase(id)` reads `registry.ensure(id).defaultBaseBranch() ?? registry.ensure(id).localStats()?.branch ?? repoDetectedBranch() ?? "main"`. +`registry.ensure` and both store fields already exist +(`project/registry.ts:39`, `project/store.ts:62,67,113,123`). + +### 4. `webview-ui/agent-manager/ProjectList.tsx` + +`newWorktree(projectId)` (`:136-146`) passes the same `projects` / `activeProjectId` / +`defaultBase` props with `projectId` as the initial value, so the per-project `+` button +opens the dialog pre-scoped but still switchable. Its current inline +`state?.defaultBaseBranch ?? props.local[projectId]?.branch` expression is replaced by the +shared resolver passed down from `AgentManagerApp`. + +### 5. `src/agent-manager/worktree-importer.ts` + +Stamp `projectId` on all three `agentManager.branches` posts (`:27`, `:48`, `:54`). The +field is already in the type; the value is available from the ambient `ProjectScope` +context the message runs in (`AgentManagerProvider.ts:479`). Without this the stale-reply +guard in the webview is inert. + +### 6. Activate the created worktree when the project differs + +Creating in a non-active project currently leaves the sidebar where it is: +`createMultiVersion` never activates (no activation call in `provider-multi-version.ts`), +and the new worktree just appears in that project's accordion. That is right for the +per-project `+` button, but for `Cmd+N` where the user deliberately switched projects, +landing in the new worktree is what the flow implies. + +Post an `agentManager.activateSelection` for the first created worktree when the chosen +project differs from the active one. `activateSelection` already handles readiness, trust, +and stale-target fallback (`project/messages.ts:76-99`), so this is one message, not new +machinery. Hook it to the existing `agentManager.worktreeSetup` / `multiVersionProgress` +handling in `AgentManagerApp.tsx:1453-1471`, which already carries `projectId`. + +### 7. i18n + +New keys in `webview-ui/agent-manager/i18n/en.ts` (near the existing +`agentManager.dialog.*` block at `:130`): + +- `agentManager.dialog.project.select` — "Select project" +- `agentManager.dialog.project.untrusted` — "Trust this project in the sidebar first" +- `agentManager.dialog.project.missing` — "Repository not found" + +Then translate the four keys into the other 20 locale files in that directory via the +`translator` subagent. + +--- + +## Implementation order + +1. `ProjectSelect.tsx` with the presentational list, plus i18n keys in `en.ts`. +2. Dialog: `project` signal, prop rename to `defaultBase`, route all four outbound + messages through `project()`, render the row behind `showProject()`. +3. Dialog: `createEffect(on(project, …))` branch reload, base-branch reset, stale-reply + guard. +4. Call-site updates in `AgentManagerApp.tsx` and `ProjectList.tsx`, shared `defaultBase` + resolver. +5. `projectId` stamp in `worktree-importer.ts`. +6. `/project` slash command. +7. Post-create activation when the target project differs. +8. Locale fan-out. +9. Changeset (`minor`, user-facing): worktree creation targets an explicit project. + +--- + +## Tests + +Existing source-text unit tests already assert against this dialog and will need to stay +green: `tests/unit/new-worktree-dialog-sandbox.test.ts`, +`tests/unit/prompt-input-bidirectional.test.ts`, and the dialog entry in +`tests/unit/agent-manager-arch.test.ts`. + +New coverage: + +- The dialog posts `createMultiVersion` / `requestBranches` / `importFromBranch` / + `importFromPR` with the *selected* project id, not the prop, after a project change. +- A `agentManager.branches` reply carrying a non-current `projectId` does not mutate the + branch list (the race guard). +- Changing project clears `baseBranch` and re-derives `defaultBranch` from `defaultBase`. +- Prompt text survives a project change (no accidental reset through the shared + `advancedDialogPrompt` cache). +- The row does not render when `projects` is empty, so the single-project dialog is + unchanged. +- Untrusted and missing projects are not selectable. + +Checks to run before declaring done, from `packages/kilo-vscode/`: +`bun run typecheck`, `bun run lint`, `bun run test:unit`, `bun run knip`. + +--- + +## Manual verification + +In the isolated harness (`bun run extension:isolated`) with +`kilo-code.new.experimental.multiProject` enabled and two repositories registered: + +1. `Cmd+N` from project A shows "Project: A". Switch to B, create, and confirm the + worktree lands in B's accordion and the sidebar activates it. +2. Switch project with Advanced options open and confirm the base-branch list and default + badge follow the new project rather than showing A's branches. +3. Switch project rapidly back and forth and confirm the branch list matches the selected + project (the race guard). +4. Type a prompt, switch project, confirm the prompt is retained. +5. Use the Import tab after switching project and confirm branches and PR import target + the selected repository. +6. Turn the flag off and confirm the dialog is visually identical to today. + +--- + +## Out of scope + +- Trusting or removing a project from inside the dialog. Trust stays in the sidebar; the + dialog only disables untrusted entries. +- Any change to how the active project is persisted. +- The quick-create path (`Cmd+Shift+N` → `agentManager.createWorktree`, + `AgentManagerApp.tsx:1863-1867`). It has no dialog, so it keeps targeting the active + project. Worth revisiting only if the explicit-target rule should apply there too. +- Per-project setup-script or agent selection in the dialog. + +## Risks + +- **Stale branch data** is the only real correctness risk, and it is why the `projectId` + stamp plus the reply guard are mandatory rather than optional polish. +- **Prop signature change** (`defaultBaseBranch: string` → `defaultBase: (id) => string`) + touches both call sites; a partial migration would silently show one project's default + branch while creating in another. +- **Dialog file growth**; mitigated by extracting `ProjectSelect.tsx`. + +--- + +# Appendix: exact UI and styling specification + +Everything below is copy-paste ready. Class names, tokens, and icon names are all verified +against the current tree. Do not invent new tokens or new class names beyond the ones +listed here. + +## A. Visual layout + +``` +┌─ New Worktree ─────────────────────────────────────────── X ─┐ +│ │ +│ [ New ][ Import ] [ 📁 kilocode ⌃⌄ ] │ +│ └────────────────────────────────────────────┘ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Worktree name (optional) │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ prompt … │ │ +│ │ Code ▾ OpenAI / GPT-5.6 ▾ None ▾ ✨ 🔒 🎤 │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ › Advanced options │ +│ VERSIONS [1][2][3][4] [⧉ Compare Models] │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Create Worktree │ │ +│ └──────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────┘ +``` + +Open dropdown (anchored under the trigger, same width as the trigger): + +``` + ┌────────────────────────────────────────────┐ + │ 📁 kilocode ~/Documents/git/kilocode ✓│ ← .am-project-option-active + │ 📁 cloud ~/Documents/git/cloud │ + │ 🔒 sample-app ~/dev/sample-app │ ← disabled, 50% opacity + │ ⚠ old-repo ~/dev/old-repo │ ← disabled, 50% opacity + └────────────────────────────────────────────┘ +``` + +Rules: + +- The selector is inline with the New/Import buttons inside the tab-switcher flex row, so + it applies to New and Import alike. +- The project name and folder icon identify the scope without a separate visible label. +- The trigger is the same control as the Advanced options base-branch trigger + (`.am-selector-trigger`), so the dialog has one visual language for "pick a thing". +- The row is **not rendered at all** when `props.projects` is empty or undefined. That is + the single-project / flag-off case, which must stay pixel-identical to today. + +## B. Exact icon names + +Only these, from `packages/ui/src/components/icon.tsx`: + +| Where | `Icon name` | Notes | +|---|---|---| +| Trigger left | `folder` | Always, regardless of project state. | +| Trigger right | `selector` | Same as every other `.am-selector-trigger`. | +| Option row, normal | `folder` | | +| Option row, untrusted | `lock` | Matches the sidebar accordion affordance. | +| Option row, missing | `warning` | Matches the sidebar accordion affordance. | +| Option row, selected | `check-small` | Right-aligned. | + +All at `size="small"`. Do not use `folder-add-left`, `check`, or `plus`. + +## C. New file: `webview-ui/agent-manager/ProjectSelect.tsx` + +```tsx +// Project picker list for the New Worktree dialog + +/** @jsxImportSource solid-js */ + +import { For, Show, type Component } from "solid-js" +import { Icon } from "@kilocode/kilo-ui/icon" +import type { AgentProjectSnapshot } from "../src/types/messages" + +interface ProjectSelectProps { + projects: AgentProjectSnapshot[] + selected?: string + onSelect: (id: string) => void + labels: { untrusted: string; missing: string } +} + +export const ProjectSelect: Component = (props) => ( +
+ + {(project) => { + const blocked = () => !project.trusted || project.missing + const hint = () => { + if (project.missing) return props.labels.missing + if (!project.trusted) return props.labels.untrusted + return project.root + } + const icon = () => { + if (project.missing) return "warning" as const + if (!project.trusted) return "lock" as const + return "folder" as const + } + return ( + + ) + }} + +
+) +``` + +Notes for the implementer: + +- Wrap in `.am-dropdown-list`, not a bare fragment: that class supplies the scroll cap and + 4px padding, and `.am-dropdown [data-slot="popover-body"]` zeroes the popover padding. +- No search input. A project list is short; adding one would need keyboard nav plumbing for + no benefit. +- `props.labels` is passed in rather than calling `useLanguage()` here, matching how + `BranchSelect` and `SidebarSearchMenu` take label props. + +## D. Exact JSX inserted into `NewWorktreeDialog.tsx` + +### D.1 Imports + +Add to the existing type import block at lines 6-12: + +```ts + AgentProjectSnapshot, +``` + +Add after line 48 (`import { BranchSelect, BranchSelectPopover } …`): + +```ts +import { ProjectSelect } from "./ProjectSelect" +``` + +`Icon`, `Show`, `DeferredPopover`, `createSignal`, `createEffect` are already imported. +`on` from `solid-js` must be added to the line 5 import list. + +### D.2 Props + +Replace the component signature at lines 84-89 with: + +```tsx +export const NewWorktreeDialog: Component<{ + onClose: () => void + /** Resolves the default base branch for one project. */ + defaultBase?: (projectId: string) => string | undefined + /** Initial target project. The user can change it while the dialog is open. */ + projectId?: string + /** Full project catalog. Empty or undefined hides the project row entirely. */ + projects?: () => AgentProjectSnapshot[] + /** Project the sidebar currently has active; used as the default target. */ + activeProjectId?: string + mode: ModeRouter +}> = (props) => { +``` + +### D.3 State + +Immediately after line 101 (`const [tab, setTab] = createSignal("new")`): + +```tsx +const [project, setProject] = createSignal(props.projectId ?? props.activeProjectId) +const [projectOpen, setProjectOpen] = createSignal(false) +const projects = () => props.projects?.() ?? [] +const showProject = () => projects().length > 0 +const projectLabel = () => projects().find((p) => p.id === project())?.label ?? "" +``` + +`defaultBranch` (line 106) changes from `props.defaultBaseBranch ?? "main"` to: + +```tsx +const [defaultBranch, setDefaultBranch] = createSignal( + (project() && props.defaultBase?.(project()!)) || "main", +) +``` + +### D.4 The inline selector + +Insert inside the tab switcher after the Import button: + +```tsx +{/* Project scope — applies to both tabs. Hidden unless multi-project is on. */} + +
+
+ + + + + {t("agentManager.dialog.project.select")} + + } + > + {projectLabel()} + + + + + + + } + > + { + track("project_select", { changed: id !== props.activeProjectId }) + setProject(id) + setProjectOpen(false) + }} + labels={{ + untrusted: t("agentManager.dialog.project.untrusted"), + missing: t("agentManager.dialog.project.missing"), + }} + /> + +
+
+
+``` + +Critical details, in order of how easily they get wrong: + +1. `placement="bottom-start"`, **not** `top-start`. The rest of this dialog uses + `top-start` because those triggers sit near the bottom of the panel. This one sits at + the top, so it must open downward. +2. `portal={false}` plus the escape CSS in section E.3. Do not switch to a portal unless + the clipping fallback in E.3 is needed. +3. `sameWidth` so the dropdown matches the trigger width, consistent with the base-branch + and compare-models popovers. +4. Never send the project label, root, or id as a telemetry property. `track` takes only + the boolean shown above. + +### D.5 Reactive reload on project change + +Delete the one-shot request at lines 319-321 inside `onMount` and replace it with an effect +placed next to the other `createEffect` calls: + +```tsx +// Project scope owns the branch data, the base branch, and the default badge. +// Prompt, name, model, agent, versions and attachments are project-agnostic and survive. +createEffect( + on(project, (id) => { + if (!id) return + setBranches([]) + setBranchSearch("") + setHighlightedIndex(0) + setBaseBranch(null) + setDefaultBranch(props.defaultBase?.(id) ?? "main") + setBranchesLoading(true) + vscode.postMessage({ type: "agentManager.requestBranches", projectId: id }) + }), +) +``` + +`on(project, …)` without `{ defer: true }` runs immediately, which replaces the removed +`onMount` request. Keep the textarea focus logic in `onMount` untouched. + +In the `agentManager.branches` handler (lines 520-525), replace the body with: + +```tsx +if (msg.type === "agentManager.branches") { + const ev = msg as AgentManagerBranchesMessage + if (ev.projectId && ev.projectId !== project()) return + setBranches(ev.branches) + const id = project() + if (!id || !props.defaultBase?.(id)) setDefaultBranch(ev.defaultBranch) + setBranchesLoading(false) +} +``` + +### D.6 Outbound project id + +Four call sites change from `props.projectId` to `project()`: + +| Line | Message | +|---|---| +| 321 (now inside the effect) | `agentManager.requestBranches` | +| 373 | `agentManager.createMultiVersion` | +| 566 | `agentManager.importFromPR` | +| 575 | `agentManager.importFromBranch` | + +Grep afterwards: `props.projectId` must appear exactly once in the file, in the `project` +signal initializer. + +## E. Exact CSS + +All of it goes into `webview-ui/agent-manager/agent-manager.css`. No changes to kilo-ui. + +### E.1 The inline selector + +Insert directly after the `.am-tab-switcher-pill-active` rule (agent-manager.css:3614-3617), +before the `/* Import tab layout */` comment at line 3619: + +```css +/* Project scope selector — inline with the New/Import tabs */ + +.am-nv-project-inline { + display: flex; + align-items: center; + flex-shrink: 0; + flex: 0 1 260px; + min-width: 0; + margin-left: auto; +} + +.am-nv-project-inline .am-selector-wrapper { + width: 100%; + min-width: 0; +} +``` + +The `flex: 0 1 260px` cap keeps the project control compact while allowing long project +names to truncate. `margin-left: auto` keeps it aligned to the right of the New/Import +buttons. + +### E.2 The dropdown rows + +Insert after the `.am-dropdown-empty` rule (agent-manager.css:3863-3868), before the +`/* Import empty state */` comment at line 3870: + +```css +/* Project option rows in the New Worktree project dropdown. + Deliberately distinct from .am-project-item, which styles the sidebar accordion. */ + +.am-project-option { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + width: 100%; + padding: 6px 8px; + border: none; + border-radius: var(--radius-sm); + background: none; + color: var(--text-base); + font-size: var(--font-size-base); + font-family: inherit; + text-align: left; + cursor: pointer; +} + +.am-project-option:hover:not(:disabled) { + background: var(--surface-inset-base-hover); +} + +.am-project-option-active { + background: var(--surface-inset-base); +} + +.am-project-option:disabled { + opacity: 0.5; + cursor: default; +} + +.am-project-option-left { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + flex: 1; +} + +.am-project-option-left [data-component="icon"] { + color: var(--text-weaker); + flex-shrink: 0; +} + +.am-project-option-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex-shrink: 0; + max-width: 45%; +} + +.am-project-option-root { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + font-size: var(--kilo-font-size-11); + color: var(--text-weaker); +} + +``` + +Why not reuse `.am-branch-item`: it is defined twice (lines 3211 and 3810) and the earlier +definition sets `font-family: var(--font-mono, monospace)` on `.am-branch-item-name`, which +would render project labels in monospace. Reusing it also couples project rows to future +branch-row changes. `.am-project-item` is likewise off limits: it already styles the +sidebar project accordion header (line 325). + +### E.3 Popover clipping escape + +`[data-slot="dialog-body"]` is `overflow: hidden` in `packages/ui/src/components/dialog.css:99-105`, +and `[data-slot="dialog-content"]` is `overflow: auto` (line 38). The existing escape rules +at agent-manager.css:2822-2828 only match popovers **inside** `.am-nv-dialog`, and this row +is deliberately outside it. Without the following, the dropdown is clipped by the dialog. + +Add to that same rule group (extend the existing selector list rather than duplicating the +declaration): + +```css +[data-component="dialog"]:has(.am-nv-project-inline [data-component="popover-content"]) [data-slot="dialog-content"], +[data-component="dialog"]:has(.am-nv-project-inline [data-component="popover-content"]) [data-slot="dialog-body"] { + overflow: visible; +} +``` + +Verification step, not optional: open the dropdown with four or more projects registered +and confirm no row is cut off and no inner scrollbar appears on the dialog. If it still +clips, the documented fallback is to drop `portal={false}` from the `DeferredPopover` in +D.4 and delete this rule; the dialog already sets `overflow: visible` on +`[data-slot="dialog-content"]` for portal-based dropdowns (agent-manager.css:2755-2760). + +## F. i18n + +Add to `webview-ui/agent-manager/i18n/en.ts`, immediately after +`"agentManager.dialog.namePlaceholder"`: + +```ts +"agentManager.dialog.project.select": "Select project", +"agentManager.dialog.project.untrusted": "Trust this project in the sidebar first", +"agentManager.dialog.project.missing": "Repository not found", +``` + +Then add the same three keys to all 20 sibling locale files in that directory (`ar bs br da +de es fa fr it ja ko nl no pl ru th tr uk zh zht`) via the `translator` subagent. + +## G. What must not change + +- No new CSS variables or tokens. Only the ones listed above, all already in use in this + file. +- No edits to `packages/kilo-ui/` or `packages/ui/`. +- No change to `.am-project-item`, `.am-branch-item`, `.am-selector-trigger`, + `.am-nv-config-label`, or any other existing rule. The only existing rule touched is the + `overflow: visible` selector group in E.3, and only by adding selectors to it. +- No new message types. `agentManager.requestBranches`, `agentManager.createMultiVersion`, + `agentManager.importFromBranch`, and `agentManager.importFromPR` all already exist and + already accept what is needed. +- With `props.projects` empty, the rendered dialog markup must be identical to before the + change. Verify by toggling `kilo-code.new.experimental.multiProject` off. diff --git a/.kilo/plans/jetbrains-context-settings-page.md b/.kilo/plans/jetbrains-context-settings-page.md new file mode 100644 index 00000000000..1b76e7b6ce8 --- /dev/null +++ b/.kilo/plans/jetbrains-context-settings-page.md @@ -0,0 +1,423 @@ +# JetBrains Context Settings Page + +Implement the Tier 1 Context settings from `docs/jetbrains-vscode-settings-parity.md` in the JetBrains plugin. This is a pure `kilo.json` settings UI: no CLI feature work, no SDK regen, and no session-rendering changes. + +## Goal + +Add a new JetBrains settings page under `Settings -> Tools -> Kilo Code -> Context` for: + +| Setting | Config key | Type | +|---|---|---| +| Auto-compaction | `compaction.auto` | boolean | +| Compaction threshold percent | `compaction.threshold_percent` | number or null | +| Prune on compaction | `compaction.prune` | boolean | +| Watcher ignore patterns | `watcher.ignore` | string array | + +Do not include VS Code Context-tab memory/indexing controls in this first pass. JetBrains does not have the equivalent memory/indexing settings service yet, and the parity doc excludes indexing from easy wins. + +Do not put `snapshot` on this page unless product explicitly decides to combine Context and Checkpoints. The parity doc suggests `snapshot` belongs on a new Checkpoints page. + +## Context Verified + +- Source parity doc: `docs/jetbrains-vscode-settings-parity.md`. +- JetBrains settings guidance: `packages/kilo-jetbrains/AGENTS.md`, especially `Settings UI`. +- Existing settings pages are registered in `packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml`. +- Existing page pattern to mirror: + - `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsConfigurable.kt` + - `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt` + - `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsState.kt` +- Existing global config write path is sufficient once DTO/parser support is added: + - Frontend: `KiloAppService.updateConfigAsync(...)` + - RPC: `KiloAppRpcApi.updateConfig(patch: ConfigPatchDto)` + - Backend: `KiloBackendAppService.updateConfig(...)` + - HTTP: `PATCH /global/config`, then `GET /global/config` +- Existing backend parser currently only serializes selected string keys from `ConfigPatchDto.values`; Context needs typed booleans, numbers, explicit null, and string arrays. + +## Decisions + +- Use global config for the first implementation, matching the existing app-level settings write path. +- Add typed DTO fields instead of overloading `ConfigPatchDto.values` for non-string values. +- Use an explicit `clear` list for nullable compaction fields, because `Double?` cannot distinguish absent from explicit `null`. +- Reuse `BaseSettingsUi`, `DraftReadyConfigurable`, `SettingsDraftState`, `SettingsRows`, `SettingsRow`, and `SettingsToggle`. +- Use the shared settings list primitives for `watcher.ignore`; do not build a bespoke add/remove list if `SettingsListPanel` or adjacent list primitives fit. +- Keep all UI strings in `KiloBundle.properties`. Let other locale bundles fall back unless the repo's resource-bundle checks require duplicated English keys. + +## Part A - Shared DTOs + +File: `packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/KiloAppStateDto.kt` + +Add config read DTOs: + +```kotlin +@Serializable +data class WatcherConfigDto( + val ignore: List = emptyList(), +) + +@Serializable +data class CompactionConfigDto( + val auto: Boolean? = null, + val threshold_percent: Double? = null, + val prune: Boolean? = null, +) +``` + +Extend `ConfigDto`: + +```kotlin +val watcher: WatcherConfigDto? = null, +val compaction: CompactionConfigDto? = null, +``` + +Add patch DTOs: + +```kotlin +@Serializable +data class WatcherPatchDto( + val ignore: List? = null, +) + +@Serializable +data class CompactionPatchDto( + val clear: List = emptyList(), + val auto: Boolean? = null, + val threshold_percent: Double? = null, + val prune: Boolean? = null, +) +``` + +Extend `ConfigPatchDto`: + +```kotlin +val watcher: WatcherPatchDto? = null, +val compaction: CompactionPatchDto? = null, +``` + +Notes: + +- `watcher.ignore = null` means no change. +- `watcher.ignore = emptyList()` means explicitly save an empty list. +- `compaction.threshold_percent = null` alone means no change. +- `compaction.clear = listOf("threshold_percent")` means emit JSON `"threshold_percent": null`. +- `false` boolean values must be serialized; do not treat `false` as absent. + +## Part B - Backend Config Parser And Serializer + +File: `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt` + +### Parse + +Extend `parseConfig(raw)` to read: + +- `watcher.ignore` +- `compaction.auto` +- `compaction.threshold_percent` +- `compaction.prune` + +Add private helpers near `parseSkillsConfig` / `parseMcpConfig`: + +```kotlin +private fun parseWatcherConfig(obj: JsonObject?): WatcherConfigDto? +private fun parseCompactionConfig(obj: JsonObject?): CompactionConfigDto? +``` + +Use existing helper style: + +- strings: `str(...)` +- booleans: `flagOrNull(...)` +- numbers: `num(...)` +- arrays: `arr()?.mapNotNull { it.jsonPrimitive.contentOrNull }` + +### Serialize + +Extend `buildConfigPatch(patch)` to emit typed context patches: + +```json +{ + "watcher": { + "ignore": ["**/node_modules/**"] + }, + "compaction": { + "auto": true, + "threshold_percent": 80, + "prune": false + } +} +``` + +For explicit threshold clearing: + +```json +{ + "compaction": { + "threshold_percent": null + } +} +``` + +Keep the existing `values` allowlist for string model keys. Do not pass Context values through `values`. + +## Part C - Frontend State Model + +Add package: `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/context/` + +New file: `ContextSettingsState.kt` + +Define: + +```kotlin +internal data class ContextDraft( + val auto: Boolean? = null, + val threshold: String = "", + val prune: Boolean? = null, + val ignore: List = emptyList(), +) +``` + +Use a string for the threshold draft so the UI can represent blank/invalid intermediate input without losing user text. Convert only when building a patch. + +Functions to add: + +- `contextDraft(config: ConfigDto?): ContextDraft` +- `patch(from: ContextDraft, to: ContextDraft): ConfigPatchDto` +- `savedMatches(base: ContextDraft, draft: ContextDraft): Boolean` +- `threshold(value: String): Double?` or equivalent parsing helper +- validation helper for threshold range if desired + +Patch behavior: + +- Only emit changed fields. +- Emit `CompactionPatchDto(auto = false)` when the user turns auto-compaction off. +- Emit `CompactionPatchDto(prune = false)` when the user turns pruning off. +- Emit `CompactionPatchDto(threshold_percent = 80.0)` for a non-blank valid number. +- Emit `CompactionPatchDto(clear = listOf("threshold_percent"))` when an existing threshold is cleared. +- Emit `WatcherPatchDto(ignore = emptyList())` when the last ignore pattern is removed. +- Return no change from the page when all fields match the baseline. + +## Part D - Frontend UI Page + +New file: `ContextConfigurable.kt` + +Mirror `ModelsConfigurable`: + +- Extend `DraftReadyConfigurable`. +- `ID = "ai.kilocode.jetbrains.settings.context"`. +- `getDisplayName()` returns `KiloBundle.message("settings.context.displayName")`. +- `create(cs)` returns `ContextSettingsUi(cs)`. + +New file: `ContextSettingsUi.kt` + +Mirror the simple parts of `ModelsSettingsUi`: + +- Extend `BaseSettingsUi`. +- Initial draft is `ContextDraft()`. +- `save(change, done)` calls `app.updateConfigAsync(change, done)`. +- `base(result)` and `draft(state)` call `contextDraft(state.config)`. +- `saved(base, draft)` calls `savedMatches(base, draft)`. +- `pendingText()` uses `settings.context.save.pending`. +- `failedText()` uses `settings.context.save.failed`. +- `loadWorkspace(root)` returns `Unit`; `applyWorkspace(result)` is `Unit`. +- `models(state)` is `Unit`. +- `syncContent()` updates enabled states, field values, save/progress overlay, and validation messaging. + +New content class: `ContextSettingsContent` + +Suggested layout: + +- Section `settings.context.compaction.title` + - Toggle row `settings.context.compaction.auto.title` + - Numeric row `settings.context.compaction.threshold.title` + - Toggle row `settings.context.compaction.prune.title` +- Section `settings.context.watcher.title` + - List editor row/panel for ignore patterns + +Controls: + +- Use `SettingsToggle` for booleans. +- Use `JBTextField` or a small reusable numeric field pattern based on `AgentEditDialog` for threshold. +- Use shared list primitives (`SettingsListPanel` / `SettingsListView` / `SettingsListItem` / `SettingsListCell`) for `watcher.ignore` where practical. +- Keep the page editable while app status is ready and no save is pending. +- Disable controls while saving. + +Validation: + +- Blank threshold is valid and means clear/reset the config value if it differs from baseline. +- Non-numeric threshold is invalid and should prevent `apply()` from sending a patch. +- Suggested accepted range is `0..100`; if existing CLI allows a broader range, follow CLI behavior. +- Show validation through existing settings messaging rather than custom ad hoc labels. + +## Part E - Settings Registration And Root Navigation + +File: `packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml` + +Add a child configurable: + +```xml + +``` + +Adjust weights so the desired order is stable. Recommended order: + +| Page | Weight | +|---|---| +| User Profile | 5 | +| Models | 4 | +| Context | 3 | +| Providers | 2 | +| Agent Behavior | 1 | + +File: `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurable.kt` + +Add a root-page `ActionLink` for Context between Models and Providers: + +- Import `ContextConfigurable`. +- Link text: `settings.context.displayName`. +- Link target: `ContextConfigurable.ID`. + +`KiloSettingsSelection.kt` probably needs no code changes because child IDs already share the root prefix. + +## Part F - Strings + +File: `packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties` + +Add base strings near the other settings strings: + +```properties +settings.context.displayName=Context +settings.context.description=Configure compaction and file-watcher context behavior. +settings.context.save.pending=Saving context settings... +settings.context.save.failed=Failed to save context settings +settings.context.compaction.title=Compaction +settings.context.compaction.description=Control when Kilo summarizes long sessions to reduce context usage. +settings.context.compaction.auto.title=Auto-compaction +settings.context.compaction.auto.description=Automatically compact long conversations before they exceed the model context window. +settings.context.compaction.threshold.title=Compaction threshold +settings.context.compaction.threshold.description=Percent of the context window to use before auto-compaction starts. Leave blank to use the default. +settings.context.compaction.threshold.invalid=Enter a number from 0 to 100, or leave the field blank. +settings.context.compaction.prune.title=Prune on compaction +settings.context.compaction.prune.description=Drop older raw conversation details after compaction to keep the session context smaller. +settings.context.watcher.title=Watcher ignore patterns +settings.context.watcher.description=Glob patterns Kilo should ignore when watching repository file changes. +settings.context.watcher.add=Add pattern +settings.context.watcher.empty=No ignore patterns configured. +settings.context.watcher.placeholder=e.g. **/dist/** +settings.context.watcher.remove=Remove {0} +``` + +If resource-bundle tests require every key in every locale bundle, copy English values into the localized bundles and leave translation work for a later i18n pass. + +## Part G - Test Updates + +### Frontend state tests + +Add: `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/context/ContextSettingsStateTest.kt` + +Cover: + +- Draft reads `ConfigDto.watcher` and `ConfigDto.compaction`. +- Unchanged draft emits no patch. +- Boolean changes emit `false` and `true` correctly. +- Threshold set emits `threshold_percent`. +- Threshold clear emits `clear = listOf("threshold_percent")`. +- Watcher list add/remove emits the whole new `ignore` list, including empty list. +- Invalid threshold is rejected before save if validation lives in state helpers. + +### Frontend UI tests + +Add: `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/context/ContextSettingsUiTest.kt` + +Use `ModelsSettingsUiTest` as the main pattern: + +- `BasePlatformTestCase`. +- Real EDT. +- `FakeAppRpcApi`. +- `KiloAppService`. +- `flushUntil` helpers. +- Assert `rpc.configPatches` after user interaction. +- Assert controls disable during pending save. +- Assert failed save leaves page modified and shows `settings.context.save.failed`. + +Update `FakeAppRpcApi`: + +- File: `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAppRpcApi.kt` +- Apply `patch.watcher` and `patch.compaction` to fake config state. +- Preserve explicit empty lists. +- Preserve boolean `false`. +- Honor `compaction.clear` by setting cleared fields to `null`. + +### Backend parser tests + +Update: `packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt` + +Add exact JSON tests for: + +- `parseConfig` reads watcher and compaction fields. +- `buildConfigPatch` emits watcher ignore arrays. +- `buildConfigPatch` emits `auto=false` and `prune=false`. +- `buildConfigPatch` emits numeric `threshold_percent`. +- `buildConfigPatch` emits explicit `threshold_percent:null` when `clear` includes the field. + +### Backend app service tests + +Update: `packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendAppServiceTest.kt` + +Add a test similar to the existing model config update test: + +- Call `updateConfig(ConfigPatchDto(watcher = ..., compaction = ...))`. +- Assert `MockCliServer.lastConfigPatchBody` exactly matches the expected nested JSON. +- Assert the returned/reloaded `ConfigDto` includes the saved Context values. + +### Root settings tests + +Update: `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurableTest.kt` + +Add: + +- `ContextConfigurable.ID == "ai.kilocode.jetbrains.settings.context"`. +- Root page includes a Context link. +- Link order matches XML order. + +## Validation + +Run from `packages/kilo-jetbrains/`: + +```bash +./gradlew typecheck +./gradlew test +``` + +Focused checks while iterating: + +```bash +./gradlew :shared:test --tests '*ContextSettingsStateTest' +./gradlew :frontend:test --tests '*ContextSettingsUiTest' +./gradlew :backend:test --tests '*KiloCliDataParserTest' +./gradlew :backend:test --tests '*KiloBackendAppServiceTest' +``` + +If the exact Gradle module test selectors differ, run the package-level `./gradlew test` before marking the implementation ready. + +Manual verification: + +1. Run `./gradlew runIde` from `packages/kilo-jetbrains/`. +2. Open `Settings -> Tools -> Kilo Code -> Context`. +3. Toggle auto-compaction and prune. +4. Set threshold to a number, apply, reopen settings, and verify it persists. +5. Clear threshold, apply, reopen settings, and verify it resets. +6. Add and remove watcher ignore patterns, apply, reopen settings, and verify the list persists. +7. Inspect the global Kilo config file through the existing `Open: global ...` action if needed. + +## Risks And Follow-ups + +- Global vs project-local config: this plan uses the existing global config write path. Project-local Context settings would need new workspace config RPC plumbing. +- Threshold null semantics: implement explicit clear handling; otherwise clearing the field will silently do nothing. +- String-array UI: reuse list primitives even if it takes a small adapter type; avoid one-off list widgets. +- VS Code memory/indexing parity: defer because it is not pure config and is excluded by the easy-win criteria. +- Checkpoints page: implement `snapshot` separately unless product asks to combine it with Context. +- Changeset: when implementing this user-facing JetBrains settings feature, add a patch changeset for `kilo-code`/JetBrains according to repo release guidance. diff --git a/.kilo/skills/chart/SKILL.md b/.kilo/skills/chart/SKILL.md new file mode 100644 index 00000000000..ddff65e6677 --- /dev/null +++ b/.kilo/skills/chart/SKILL.md @@ -0,0 +1,157 @@ +--- +name: chart +description: Use when the user asks to visualize data with charts, graphs, or plots using the `chart` tool (bar, line, scatter, pie, time series, etc.). +--- + +# Data Visualization + +The `chart` tool is ALWAYS available in this environment. When the user asks to visualize data (charts, graphs, plots), you MUST call the `chart` tool. Never output the config as text, never say the tool is unavailable, never suggest external renderers. Always use the tool call — it is the only correct response for data visualization requests. Do NOT repeat or echo the config JSON in your text response. + +Use the `chart` tool only when the user explicitly asks for a chart, graph, or plot. Only use these supported Chart.js v4 types: `bar`, `bubble`, `pie`, `doughnut`, `line`, `mixed`, `polarArea`, `radar`, `scatter`. For area charts, use `line` with `fill: true` on the dataset — do NOT use `area` as a type. + +Use mermaid fenced code blocks (` ```mermaid `) when: +- The user asks for a diagram, flowchart, sequence diagram, ER diagram, or architecture diagram +- Visualizing relationships, processes, or structure — not data values + +Mermaid is NOT a tool and is NOT Chart.js — never call the `chart` tool for mermaid diagrams. Just write the mermaid syntax directly in your text response inside a fenced code block. No tool call needed. + +Do not use either for: code, text, or data that is already clear in prose or table form. + +The `chart` tool input accepts: +- `title` (string) — short label shown in the tool header +- `description` (string, optional) — subtitle shown below the title +- `spec` (string) — a Chart.js config object as a JSON string + +The `spec` field must be a Chart.js config JSON string with `type`, `data`, and optionally `options`. Examples: + +Bar chart: +```json +{ + "type": "bar", + "data": { + "labels": ["A", "B", "C"], + "datasets": [{ "label": "Value", "data": [10, 20, 15] }] + } +} +``` + +Area chart (line with fill): +```json +{ + "type": "line", + "data": { + "labels": ["Jan", "Feb", "Mar", "Apr"], + "datasets": [{ "label": "Value", "data": [10, 28, 19, 45], "fill": true }] + } +} +``` + +Line chart: +```json +{ + "type": "line", + "data": { + "labels": ["Jan", "Feb", "Mar", "Apr"], + "datasets": [{ "label": "Value", "data": [10, 28, 19, 45], "fill": false }] + } +} +``` + +Scatter plot: +```json +{ + "type": "scatter", + "data": { + "datasets": [{ + "label": "Points", + "data": [{ "x": 1, "y": 5 }, { "x": 2, "y": 8 }, { "x": 3, "y": 3 }] + }] + } +} +``` + +Time series: +```json +{ + "type": "line", + "data": { + "labels": ["2024-01", "2024-02", "2024-03", "2024-04"], + "datasets": [{ "label": "Value", "data": [120, 145, 132, 178], "fill": true }] + } +} +``` + +Pie chart: +```json +{ + "type": "pie", + "data": { + "labels": ["A", "B", "C"], + "datasets": [{ "data": [30, 50, 20] }] + } +} +``` + +Doughnut chart: +```json +{ + "type": "doughnut", + "data": { + "labels": ["A", "B", "C"], + "datasets": [{ "data": [30, 50, 20] }] + } +} +``` + +Radar chart: +```json +{ + "type": "radar", + "data": { + "labels": ["Speed", "Power", "Agility", "Stamina"], + "datasets": [{ "label": "Player", "data": [80, 60, 90, 70] }] + } +} +``` + +Bubble chart: +```json +{ + "type": "bubble", + "data": { + "datasets": [{ + "label": "Group A", + "data": [{ "x": 10, "y": 20, "r": 8 }, { "x": 15, "y": 10, "r": 5 }] + }] + } +} +``` + +Polar area chart: +```json +{ + "type": "polarArea", + "data": { + "labels": ["A", "B", "C", "D"], + "datasets": [{ "data": [11, 16, 7, 14] }] + } +} +``` + +Mixed chart (bar + line): +```json +{ + "type": "bar", + "data": { + "labels": ["Jan", "Feb", "Mar"], + "datasets": [ + { "type": "bar", "label": "Revenue", "data": [100, 120, 90] }, + { "type": "line", "label": "Trend", "data": [95, 115, 100] } + ] + } +} +``` + +You may customize colors by setting `backgroundColor` and `borderColor` arrays on datasets. The renderer handles sizing — do not set width or height. + +Only include `scales` in `options` for cartesian chart types: `bar`, `line`, `scatter`, `bubble`. Do NOT include `scales` for `pie`, `doughnut`, `polarArea`, `radar`, or `mixed` — it will cause them to fail. diff --git a/.kilo/skills/icon-jetbrains/SKILL.md b/.kilo/skills/icon-jetbrains/SKILL.md new file mode 100644 index 00000000000..2a20da53ed8 --- /dev/null +++ b/.kilo/skills/icon-jetbrains/SKILL.md @@ -0,0 +1,137 @@ +--- +name: icon-jetbrains +description: Create or review IntelliJ New UI SVG icons, theme variants, sizes, and palette. +--- + +# IntelliJ Platform New UI Icons + +Guidance for authoring SVG icons for the JetBrains plugin. This skill is the single source of truth for icon sizing, palette, dark variants, composition rules, and placement. Other docs (including `packages/kilo-jetbrains/AGENTS.md`) defer here for SVG authoring details. + +Icons follow IntelliJ New UI conventions: a fixed canvas per role, a strict light/dark palette, and explicit per-shape colors (the IntelliJ SVG loader recolors by matching literal hex values, so `currentColor` and CSS do not work). The plugin loads icons directly from its resource folder — see [Where the SVGs live](#where-the-svgs-live). + +## Golden rules + +1. **Always ship two SVGs** — one for the light theme (e.g. `add-file.svg`) and one for the dark theme with the `_dark` suffix (`add-file_dark.svg`). Geometry must be identical between them; only the palette swaps. + - **Tool-window icons ship as a quartet, not a pair.** When a tool window has both a 16×16 base (`name.svg` / `name_dark.svg`) and a 20×20 stripe variant (`name@20x20.svg` / `name@20x20_dark.svg`), they must share the same metaphor. The stripe is only one surface — the 16×16 base also appears in **Search Everywhere**, **Find Action**, context menus, the Services tool window, recent locations, and the View ▸ Tool Windows menu. Changing only the @20x20 leaves users seeing two different icons for the same tool window depending on where they encounter it. Always update all four files together (this repo's tool-window quartet is `kilo.svg` / `kilo_dark.svg` + `kilo@20x20.svg` / `kilo@20x20_dark.svg`). +2. **Only use colors from the canonical palette.** See [palette.md](./palette.md). Picking a one-off color breaks theming and contrast. +3. **One canvas size per icon role.** See [Icon roles](#icon-roles). Do not invent new sizes or pad with empty space — IntelliJ scales the canvas as a single unit. +4. **No raster, no gradients, no filters, no embedded fonts.** Path geometry only (``, ``, ``, ``, ``, ``). Text must be converted to outlines. +5. **Use `fill="none"` on the root ``** and set `fill` / `stroke` explicitly per shape — never rely on CSS or `currentColor`. +6. **Strokes use `stroke-width="1"`, `stroke-linecap="round"`, `stroke-linejoin="round"`** (or `stroke-miterlimit="10"` for hard joins). Heavier strokes are reserved for hero glyphs inside a circle badge (e.g. status checkmarks) and use `stroke-width="1.5"` or `"2"`. The `1` applies to the **primary glyph stroke** — do not force *every* stroke to 1. Hairline/decorative strokes (e.g. a thin stroke used to fatten a filled dot) and strokes whose width is coupled to geometry (e.g. a badge ring meant to sit flush with a fill edge) must keep their intended weight (scale with the artwork), or they fatten and misalign. +7. **Pixel-grid align**: keep stroke axes on half-pixel centers (`x.5`) and fills on whole pixels so the icon stays crisp at 1× rendering. Getting the base grid right also keeps it crisp on HiDPI/Retina; fine sub-pixel detail blurs at fractional scales (125%/150%), so keep geometry simple rather than chasing detail that won't survive scaling. SVGs are resolution-independent — ship one vector per theme, never `@2x` raster variants. +8. **File names use kebab-case and stay ASCII-only** (e.g. `arrow-down-to-line.svg`, `book-open-check.svg`, `kilo@20x20.svg`), matching every existing icon in `packages/kilo-jetbrains/frontend/src/main/resources/icons/`. + +## Icon roles + +Pick the canvas size from the role, not the other way around. All icons land in this repo's `packages/kilo-jetbrains/frontend/src/main/resources/icons/` (with a `views/` subfolder for in-view icons — see [Where the SVGs live](#where-the-svgs-live)). + +| Role | Canvas | Filename pattern | +|---|---|---| +| Action icons (menus, popups, toolbars) | **16×16** | `name.svg` + `name_dark.svg` | +| Tree node icons (PSI, structure view) | **16×16** | `name.svg` + `name_dark.svg` | +| Tool-window stripe icons (compact/16) | **16×16** | `name.svg` + `name_dark.svg` | +| Tool-window stripe icons (New UI) | **20×20** | `name@20x20.svg` + `name@20x20_dark.svg` | +| Main toolbar (New UI) | **20×20** | `name@20x20.svg` + `name@20x20_dark.svg` | +| Editor gutter icons | **14×14** (small marks **12×12**, **9×9**) | `name.svg` + `name_dark.svg` | +| Status bar / inline status | **16×16** | `name.svg` + `name_dark.svg` | +| Breakpoint marks | **14×14** (12×12 for sub-marks) | `name.svg` + `name_dark.svg` | +| Run-config tags & disclosure chevrons | **16×16** (`chevron*.svg` may be 9×9 to 16×16) | `name.svg` + `name_dark.svg` | +| Welcome/onboarding & logos | **16, 20, 28, 48** (per surface) | `name.svg` + `name_dark.svg` | + +When in doubt, find a sibling icon of the same role already in the icons folder and copy its `width` / `height` / `viewBox`. + +## Where the SVGs live + +Plugin icons go into `packages/kilo-jetbrains/frontend/src/main/resources/icons/` (action icons, tool-window icons) or the `icons/views/` subfolder (in-view icons used by the chat/session UI). Icons are loaded directly via `IconLoader` — there is no role-based subfolder structure and no mapping file. Place both the light SVG and its `_dark` sibling in the folder and reference them from the plugin's icon-holder class. + +## SVG skeleton + +```svg + + + +``` + +The dark variant is the same file with light-palette colors swapped for their dark-theme partner — see [palette.md](./palette.md). Never change geometry between the two. + +## Composition rules + +- **One semantic meaning per icon.** A status badge, an accent dot, or a "+" overlay is fine; two unrelated glyphs in one icon is not. +- **Optical centering, not geometric.** Plus/arrow/refresh glyphs sit slightly above center; round badges (class, method, status) are centered on `(cx=8, cy=8)` for 16×16 and `(cx=10, cy=10)` for 20×20. +- **Outer keep-out**: leave at least **1 px** of empty padding on each side of a 16×16 icon (so meaningful geometry lives within `1..15`). For 20×20 use **2 px** of padding. Stripe icons must stay visually balanced inside their 20×20 cell. +- **Round caps overshoot the endpoint.** A round `stroke-linecap`/`stroke-linejoin` extends **half the stroke width past the endpoint**, so a 1px round-capped stroke ending at `0` or `16` is clipped by the canvas. Keep stroke endpoints within `0.5..15.5` (this is stricter than the fill keep-out). +- **Stroke + fill pairing for node-style icons** (class/method nodes): a light-tinted fill at radius 6.5 with a stroke in the accent color, plus a glyph filled with the same accent. + - Light: `` then glyph `fill=""`. + - Dark: same circle with `fill="" stroke=""` and glyph filled with ``. +- **Stroke-only icons** (chevrons, refresh, edit pencil): a single-color path using the neutral stroke (`#6C707E` light / `#CED0D6` dark) at `stroke-width="1"`. Use `#818594` (light) / `#6F737A` (dark) for "secondary" stroke glyphs like dropdown chevrons. +- **Status badges** (error/warning/success/info) follow this template: + - Light: filled circle/triangle in the *accent* color, glyph painted in `white`. + - Dark: filled circle/triangle in the *dark accent* color, glyph painted in the matching *muted dark fill* (e.g. `#5E4D33` inside `#F2C55C` warning) — never plain white. +- **Two-tone action icons** (a base glyph plus a small modifier): the base glyph uses the neutral gray, and the small modifier (`+`, ✕, ↻, gear) uses the primary blue accent. Light gray + blue accent → dark gray + blue accent in the dark variant. +- **Disabled / stroke-only variants** (e.g. `*-stroke.svg`): outline-only, same neutral stroke color, no fills. + +## Palette quick reference + +The full lookup is in [palette.md](./palette.md). Most icons only need: + +| Role | Light | Dark | +|---|---|---| +| Primary stroke / fill | `#6C707E` | `#CED0D6` | +| Secondary stroke | `#818594` | `#6F737A` | +| Disabled / faint fill | `#EBECF0` | `#43454A` | +| Accent — Blue | `#3574F0` | `#548AF7` | +| Accent — Blue (bg) | `#EDF3FF` / `#E7EFFD` | `#25324D` | +| Accent — Red | `#DB3B4B` | `#DB5C5C` | +| Accent — Red (bg) | `#FFF7F7` | `#402929` | +| Status — Error fill | `#E55765` | `#DB5C5C` | +| Status — Warning fill | `#FFAF0F` | `#F2C55C` | +| Status — Success fill | `#55A76A` | `#57965C` | +| Accent — Green | `#208A3C` | `#57965C` | +| Accent — Green (bg) | `#F2FCF3` | `#253627` | +| Accent — Orange | `#E66D17` | `#C77D55` | +| Accent — Orange (bg) | `#FFF4EB` | `#45322B` | +| Accent — Yellow/Gold | `#FFAF0F` / `#C27D04` | `#F2C55C` / `#D6AE58` | +| Accent — Purple | `#834DF0` | `#B589EC` | +| Accent — Purple (bg) | `#FAF5FF` | `#2F2936` | +| White on dark badge | `white` | matching muted-dark fill (e.g. `#5E4D33`) | + +Do not use plain `#000000` or off-the-palette grays. + +## Generation workflow + +1. **Pick the role and canvas size** from the table above. Find at least two visually similar sibling icons already in the icons folder and mirror their stroke/fill mix. +2. **Lay out geometry on the pixel grid** (whole-pixel fills, half-pixel stroke centers). Optical-center the glyph. +3. **Apply the canonical light palette** from [palette.md](./palette.md). Never invent colors. +4. **Save the light SVG** with `width`/`height`/`viewBox` matching the role and `fill="none"` on ``. +5. **Duplicate to the `_dark` filename** and swap each color for its dark-theme partner from the palette mapping. Keep paths byte-identical otherwise. +6. **Place and wire the file** → drop both files into `packages/kilo-jetbrains/frontend/src/main/resources/icons/` (action icons, tool-window icons) or `icons/views/` (in-view chat/session icons), then reference the icon from the plugin's icon-holder class. +7. **Verify visually** in both themes via the image preview in the IDE, or run the IDE and toggle *View ▸ Appearance ▸ New UI* to compare. Check selection states for stripe icons. + +## Adapting or rescaling an existing icon + +Importing a Lucide/Codicon-style icon (often drawn on a 20- or 24-unit grid) or moving one onto the 16×16 grid is not a blanket "set everything to the 16 defaults" operation: + +1. **Scale geometry *and* every `stroke-width`** by the same factor (`16 ÷ source size`). This keeps the render faithful before you change anything intentionally. +2. **Only then re-weight the primary glyph stroke** to `1` if it wasn't already ~1px effective. Leave hairlines and geometry-coupled strokes at their scaled value (see rule 6). +3. **Re-check what scaling breaks:** + - Round caps clipping at the edge → nudge stroke endpoints into `0.5..15.5`. + - Strokes that were flush with a fill edge (e.g. a badge ring around a status dot) → the stroke must still straddle the fill boundary after scaling; scale its width too. +4. **Crispness is a separate pass.** Scaling rarely lands coordinates on the pixel grid, so the result is faithful but not automatically crisp; the pixel-grid snap (rule 7) is manual and per-icon. + +## Common mistakes + +- Off-palette colors (e.g. importing from Figma without remapping). They will not theme correctly and reviewers will reject the PR. +- Different geometry between light and dark variants — selection animations and HiDPI overlays will glitch. +- A 16-px glyph saved into a 20×20 canvas without re-balancing. Tool-window/main-toolbar icons need geometry tuned for 20×20, not a 16×16 reused with extra whitespace. +- Using `currentColor`, CSS, or `

- English | 简体中文 | 繁體中文 | 한국어 | Deutsch | Español | Français | Italiano | Dansk | 日本語 | Polski | Русский | Bosanski | العربية | Norsk | Português (Brasil) | ไทย | Türkçe | Українська | বাংলা | Ελληνικά | Tiếng Việt + English | 简体中文 | 繁體中文 | 한국어 | Deutsch | Español | Français | Italiano | Dansk | 日本語 | Polski | Русский | Bosanski | العربية | Norsk | Português (Brasil) | ไทย | Türkçe | Українська | বাংলা | Ελληνικά | Tiếng Việt

@@ -168,7 +168,7 @@ MIT. You're free to use, modify, and distribute this code, including commerciall

Where did Kilo CLI come from? -Kilo CLI is a fork of [OpenCode](https://github.com/Kilo-Org/kilocode), enhanced to work within the Kilo agentic engineering platform. +Kilo CLI is a fork of [OpenCode](https://github.com/anomalyco/opencode), enhanced to work within the Kilo agentic engineering platform.
diff --git a/artifacts/glm52-rise-video/.gitignore b/artifacts/glm52-rise-video/.gitignore new file mode 100644 index 00000000000..9d38bb25911 --- /dev/null +++ b/artifacts/glm52-rise-video/.gitignore @@ -0,0 +1,3 @@ +node_modules +.remotion +out/frame-*.png diff --git a/artifacts/glm52-rise-video/bun.lock b/artifacts/glm52-rise-video/bun.lock new file mode 100644 index 00000000000..cac009ae812 --- /dev/null +++ b/artifacts/glm52-rise-video/bun.lock @@ -0,0 +1,483 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "glm52-rise-video", + "dependencies": { + "@remotion/cli": "^4.0.384", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "remotion": "^4.0.384", + }, + "devDependencies": { + "@types/react": "^19.2.8", + "@types/react-dom": "^19.2.3", + "typescript": "^5.8.2", + }, + }, + }, + "packages": { + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/parser": ["@babel/parser@7.24.1", "", { "bin": "./bin/babel-parser.js" }, "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg=="], + + "@babel/types": ["@babel/types@7.24.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w=="], + + "@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.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@mediabunny/aac-encoder": ["@mediabunny/aac-encoder@1.47.0", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-JNzgdJoHMFFnv5imi1+dmjZMudsJ1zNCUCEkKjBh90cqGhAFg8xu4V2gsyxE2i6oq/YpWx23P+OvoANLSMcDzA=="], + + "@mediabunny/flac-encoder": ["@mediabunny/flac-encoder@1.47.0", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-VpKmJO0xlYcFCRD6JvJlMbNQ6d/6YMHdO1gFIqWlZABHjSSL6BquNNEgWSCv5vdF8ELDBwIYBVZEslakIB/7GA=="], + + "@mediabunny/mp3-encoder": ["@mediabunny/mp3-encoder@1.47.0", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-JyzZyGeGRm2HVUQaGJ/VZT9OG+kG00mA8SLP/f3CO7+qWAmBncKc16WYXWHbZEo8Jn/ZGA6De32S5R2bTQbDqA=="], + + "@module-federation/error-codes": ["@module-federation/error-codes@0.22.0", "", {}, "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug=="], + + "@module-federation/runtime": ["@module-federation/runtime@0.22.0", "", { "dependencies": { "@module-federation/error-codes": "0.22.0", "@module-federation/runtime-core": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA=="], + + "@module-federation/runtime-core": ["@module-federation/runtime-core@0.22.0", "", { "dependencies": { "@module-federation/error-codes": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA=="], + + "@module-federation/runtime-tools": ["@module-federation/runtime-tools@0.22.0", "", { "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/webpack-bundler-runtime": "0.22.0" } }, "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA=="], + + "@module-federation/sdk": ["@module-federation/sdk@0.22.0", "", {}, "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g=="], + + "@module-federation/webpack-bundler-runtime": ["@module-federation/webpack-bundler-runtime@0.22.0", "", { "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], + + "@remotion/bundler": ["@remotion/bundler@4.0.483", "", { "dependencies": { "@remotion/media-parser": "4.0.483", "@remotion/studio": "4.0.483", "@remotion/studio-shared": "4.0.483", "@remotion/timeline-utils": "4.0.483", "@rspack/core": "1.7.11", "@rspack/plugin-react-refresh": "1.6.1", "css-loader": "7.1.4", "esbuild": "0.28.1", "react-refresh": "0.18.0", "remotion": "4.0.483", "style-loader": "4.0.0", "webpack": "5.105.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-5fQRMYgr2sxZPBOThnIVuFwHBq/FhusJ+Ke6xopga8io8ecK/mj9GaRXC3tgfMa+YWuu8ZSXZ0U9EsMZjjP5YQ=="], + + "@remotion/canvas-capture": ["@remotion/canvas-capture@4.0.483", "", { "dependencies": { "mediabunny": "1.47.0", "remotion": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JABgbfvXwjFp1E61tlDN9gVAH8OoeF4kUUELNwG9kcNOE4ex8+vCYEO6VKIaXZ4l8A1PEAQ5EK//NheTJ80gfw=="], + + "@remotion/cli": ["@remotion/cli@4.0.483", "", { "dependencies": { "@remotion/bundler": "4.0.483", "@remotion/media-utils": "4.0.483", "@remotion/player": "4.0.483", "@remotion/renderer": "4.0.483", "@remotion/studio": "4.0.483", "@remotion/studio-server": "4.0.483", "@remotion/studio-shared": "4.0.483", "dotenv": "17.3.1", "minimist": "1.2.6", "prompts": "2.4.2", "remotion": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" }, "bin": { "remotion": "remotion-cli.js", "remotionb": "remotionb-cli.js", "remotiond": "remotiond-cli.js" } }, "sha512-z4TQcgTfQbSIV4GQpCvIihDL77M+leQelbxv962PmfSKTT7z3XnNN732PcZ4sei2+Xg+d2SD95K4tI5XBYs5Gw=="], + + "@remotion/compositor-darwin-arm64": ["@remotion/compositor-darwin-arm64@4.0.483", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CkEJXouGEoCs5PX3KOufkXrlT/Kgn0ZF7SEQ4meCYGYMZD+QPVtsmS1MXH8pQ4RHlFO6Tk7g0CIHMT3Y4yzpuA=="], + + "@remotion/compositor-darwin-x64": ["@remotion/compositor-darwin-x64@4.0.483", "", { "os": "darwin", "cpu": "x64" }, "sha512-vDZK5U8FYbHGMjFC2lUgJRBz8KXwbwFSvEQi8FATD0roNg75SoaEJWrqAOIVv8v5eBjPvo18znDc36NLRYn1Eg=="], + + "@remotion/compositor-linux-arm64-gnu": ["@remotion/compositor-linux-arm64-gnu@4.0.483", "", { "os": "linux", "cpu": "arm64" }, "sha512-1LzfYDY/DQxy3MIc9pCbTn1ObcBDcyI6MzODOwe1bTZl8TZxdUUt1M3hDrQ60AB6iHwruBAnzT9dnogiMIdSew=="], + + "@remotion/compositor-linux-arm64-musl": ["@remotion/compositor-linux-arm64-musl@4.0.483", "", { "os": "linux", "cpu": "arm64" }, "sha512-nG1btxg0HXuLzulIqAg9NnxWyewZNrA0iLe82HdniaJjeWLORreGo4cEEJz1SjbDpQuPEY562XGkTO2DLKK5mA=="], + + "@remotion/compositor-linux-x64-gnu": ["@remotion/compositor-linux-x64-gnu@4.0.483", "", { "os": "linux", "cpu": "x64" }, "sha512-CV4h6r2rNYh4P8utUzP6RM2vW2vq43nA3IvVOwRSuh9LwJh/7rwG53Oco4HanY/sstaNuymGpT57wdXwONU5CQ=="], + + "@remotion/compositor-linux-x64-musl": ["@remotion/compositor-linux-x64-musl@4.0.483", "", { "os": "linux", "cpu": "x64" }, "sha512-moKvavbkoz5nteJZA4K6LMgTTlewhwIbtO7DYARZ3LK3ClGUKnYfFcmaPS15hMun02JPTOMIWbrlaszdR75mxA=="], + + "@remotion/compositor-win32-x64-msvc": ["@remotion/compositor-win32-x64-msvc@4.0.483", "", { "os": "win32", "cpu": "x64" }, "sha512-eDpWe6Vy7ySHugwxlgaeohqdBwR1etxiymXzT13cgbPw2+p6d55z1tJ7VPTXyfu0o+UdyRTauWqD4msoxN0wow=="], + + "@remotion/licensing": ["@remotion/licensing@4.0.483", "", {}, "sha512-GRtjykrxMmB5Cjpq1oVqeYIRXI95vid6afX0LTLfp1shD6MR4lTZRoRuopxrrZp1E4XFGXoY64b6dlIudgTyBQ=="], + + "@remotion/media-parser": ["@remotion/media-parser@4.0.483", "", {}, "sha512-iva9Eof7QQX+3hGxH/N8pGi80wgdGuzOr825Zn7IuEe7IWdtRYWlFpCEghmyfhrwgaWa7RY51wg77ycR3c3nRg=="], + + "@remotion/media-utils": ["@remotion/media-utils@4.0.483", "", { "dependencies": { "mediabunny": "1.47.0", "remotion": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4T4WQh/U95kCBzgmz/EL/HL8Zeox/AKAL5ufsehrZFgwlcvH1EsKNJH/RWaAtNPRLF+pskEOgFzPYaczHeI8hg=="], + + "@remotion/player": ["@remotion/player@4.0.483", "", { "dependencies": { "remotion": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-yyIQ9vinUqqS0fQeL52bPCWQIr8qDXCqQ4iys8P7dSZncdu8V29p4W2a0/4eKV8bv5+mjnJQ/U6nl9NxBF1hdA=="], + + "@remotion/renderer": ["@remotion/renderer@4.0.483", "", { "dependencies": { "@remotion/licensing": "4.0.483", "@remotion/streaming": "4.0.483", "execa": "5.1.1", "remotion": "4.0.483", "source-map": "0.8.0-beta.0", "ws": "8.21.0" }, "optionalDependencies": { "@remotion/compositor-darwin-arm64": "4.0.483", "@remotion/compositor-darwin-x64": "4.0.483", "@remotion/compositor-linux-arm64-gnu": "4.0.483", "@remotion/compositor-linux-arm64-musl": "4.0.483", "@remotion/compositor-linux-x64-gnu": "4.0.483", "@remotion/compositor-linux-x64-musl": "4.0.483", "@remotion/compositor-win32-x64-msvc": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-eSKdQqm8rcl+GNyOhFsBgcPYWMcxTJWHL5xaNrd037piCaL3sdJ57OEO/AbfEvvOO14zwUvlsONKjlkLlEVyKg=="], + + "@remotion/streaming": ["@remotion/streaming@4.0.483", "", {}, "sha512-96rqrk+l5AfriHOqmqxMn8rI6UJfFY7r5UMDZoSKqXWzTOwj+0VWHTRl9ilZQMXhYACaMSA/5yrL9jgQEK8Yuw=="], + + "@remotion/studio": ["@remotion/studio@4.0.483", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.31", "@remotion/canvas-capture": "4.0.483", "@remotion/media-utils": "4.0.483", "@remotion/player": "4.0.483", "@remotion/renderer": "4.0.483", "@remotion/studio-shared": "4.0.483", "@remotion/timeline-utils": "4.0.483", "@remotion/web-renderer": "4.0.483", "@remotion/zod-types": "4.0.483", "mediabunny": "1.47.0", "memfs": "3.4.3", "open": "8.4.2", "remotion": "4.0.483", "semver": "7.5.3", "zod": "4.3.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-gNa1Lw8NmHl0jwFb5fk7rzA+WFTJQpGDT/49A38t9jpEwoF+h1mKTgY8gA202apzjBHrEECdFE450npnBOUpUw=="], + + "@remotion/studio-server": ["@remotion/studio-server@4.0.483", "", { "dependencies": { "@babel/parser": "7.24.1", "@babel/types": "7.24.0", "@remotion/bundler": "4.0.483", "@remotion/renderer": "4.0.483", "@remotion/studio-shared": "4.0.483", "memfs": "3.4.3", "open": "8.4.2", "prettier": "3.8.1", "recast": "0.23.11", "remotion": "4.0.483", "semver": "7.5.3" } }, "sha512-2FSLbVN5N8bgtQPX+RQRbis557tJJnVTOZuEBZndfC9NOoPdARlSgEnwVer9tBNczUlc6B3KrPxyPSlS3UjL+Q=="], + + "@remotion/studio-shared": ["@remotion/studio-shared@4.0.483", "", { "dependencies": { "remotion": "4.0.483" } }, "sha512-aXxBYUgsWgphHVq7T7bz+ICMGzfii7pcX1IxS5hbLMLJHqlDtk9g+qcxm4kJ2Fjj/Gb5gERKiNzYym8WZTIM2Q=="], + + "@remotion/timeline-utils": ["@remotion/timeline-utils@4.0.483", "", { "dependencies": { "mediabunny": "1.47.0" } }, "sha512-KTTMdpNA5uRLX+iisAITO+V9cJBk17F8EWvuUnAEVmAFOQTbGIyPI9HNw5pZmL7SlzOJdFjAtksTjipeYGkEug=="], + + "@remotion/web-renderer": ["@remotion/web-renderer@4.0.483", "", { "dependencies": { "@mediabunny/aac-encoder": "1.47.0", "@mediabunny/flac-encoder": "1.47.0", "@mediabunny/mp3-encoder": "1.47.0", "@remotion/licensing": "4.0.483", "mediabunny": "1.47.0", "remotion": "4.0.483" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-14iURfVWA/hcF/rJvkid1ZR5nmnVhkTIf071FabCFi6kdViOSVBugSglJ8tUPvWVbWmxlXGDGQD9CSU18IATww=="], + + "@remotion/zod-types": ["@remotion/zod-types@4.0.483", "", { "dependencies": { "remotion": "4.0.483" }, "peerDependencies": { "zod": "4.3.6" } }, "sha512-KIVYIzFpZBDB0wxGGQu5tDBr4XOYRzvrPXjmo6O/FI5Xx7O8hgvJmBs9VDkEwonAOft86sg79hI19F1DTX1p4w=="], + + "@rspack/binding": ["@rspack/binding@1.7.11", "", { "optionalDependencies": { "@rspack/binding-darwin-arm64": "1.7.11", "@rspack/binding-darwin-x64": "1.7.11", "@rspack/binding-linux-arm64-gnu": "1.7.11", "@rspack/binding-linux-arm64-musl": "1.7.11", "@rspack/binding-linux-x64-gnu": "1.7.11", "@rspack/binding-linux-x64-musl": "1.7.11", "@rspack/binding-wasm32-wasi": "1.7.11", "@rspack/binding-win32-arm64-msvc": "1.7.11", "@rspack/binding-win32-ia32-msvc": "1.7.11", "@rspack/binding-win32-x64-msvc": "1.7.11" } }, "sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q=="], + + "@rspack/binding-darwin-arm64": ["@rspack/binding-darwin-arm64@1.7.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig=="], + + "@rspack/binding-darwin-x64": ["@rspack/binding-darwin-x64@1.7.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA=="], + + "@rspack/binding-linux-arm64-gnu": ["@rspack/binding-linux-arm64-gnu@1.7.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA=="], + + "@rspack/binding-linux-arm64-musl": ["@rspack/binding-linux-arm64-musl@1.7.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw=="], + + "@rspack/binding-linux-x64-gnu": ["@rspack/binding-linux-x64-gnu@1.7.11", "", { "os": "linux", "cpu": "x64" }, "sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ=="], + + "@rspack/binding-linux-x64-musl": ["@rspack/binding-linux-x64-musl@1.7.11", "", { "os": "linux", "cpu": "x64" }, "sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg=="], + + "@rspack/binding-wasm32-wasi": ["@rspack/binding-wasm32-wasi@1.7.11", "", { "dependencies": { "@napi-rs/wasm-runtime": "1.0.7" }, "cpu": "none" }, "sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ=="], + + "@rspack/binding-win32-arm64-msvc": ["@rspack/binding-win32-arm64-msvc@1.7.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA=="], + + "@rspack/binding-win32-ia32-msvc": ["@rspack/binding-win32-ia32-msvc@1.7.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw=="], + + "@rspack/binding-win32-x64-msvc": ["@rspack/binding-win32-x64-msvc@1.7.11", "", { "os": "win32", "cpu": "x64" }, "sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q=="], + + "@rspack/core": ["@rspack/core@1.7.11", "", { "dependencies": { "@module-federation/runtime-tools": "0.22.0", "@rspack/binding": "1.7.11", "@rspack/lite-tapable": "1.1.0" }, "peerDependencies": { "@swc/helpers": ">=0.5.1" }, "optionalPeers": ["@swc/helpers"] }, "sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew=="], + + "@rspack/lite-tapable": ["@rspack/lite-tapable@1.1.0", "", {}, "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw=="], + + "@rspack/plugin-react-refresh": ["@rspack/plugin-react-refresh@1.6.1", "", { "dependencies": { "error-stack-parser": "^2.1.4", "html-entities": "^2.6.0" }, "peerDependencies": { "react-refresh": ">=0.10.0 <1.0.0", "webpack-hot-middleware": "2.x" }, "optionalPeers": ["webpack-hot-middleware"] }, "sha512-eqqW5645VG3CzGzFgNg5HqNdHVXY+567PGjtDhhrM8t67caxmsSzRmT5qfoEIfBcGgFkH9vEg7kzXwmCYQdQDw=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], + + "@types/dom-mediacapture-transform": ["@types/dom-mediacapture-transform@0.1.11", "", { "dependencies": { "@types/dom-webcodecs": "*" } }, "sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ=="], + + "@types/dom-webcodecs": ["@types/dom-webcodecs@0.1.13", "", {}, "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ=="], + + "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], + + "@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], + + "@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=="], + + "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="], + + "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="], + + "@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="], + + "@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="], + + "@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="], + + "@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="], + + "@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="], + + "@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="], + + "@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="], + + "@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="], + + "@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="], + + "@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="], + + "@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="], + + "@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="], + + "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="], + + "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="], + + "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="], + + "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], + + "acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="], + + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + + "ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], + + "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.40", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw=="], + + "browserslist": ["browserslist@4.28.4", "", { "dependencies": { "baseline-browser-mapping": "^2.10.38", "caniuse-lite": "^1.0.30001799", "electron-to-chromium": "^1.5.376", "node-releases": "^2.0.48", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="], + + "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="], + + "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "css-loader": ["css-loader@7.1.4", "", { "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.4.40", "postcss-modules-extract-imports": "^3.1.0", "postcss-modules-local-by-default": "^4.0.5", "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", "semver": "^7.6.3" }, "peerDependencies": { "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", "webpack": "^5.27.0" }, "optionalPeers": ["@rspack/core", "webpack"] }, "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], + + "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.379", "", {}, "sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA=="], + + "enhanced-resolve": ["enhanced-resolve@5.24.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw=="], + + "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], + + "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], + + "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=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "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=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], + + "fs-monkey": ["fs-monkey@1.0.3", "", {}, "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="], + + "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="], + + "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "icss-utils": ["icss-utils@5.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA=="], + + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + + "loader-runner": ["loader-runner@4.3.2", "", {}, "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w=="], + + "lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="], + + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "mediabunny": ["mediabunny@1.47.0", "", { "dependencies": { "@types/dom-mediacapture-transform": "^0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-XQMZAcaKPkJ7hQ/Q2fvBdl3ZazQl2WVxDysUbJWh4PuAnLoerdsQBdPTDWdUdK6hh26LQ8Ue94MLLnmpWvlUYg=="], + + "memfs": ["memfs@3.4.3", "", { "dependencies": { "fs-monkey": "1.0.3" } }, "sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "minimist": ["minimist@1.2.6", "", {}, "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="], + + "nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="], + + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + + "node-releases": ["node-releases@2.0.50", "", {}, "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg=="], + + "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "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=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + + "postcss-modules-extract-imports": ["postcss-modules-extract-imports@3.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q=="], + + "postcss-modules-local-by-default": ["postcss-modules-local-by-default@4.2.0", "", { "dependencies": { "icss-utils": "^5.0.0", "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.1.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw=="], + + "postcss-modules-scope": ["postcss-modules-scope@3.2.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA=="], + + "postcss-modules-values": ["postcss-modules-values@4.0.0", "", { "dependencies": { "icss-utils": "^5.0.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@7.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + + "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + + "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], + + "remotion": ["remotion@4.0.483", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-lf4xq4Twn75TQeTavFLrTE4zdck7EKBSx07ZrPv4om40Sist+3G0kq7NNzSjMeJ6G2Wx6vI+0oICD/vv4sGWtg=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], + + "semver": ["semver@7.5.3", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ=="], + + "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=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], + + "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "style-loader": ["style-loader@4.0.0", "", { "peerDependencies": { "webpack": "^5.27.0" } }, "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA=="], + + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "terser": ["terser@5.48.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q=="], + + "terser-webpack-plugin": ["terser-webpack-plugin@5.6.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "to-fast-properties": ["to-fast-properties@2.0.0", "", {}, "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="], + + "tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + + "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=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "watchpack": ["watchpack@2.5.2", "", { "dependencies": { "graceful-fs": "^4.1.2" } }, "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg=="], + + "webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="], + + "webpack": ["webpack@5.105.0", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.19.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.5.1", "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw=="], + + "webpack-sources": ["webpack-sources@3.5.0", "", {}, "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ=="], + + "whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "css-loader/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + } +} diff --git a/artifacts/glm52-rise-video/out/flash-share.mp4 b/artifacts/glm52-rise-video/out/flash-share.mp4 new file mode 100644 index 00000000000..fa0a9920166 --- /dev/null +++ b/artifacts/glm52-rise-video/out/flash-share.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e732068df8f53724058c5d4a3d49440dce1175814fde27b063c718c8be7c4f79 +size 333945 diff --git a/artifacts/glm52-rise-video/out/glm-52-broke-out.mp4 b/artifacts/glm52-rise-video/out/glm-52-broke-out.mp4 new file mode 100644 index 00000000000..684163d71c8 --- /dev/null +++ b/artifacts/glm52-rise-video/out/glm-52-broke-out.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd7dfb21da6ef6e81c68904aab90ebacd6ac5560b9886090920147df2e0737d8 +size 1591636 diff --git a/artifacts/glm52-rise-video/out/june-totals.png b/artifacts/glm52-rise-video/out/june-totals.png new file mode 100644 index 00000000000..678376dbed6 Binary files /dev/null and b/artifacts/glm52-rise-video/out/june-totals.png differ diff --git a/artifacts/glm52-rise-video/out/minimax-climb.mp4 b/artifacts/glm52-rise-video/out/minimax-climb.mp4 new file mode 100644 index 00000000000..de74d9ac0d6 --- /dev/null +++ b/artifacts/glm52-rise-video/out/minimax-climb.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44d0db412b4b4564f6f6882e27f54a0b6e9c540a930ee200effe2671d1f7de75 +size 349608 diff --git a/artifacts/glm52-rise-video/out/novel-1984.mp4 b/artifacts/glm52-rise-video/out/novel-1984.mp4 new file mode 100644 index 00000000000..b74910859c3 --- /dev/null +++ b/artifacts/glm52-rise-video/out/novel-1984.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4711b5fc12c048a153cf43795c568599a08115c8292326460291aae475ad2f1f +size 2569837 diff --git a/artifacts/glm52-rise-video/out/nz-sheep.mp4 b/artifacts/glm52-rise-video/out/nz-sheep.mp4 new file mode 100644 index 00000000000..d1df94d4725 --- /dev/null +++ b/artifacts/glm52-rise-video/out/nz-sheep.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fec0900bb1250e45b75f60c60cd830f02d0b7eaf8fc3cfcad7772f44f5f55208 +size 2788751 diff --git a/artifacts/glm52-rise-video/package.json b/artifacts/glm52-rise-video/package.json new file mode 100644 index 00000000000..6e8b5a6962a --- /dev/null +++ b/artifacts/glm52-rise-video/package.json @@ -0,0 +1,25 @@ +{ + "name": "glm52-rise-video", + "private": true, + "type": "module", + "scripts": { + "render": "remotion render src/index.tsx GLM52Rise out/glm-52-broke-out.mp4 --codec h264 --pixel-format yuv420p", + "render:sheep": "remotion render src/index.tsx NZSheep out/nz-sheep.mp4 --codec h264 --pixel-format yuv420p", + "render:novel": "remotion render src/index.tsx NovelTokens out/novel-1984.mp4 --codec h264 --pixel-format yuv420p", + "render:flash": "remotion render src/index.tsx FlashShare out/flash-share.mp4 --codec h264 --pixel-format yuv420p", + "render:minimax": "remotion render src/index.tsx MiniMaxClimb out/minimax-climb.mp4 --codec h264 --pixel-format yuv420p", + "still:june": "remotion still src/index.tsx JuneTotals out/june-totals.png --frame=0" + }, + "dependencies": { + "@remotion/cli": "^4.0.384", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "remotion": "^4.0.384" + }, + "devDependencies": { + "@types/react": "^19.2.8", + "@types/react-dom": "^19.2.3", + "typescript": "^5.8.2" + }, + "version": "7.4.23" +} diff --git a/artifacts/glm52-rise-video/public/book.jpg b/artifacts/glm52-rise-video/public/book.jpg new file mode 100644 index 00000000000..662ca244cda Binary files /dev/null and b/artifacts/glm52-rise-video/public/book.jpg differ diff --git a/artifacts/glm52-rise-video/public/sheep.jpg b/artifacts/glm52-rise-video/public/sheep.jpg new file mode 100644 index 00000000000..d9ed232a89a Binary files /dev/null and b/artifacts/glm52-rise-video/public/sheep.jpg differ diff --git a/artifacts/glm52-rise-video/src/data.ts b/artifacts/glm52-rise-video/src/data.ts new file mode 100644 index 00000000000..d9fab2a6311 --- /dev/null +++ b/artifacts/glm52-rise-video/src/data.ts @@ -0,0 +1,156 @@ +// Verified against the production opencode-stats PlanetScale DB. +// Scope: tier='Go' (OpenCode Go), dataset='zen', client='all', source='all', grain='day'. +// metric = total_tokens. Daily token volume per model; GLM-5.2 (zhipu) launched Jun 17. +// Segments per day (tokens): glm-5.2, deepseek-v4-flash, deepseek-v4-pro, minimax-m3, all others. + +export type Day = { + date: string + total: number + glm: number + dsf: number + dsp: number + mm: number + others: number +} + +export const days: Day[] = [ + { + date: "Jun 12", + total: 2_283_799_449_383, + glm: 0, + dsf: 1_176_701_653_509, + dsp: 569_527_034_307, + mm: 159_016_250_684, + others: 378_554_510_883, + }, + { + date: "Jun 13", + total: 2_008_462_388_420, + glm: 0, + dsf: 995_338_131_997, + dsp: 445_817_536_548, + mm: 211_743_241_967, + others: 355_563_477_908, + }, + { + date: "Jun 14", + total: 2_007_785_405_251, + glm: 0, + dsf: 983_954_176_228, + dsp: 428_151_999_341, + mm: 262_476_527_930, + others: 333_202_701_752, + }, + { + date: "Jun 15", + total: 2_694_736_103_062, + glm: 0, + dsf: 1_255_893_953_859, + dsp: 632_223_338_376, + mm: 352_507_442_991, + others: 454_111_367_836, + }, + { + date: "Jun 16", + total: 2_838_153_758_908, + glm: 0, + dsf: 1_336_625_283_800, + dsp: 676_480_415_730, + mm: 305_268_829_013, + others: 519_779_230_365, + }, + { + date: "Jun 17", + total: 2_778_964_711_109, + glm: 70_095_977_043, + dsf: 1_339_831_523_555, + dsp: 660_414_395_220, + mm: 251_302_096_157, + others: 457_320_719_134, + }, + { + date: "Jun 18", + total: 2_806_992_430_656, + glm: 201_130_231_172, + dsf: 1_295_599_996_869, + dsp: 595_665_008_776, + mm: 322_205_104_324, + others: 392_392_089_515, + }, + { + date: "Jun 19", + total: 2_419_611_630_232, + glm: 199_086_413_910, + dsf: 1_115_750_468_802, + dsp: 475_965_869_304, + mm: 303_586_698_735, + others: 325_222_179_481, + }, + { + date: "Jun 20", + total: 2_188_278_916_865, + glm: 193_931_516_396, + dsf: 1_050_194_681_012, + dsp: 395_303_435_278, + mm: 281_998_000_337, + others: 266_851_283_842, + }, + { + date: "Jun 21", + total: 2_042_309_961_344, + glm: 181_894_043_118, + dsf: 985_164_570_580, + dsp: 368_194_079_542, + mm: 259_812_551_324, + others: 247_244_716_780, + }, + { + date: "Jun 22", + total: 2_893_934_325_663, + glm: 301_759_048_475, + dsf: 1_298_124_282_989, + dsp: 581_012_596_194, + mm: 371_581_117_839, + others: 341_457_280_166, + }, + { + date: "Jun 23", + total: 3_109_009_321_480, + glm: 282_277_235_158, + dsf: 1_423_571_678_821, + dsp: 627_374_654_587, + mm: 429_416_300_508, + others: 346_369_452_406, + }, + { + date: "Jun 24", + total: 2_939_149_971_595, + glm: 256_497_442_533, + dsf: 1_373_583_023_234, + dsp: 601_270_997_775, + mm: 391_586_493_231, + others: 316_212_014_822, + }, + { + date: "Jun 25", + total: 3_029_641_552_948, + glm: 256_279_657_734, + dsf: 1_481_084_002_776, + dsp: 602_077_167_287, + mm: 375_985_302_874, + others: 314_215_422_277, + }, +] + +export const launchIndex = 5 // Jun 17, first day of GLM-5.2 usage +// GLM-5.2 weekly token volume, Jun 19-25 (sum of glm): 1,671,725,357,324 = 1.67T +export const glmWeekTokensT = 1.672 + +// stacked segments, bottom -> top. GLM-5.2 is the hero (blue); the rest are the field it cut into. +export const segments = [ + { key: "glm", label: "GLM-5.2", color: "#3b5cf6", hero: true }, + { key: "dsf", label: "deepseek-v4-flash", color: "#9ca3ad" }, + { key: "dsp", label: "deepseek-v4-pro", color: "#b3b9c1" }, + { key: "mm", label: "minimax-m3", color: "#c8cdd3" }, + { key: "others", label: "other models", color: "#dde0e4" }, +] as const diff --git a/artifacts/glm52-rise-video/src/flash.tsx b/artifacts/glm52-rise-video/src/flash.tsx new file mode 100644 index 00000000000..5d7aaef9bf5 --- /dev/null +++ b/artifacts/glm52-rise-video/src/flash.tsx @@ -0,0 +1,185 @@ +import React from "react" +import { AbsoluteFill, Easing, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion" + +// stats.opencode.ai design tokens (light theme) +const c = { + bg: "#ffffff", + ink: "#161616", + muted: "#5c5c5c", + faint: "#808080", + line: "#e6e6e6", + dot: "#e4e4e4", + gray: "#aab0b8", + accent: "#3b5cf6", + accentHi: "#5b78ff", +} +const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace' + +const DOT_MASK = + "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E\")" + +// verified: OpenCode Go, week of Jun 22-28, 2026 (2026-W26). share = % of 19.64T total Go tokens. +const bars = [ + { label: "deepseek-v4-flash", share: 48.3, hero: true }, + { label: "deepseek-v4-pro", share: 19.4 }, + { label: "minimax-m3", share: 13.0 }, + { label: "glm-5.2", share: 8.3 }, + { label: "mimo-v2.5", share: 4.3 }, + { label: "kimi-k2.7-code", share: 2.6 }, + { label: "other models", share: 4.1 }, +] + +function DataWordmark({ height = 30 }: { height?: number }) { + return ( + + + + + + + + + ) +} + +export function FlashShare() { + const frame = useCurrentFrame() + const { fps } = useVideoConfig() + + const grow = (i: number) => + Math.min( + 1, + Math.max(0, spring({ frame: frame - 18 - i * 7, fps, config: { damping: 18, stiffness: 120, mass: 0.6 } })), + ) + + return ( + +
+ {/* header */} +
+ +
JUN 22–28, 2026
+
+ + {/* headline (static) */} +
+
+ OPENCODE GO · SHARE OF TOKENS +
+
+
DeepSeek V4 Flash
+
+ 48% +
+
+
+ + {/* bar chart */} +
+ {bars.map((b, i) => { + const g = grow(i) + const pct = b.share * g + return ( +
+
+ {b.label} +
+
+ {/* dotted 100% track — height is a multiple of the 12px tile, anchored bottom, so dots never clip */} +
+ {/* fill */} +
+
+
+ {Math.round(pct)}% +
+
+ ) + })} +
+ + {/* footer */} +
+
+ + DeepSeek V4 Flash · 9.48T tokens · 83.6M requests +
+
opencode.ai/data
+
+
+ + ) +} diff --git a/artifacts/glm52-rise-video/src/index.tsx b/artifacts/glm52-rise-video/src/index.tsx new file mode 100644 index 00000000000..3265151ac54 --- /dev/null +++ b/artifacts/glm52-rise-video/src/index.tsx @@ -0,0 +1,36 @@ +import { Composition, registerRoot } from "remotion" +import { GLM52Rise } from "./video" +import { NZSheep } from "./sheep" +import { NovelTokens } from "./novel" +import { FlashShare } from "./flash" +import { MiniMaxClimb } from "./minimax" +import { JuneTotals } from "./june" + +function Root() { + return ( + <> + + + + + + + + ) +} + +registerRoot(Root) diff --git a/artifacts/glm52-rise-video/src/june.tsx b/artifacts/glm52-rise-video/src/june.tsx new file mode 100644 index 00000000000..aa3f9de9bef --- /dev/null +++ b/artifacts/glm52-rise-video/src/june.tsx @@ -0,0 +1,144 @@ +import React from "react" +import { AbsoluteFill } from "remotion" + +const c = { + bg: "#ffffff", + ink: "#161616", + muted: "#5c5c5c", + faint: "#808080", + line: "#e6e6e6", + dot: "#dcdcdc", + accent: "#3b5cf6", +} +const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace' + +const DOT_MASK = + "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E\")" + +// verified: OpenCode Go (tier=Go, dataset=zen), June 1-30, 2026. +// 72.78T tokens · 651.4M requests · 11.42M sessions -> rounded headline figures. + +function DataWordmark({ height = 30 }: { height?: number }) { + return ( + + + + + + + + + ) +} + +function DotBand() { + return ( +
+ ) +} + +function Metric({ value, label }: { value: string; label: string }) { + return ( +
+
+ {value} +
+
{label}
+
+ ) +} + +export function JuneTotals() { + return ( + +
+ {/* header */} +
+ +
MONTHLY RECAP
+
+ + + {/* hero */} +
+
OPENCODE GO · JUNE 2026
+
+
+ 73T +
+
tokens processed
+
+ + {/* supporting metrics */} +
+ + +
+
+ + {/* footer */} + +
+
opencode.ai/data
+
+
+
+ ) +} diff --git a/artifacts/glm52-rise-video/src/minimax.tsx b/artifacts/glm52-rise-video/src/minimax.tsx new file mode 100644 index 00000000000..44cf09a1e75 --- /dev/null +++ b/artifacts/glm52-rise-video/src/minimax.tsx @@ -0,0 +1,201 @@ +import React from "react" +import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion" + +const c = { + bg: "#ffffff", + ink: "#161616", + muted: "#5c5c5c", + faint: "#808080", + line: "#e6e6e6", + dot: "#e4e4e4", + accent: "#3b5cf6", + accentHi: "#5b78ff", + accentDim: "#aebcf3", +} +const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace' + +const DOT_MASK = + "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E\")" + +// verified: minimax-m3, OpenCode Go (tier=Go, dataset=zen), weekly total_tokens. +// W26 2.559T is +23.2% / +482.3B vs W25 2.077T. +const weeks = [ + { label: "May 25", t: 0.008 }, + { label: "Jun 1", t: 0.429 }, + { label: "Jun 8", t: 1.192 }, + { label: "Jun 15", t: 2.077 }, + { label: "Jun 22", t: 2.559, latest: true }, +] +const AXIS_MAX = 3.0 + +function DataWordmark({ height = 30 }: { height?: number }) { + return ( + + + + + + + + + ) +} + +const CHART_H = 420 // multiple of 12 so the dotted tracks never clip + +export function MiniMaxClimb() { + const frame = useCurrentFrame() + const { fps } = useVideoConfig() + + const grow = (i: number) => + Math.min( + 1, + Math.max(0, spring({ frame: frame - 22 - i * 9, fps, config: { damping: 18, stiffness: 110, mass: 0.6 } })), + ) + + return ( + +
+ {/* header */} +
+ +
JUN 22–28, 2026
+
+ + {/* headline (static) */} +
+
+ OPENCODE GO · WEEKLY TOKENS +
+
+
MiniMax M3
+
+
+ +23.2% +
+
+ WEEK OVER WEEK +
+
+
+
+ + {/* column chart */} +
+
+ {weeks.map((w, i) => { + const g = grow(i) + const h = Math.round((w.t / AXIS_MAX) * CHART_H * g) + return ( +
+ {/* value + bar */} +
+ {/* dotted track */} +
+ {/* fill */} +
+ {/* value label */} +
+ {w.t.toFixed(2)}T +
+
+ {/* week label */} +
+ {w.label} +
+
+ ) + })} +
+
+ + {/* footer */} +
+
+ + 2.56T tokens last week · +482.3B added +
+
opencode.ai/data
+
+
+ + ) +} diff --git a/artifacts/glm52-rise-video/src/novel.tsx b/artifacts/glm52-rise-video/src/novel.tsx new file mode 100644 index 00000000000..9d649de4bbb --- /dev/null +++ b/artifacts/glm52-rise-video/src/novel.tsx @@ -0,0 +1,135 @@ +import React from "react" +import { AbsoluteFill, Easing, Img, interpolate, staticFile, useCurrentFrame } from "remotion" + +const c = { + white: "#ffffff", + dim: "rgba(255,255,255,0.74)", +} +const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace' + +// verified: OpenCode Go, week of Jun 22-28, 2026 (2026-W26) +// 19,642,742,937,105 tokens / 173,651,197 requests = 113,116 tokens/request +const AVG = 113116 +const K = Math.round(AVG / 1000) // 113 + +const nf = new Intl.NumberFormat("en-US") + +function DataWordmark({ height = 30 }: { height?: number }) { + return ( + + + + + + + + + ) +} + +export function NovelTokens() { + const frame = useCurrentFrame() + + const k = Math.round( + K * + interpolate(frame, [18, 92], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: Easing.out(Easing.cubic), + }), + ) + + const zoom = interpolate(frame, [0, 150], [1.06, 1.12], { + extrapolateRight: "clamp", + easing: Easing.inOut(Easing.quad), + }) + + return ( + + + + {/* legibility scrims */} +
+ +
+ {/* header */} +
+ +
JUN 22–28, 2026
+
+ + {/* bottom block */} +
+
+ OPENCODE GO · LAST WEEK +
+
+ {k}K +
+
tokens per request
+ +
+
{nf.format(AVG)} tokens / request · last week
+
opencode.ai/data
+
+
+
+ + ) +} diff --git a/artifacts/glm52-rise-video/src/sheep.tsx b/artifacts/glm52-rise-video/src/sheep.tsx new file mode 100644 index 00000000000..53abb39ee56 --- /dev/null +++ b/artifacts/glm52-rise-video/src/sheep.tsx @@ -0,0 +1,139 @@ +import React from "react" +import { AbsoluteFill, Easing, Img, interpolate, staticFile, useCurrentFrame } from "remotion" + +const c = { + white: "#ffffff", + dim: "rgba(255,255,255,0.72)", + faint: "rgba(255,255,255,0.55)", +} +const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace' + +// verified: NZ OpenCode Go, week of Jun 22-28, 2026 (2026-W26) +const TOKENS = 40_915_594_381 // 40.9B +const SHEEP = 23_600_000 // 23.6M +const PER_SHEEP = Math.round(TOKENS / SHEEP) // 1,734 +const nf = new Intl.NumberFormat("en-US") + +// the correct opencode "DATA" wordmark (white, over photo) +function DataWordmark({ height = 30 }: { height?: number }) { + return ( + + + + + + + + + ) +} + +export function NZSheep() { + const frame = useCurrentFrame() + + const count = Math.round( + PER_SHEEP * + interpolate(frame, [18, 90], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: Easing.out(Easing.cubic), + }), + ) + + // slow Ken Burns push-in (scale up only — never reveals an edge) + const zoom = interpolate(frame, [0, 150], [1.06, 1.12], { + extrapolateRight: "clamp", + easing: Easing.inOut(Easing.quad), + }) + + return ( + + {/* the sheep, staring */} + + + {/* legibility scrims */} +
+ + {/* content */} +
+ {/* header */} +
+ +
JUN 22–28, 2026
+
+ + {/* bottom block */} +
+
+ OPENCODE GO · NEW ZEALAND +
+
+ {nf.format(count)} +
+
tokens per sheep
+ +
+
40.9B tokens ÷ 23.6M sheep · last week
+
opencode.ai/data
+
+
+
+ + ) +} diff --git a/artifacts/glm52-rise-video/src/video.tsx b/artifacts/glm52-rise-video/src/video.tsx new file mode 100644 index 00000000000..41466a7e6c3 --- /dev/null +++ b/artifacts/glm52-rise-video/src/video.tsx @@ -0,0 +1,254 @@ +import React from "react" +import { AbsoluteFill, Easing, interpolate, useCurrentFrame, useVideoConfig } from "remotion" +import { days, launchIndex, glmWeekTokensT, segments } from "./data" + +// stats.opencode.ai design tokens (light theme) +const c = { + bg: "#ffffff", + ink: "#161616", + muted: "#5c5c5c", + faint: "#808080", + line: "#e6e6e6", + dot: "#ededed", + accent: "#3b5cf6", + accentHi: "#5b78ff", +} + +const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace' +const W = 1080 + +const DOT_MASK = + "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E\")" + +const field = segments.filter((s) => !s.hero) +const glmColor = segments.find((s) => s.hero)!.color + +const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v)) + +// the correct opencode "DATA" wordmark (from stats.opencode.ai header) +function DataWordmark({ height = 30, color = c.ink }: { height?: number; color?: string }) { + return ( + + + + + + + + + ) +} + +export function GLM52Rise() { + const frame = useCurrentFrame() + const { fps } = useVideoConfig() + + // ---------- virtual camera ---------- + // open zoomed-in on the field, pan right while the blue fills, then pull back to reveal. + const K = [0, 32, 150, 206, 240] + const ease = Easing.inOut(Easing.cubic) + const opt = { extrapolateLeft: "clamp" as const, extrapolateRight: "clamp" as const, easing: ease } + const s = interpolate(frame, K, [1.82, 1.72, 1.72, 1.0, 1.0], opt) + let fx = interpolate(frame, K, [420, 438, 760, 540, 540], opt) + let fy = interpolate(frame, K, [664, 664, 664, 540, 540], opt) + // keep the framing inside the 1080 canvas so edges never reveal black + fx = clamp(fx, 540 / s, W - 540 / s) + fy = clamp(fy, 540 / s, W - 540 / s) + const camera = `translate(${540 - fx * s}px, ${540 - fy * s}px) scale(${s})` + + // ---------- blue sweep (synced to the pan) ---------- + const p = interpolate(frame, [32, 150], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: ease }) + const revealAmount = p * (days.length - launchIndex) + 0.35 + const fillOf = (i: number) => clamp(revealAmount - (i - launchIndex), 0, 1) + + // token number climbs as the camera pulls back and the headline re-enters frame + const tokensT = + glmWeekTokensT * + interpolate(frame, [150, 202], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: Easing.out(Easing.cubic), + }) + + // chart geometry + const chartH = 440 + const chartW = 936 + const gap = 14 + const EXAGGERATE = 2.876 // broken y-axis: GLM-5.2 magnified ~2.9x for emphasis + + return ( + +
+
+ {/* header */} +
+ +
JUN 12–25, 2026
+
+ + {/* headline (static) */} +
+
+ GLM-5.2 broke out +
+
+ From 0 to {tokensT.toFixed(2)}T tokens in a week. +
+
+ + {/* stacked chart */} +
+
+ {/* faint dotted backdrop */} +
+ + {/* columns */} +
+ {days.map((d, i) => { + const glmShare = d.glm / d.total + const blueH = Math.round(glmShare * EXAGGERATE * chartH) + const filled = Math.round(blueH * fillOf(i)) + const slotGap = filled > 2 ? 5 : 0 + const fieldH = chartH - filled - slotGap + const fieldTotal = d.dsf + d.dsp + d.mm + d.others + return ( +
+ {/* gray field of other models (already in place) */} +
+ {field.map((seg) => ( +
+ ))} +
+ {/* GLM-5.2, animates in */} + {filled > 2 && ( +
+ )} +
+ ) + })} +
+
+
+ + {/* day axis */} +
+ {days.map((d, i) => ( +
+ {i === 0 || i === launchIndex || i === days.length - 1 ? d.date : ""} +
+ ))} +
+ + {/* footer */} +
+
+ + GLM-5.2 +
+
opencode.ai/data
+
+
+
+ + ) +} diff --git a/artifacts/glm52-rise-video/sst-env.d.ts b/artifacts/glm52-rise-video/sst-env.d.ts new file mode 100644 index 00000000000..64441936d7a --- /dev/null +++ b/artifacts/glm52-rise-video/sst-env.d.ts @@ -0,0 +1,10 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +/* deno-fmt-ignore-file */ +/* biome-ignore-all lint: auto-generated */ + +/// + +import "sst" +export {} \ No newline at end of file diff --git a/bun.lock b/bun.lock index a7071637366..229bc772e3e 100644 --- a/bun.lock +++ b/bun.lock @@ -5,12 +5,15 @@ "": { "name": "@kilocode/kilo", "dependencies": { + "@aws-sdk/client-s3": "3.933.0", "@kilocode/plugin": "workspace:*", "@kilocode/sdk": "workspace:*", "@opencode-ai/script": "workspace:*", + "heap-snapshot-toolkit": "1.1.3", "typescript": "catalog:", }, "devDependencies": { + "@actions/artifact": "5.0.1", "@changesets/changelog-github": "^0.5.1", "@changesets/cli": "^2.27.10", "@tsconfig/bun": "catalog:", @@ -23,54 +26,111 @@ "oxlint-tsgolint": "0.21.0", "prettier": "3.6.2", "semver": "^7.6.0", - "turbo": "2.9.14", + "sst": "catalog:", + "turbo": "2.10.2", + }, + }, + "packages/client": { + "name": "@opencode-ai/client", + "version": "7.4.23", + "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", + }, + "optionalPeers": [ + "effect", + ], + }, + "packages/codemode": { + "name": "@opencode-ai/codemode", + "version": "7.4.23", + "dependencies": { + "acorn": "8.15.0", + "effect": "catalog:", + "typescript": "catalog:", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", }, }, "packages/core": { "name": "@opencode-ai/core", - "version": "7.3.54", + "version": "7.4.23", "bin": { "opencode": "./bin/opencode", }, "dependencies": { "@ai-sdk/alibaba": "1.0.17", - "@ai-sdk/amazon-bedrock": "4.0.107", - "@ai-sdk/anthropic": "3.0.71", - "@ai-sdk/azure": "3.0.49", + "@ai-sdk/amazon-bedrock": "4.0.112", + "@ai-sdk/anthropic": "3.0.82", + "@ai-sdk/azure": "3.0.93", "@ai-sdk/cerebras": "2.0.54", "@ai-sdk/cohere": "3.0.27", "@ai-sdk/deepinfra": "2.0.41", - "@ai-sdk/gateway": "3.0.104", + "@ai-sdk/gateway": "3.0.157", "@ai-sdk/google": "3.0.73", "@ai-sdk/google-vertex": "4.0.128", "@ai-sdk/groq": "3.0.31", - "@ai-sdk/mistral": "3.0.27", - "@ai-sdk/openai": "3.0.53", + "@ai-sdk/mistral": "3.0.51", + "@ai-sdk/openai": "3.0.88", "@ai-sdk/openai-compatible": "2.0.48", "@ai-sdk/perplexity": "3.0.26", - "@ai-sdk/provider": "3.0.8", - "@ai-sdk/provider-utils": "4.0.23", + "@ai-sdk/provider": "3.0.14", + "@ai-sdk/provider-utils": "4.0.40", "@ai-sdk/togetherai": "2.0.41", "@ai-sdk/vercel": "2.0.39", - "@ai-sdk/xai": "3.0.92", - "@aws-sdk/credential-providers": "3.993.0", + "@ai-sdk/xai": "3.0.102", + "@aws-sdk/credential-providers": "3.1057.0", "@effect/opentelemetry": "catalog:", "@effect/platform-node": "catalog:", + "@effect/sql-sqlite-bun": "catalog:", + "@ff-labs/fff-bun": "0.9.4", "@kilocode/kilo-gateway": "workspace:*", + "@kilocode/kilo-indexing": "workspace:*", + "@kilocode/plugin": "workspace:*", "@kilocode/sandbox": "workspace:*", + "@lydell/node-pty": "catalog:", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", + "@opencode-ai/effect-drizzle-sqlite": "workspace:*", + "@opencode-ai/effect-sqlite-node": "workspace:*", + "@opencode-ai/llm": "workspace:*", + "@opencode-ai/schema": "workspace:*", "@openrouter/ai-sdk-provider": "2.9.0", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", + "@parcel/watcher": "2.5.1", + "@silvia-odwyer/photon-node": "0.3.4", "ai-gateway-provider": "3.1.2", + "bun-pty": "0.4.8", "cross-spawn": "catalog:", + "diff": "catalog:", + "drizzle-orm": "catalog:", "effect": "catalog:", - "gitlab-ai-provider": "6.8.0", + "fuzzysort": "3.1.0", + "gitlab-ai-provider": "6.12.1", "glob": "13.0.5", "google-auth-library": "10.5.0", + "gray-matter": "4.0.3", + "htmlparser2": "8.0.2", + "ignore": "7.0.5", "immer": "11.1.4", "jsonc-parser": "3.3.1", "mime-types": "3.0.2", @@ -78,22 +138,37 @@ "npm-package-arg": "13.0.2", "rotating-file-stream": "3.2.9", "semver": "^7.6.3", - "venice-ai-sdk-provider": "2.0.2", + "turndown": "7.2.0", + "venice-ai-sdk-provider": "2.1.1", + "which": "6.0.1", "xdg-basedir": "5.1.0", "zod": "catalog:", }, "devDependencies": { + "@opencode-ai/http-recorder": "workspace:*", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@types/cross-spawn": "catalog:", + "@types/node": "catalog:", "@types/npm-package-arg": "6.1.4", "@types/npmcli__arborist": "6.3.3", "@types/semver": "catalog:", + "@types/turndown": "5.0.5", + "@types/which": "3.0.4", + "drizzle-kit": "catalog:", }, }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "7.3.54", + "version": "7.4.23", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -105,13 +180,46 @@ "@typescript/native-preview": "catalog:", }, }, + "packages/effect-sqlite-node": { + "name": "@opencode-ai/effect-sqlite-node", + "version": "7.4.23", + "dependencies": { + "effect": "catalog:", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + }, + }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "7.3.54", + "version": "7.4.23", "dependencies": { - "@effect/platform-node": "catalog:", + "@effect/platform-node": "4.0.0-beta.83", + "@effect/platform-node-shared": "4.0.0-beta.83", "effect": "catalog:", }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@tsconfig/node22": "catalog:", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "effect": "catalog:", + "typescript": "catalog:", + }, + "peerDependencies": { + "effect": "4.0.0-beta.83", + }, + }, + "packages/httpapi-codegen": { + "name": "@opencode-ai/httpapi-codegen", + "version": "7.4.23", + "dependencies": { + "effect": "catalog:", + "prettier": "3.6.2", + }, "devDependencies": { "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", @@ -120,8 +228,9 @@ }, "packages/kilo-console": { "name": "@kilocode/kilo-console", - "version": "7.3.54", + "version": "7.4.23", "dependencies": { + "@kilocode/kilo-indexing": "workspace:*", "@kilocode/kilo-web-ui": "workspace:*", "@kilocode/sdk": "workspace:*", "@lottiefiles/dotlottie-web": "0.74.0", @@ -142,7 +251,7 @@ }, "packages/kilo-docs": { "name": "@kilocode/kilo-docs", - "version": "7.3.54", + "version": "7.4.23", "dependencies": { "@docsearch/css": "^4", "@docsearch/js": "^4", @@ -172,12 +281,10 @@ }, "packages/kilo-gateway": { "name": "@kilocode/kilo-gateway", - "version": "7.3.54", + "version": "7.4.23", "dependencies": { - "@ai-sdk/alibaba": "1.0.17", - "@ai-sdk/anthropic": "3.0.71", - "@ai-sdk/mistral": "3.0.27", - "@ai-sdk/openai": "3.0.53", + "@ai-sdk/anthropic": "3.0.82", + "@ai-sdk/openai": "3.0.88", "@ai-sdk/openai-compatible": "2.0.48", "@clack/prompts": "1.0.0-alpha.1", "@kilocode/plugin": "workspace:*", @@ -208,7 +315,7 @@ }, "packages/kilo-i18n": { "name": "@kilocode/kilo-i18n", - "version": "7.3.54", + "version": "7.4.23", "devDependencies": { "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", @@ -218,7 +325,7 @@ }, "packages/kilo-indexing": { "name": "@kilocode/kilo-indexing", - "version": "7.3.54", + "version": "7.4.23", "dependencies": { "@aws-sdk/client-bedrock-runtime": "3.1005.0", "@aws-sdk/credential-provider-ini": "3.972.31", @@ -250,13 +357,29 @@ }, "packages/kilo-jetbrains": { "name": "@kilocode/kilo-jetbrains", - "version": "7.3.54", + "version": "7.4.23", + }, + "packages/kilo-memory": { + "name": "@kilocode/kilo-memory", + "version": "7.4.23", + "dependencies": { + "effect": "catalog:", + "zod": "catalog:", + }, + "devDependencies": { + "@tsconfig/node22": "catalog:", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:", + }, }, "packages/kilo-sandbox": { "name": "@kilocode/sandbox", - "version": "7.3.52", + "version": "7.4.23", "dependencies": { + "@anthropic-ai/sandbox-runtime": "catalog:", "effect": "catalog:", + "ipaddr.js": "catalog:", }, "devDependencies": { "@effect/platform-node": "catalog:", @@ -267,7 +390,7 @@ }, "packages/kilo-telemetry": { "name": "@kilocode/kilo-telemetry", - "version": "7.3.54", + "version": "7.4.23", "dependencies": { "@kilocode/kilo-gateway": "workspace:*", "posthog-node": "4.4.0", @@ -281,7 +404,7 @@ }, "packages/kilo-ui": { "name": "@kilocode/kilo-ui", - "version": "7.3.54", + "version": "7.4.23", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "0.13.11", @@ -291,6 +414,7 @@ "@solid-primitives/media": "2.3.3", "@solid-primitives/resize-observer": "2.1.5", "@solid-primitives/rootless": "1.5.2", + "chart.js": "4.5.1", "diff": "catalog:", "lucide-solid": "0.576.0", "motion": "12.34.5", @@ -308,7 +432,7 @@ "storybook": "10.2.10", "storybook-solidjs-vite": "10.0.9", "typescript": "catalog:", - "vite": "7.3.2", + "vite": "7.3.5", "vite-plugin-solid": "2.11.10", }, "peerDependencies": { @@ -318,18 +442,21 @@ }, "packages/kilo-vscode": { "name": "kilo-code", - "version": "7.3.54", + "version": "7.4.23", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@kilocode/kilo-gateway": "workspace:*", "@kilocode/kilo-i18n": "workspace:*", "@kilocode/kilo-indexing": "workspace:*", + "@kilocode/kilo-memory": "workspace:*", "@kilocode/kilo-ui": "workspace:*", "@kilocode/plugin": "workspace:*", "@kilocode/sdk": "workspace:*", + "@opencode-ai/core": "workspace:*", "@opencode-ai/ui": "workspace:*", "@pierre/diffs": "catalog:", "@thisbeyond/solid-dnd": "0.7.5", + "@vscode/codicons": "^0.0.44", "@xterm/addon-clipboard": "0.2.0", "@xterm/addon-fit": "0.11.0", "@xterm/addon-unicode-graphemes": "0.4.0", @@ -355,6 +482,8 @@ }, "devDependencies": { "@axe-core/playwright": "4.11.3", + "@babel/core": "^7.28.4", + "@babel/preset-typescript": "^7.27.1", "@playwright/test": "1.57.0", "@storybook/addon-a11y": "10.2.10", "@storybook/addon-docs": "10.2.10", @@ -366,6 +495,7 @@ "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "^3.7.1", + "babel-preset-solid": "^1.9.9", "esbuild": "^0.27.2", "esbuild-plugin-solid": "^0.6.0", "eslint": "^9.39.2", @@ -379,13 +509,13 @@ "ts-morph": "27.0.2", "typescript": "^5.9.3", "typescript-eslint": "^8.54.0", - "vite": "7.3.2", + "vite": "7.3.5", "vite-plugin-solid": "2.11.10", }, }, "packages/kilo-web-ui": { "name": "@kilocode/kilo-web-ui", - "version": "7.3.54", + "version": "7.4.23", "dependencies": { "@kilocode/kilo-ui": "workspace:*", "@kobalte/core": "catalog:", @@ -402,8 +532,9 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "7.3.54", + "version": "7.4.23", "dependencies": { + "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", "aws4fetch": "1.0.20", @@ -420,7 +551,7 @@ }, "packages/opencode": { "name": "@kilocode/cli", - "version": "7.3.54", + "version": "7.4.23", "bin": { "kilo": "./bin/kilo", "kilocode": "./bin/kilo", @@ -430,33 +561,35 @@ "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.21.0", "@ai-sdk/alibaba": "1.0.17", - "@ai-sdk/amazon-bedrock": "4.0.96", - "@ai-sdk/anthropic": "3.0.71", - "@ai-sdk/azure": "3.0.49", - "@ai-sdk/cerebras": "2.0.54", + "@ai-sdk/amazon-bedrock": "4.0.112", + "@ai-sdk/anthropic": "3.0.82", + "@ai-sdk/azure": "3.0.93", + "@ai-sdk/cerebras": "2.0.60", "@ai-sdk/cohere": "3.0.27", "@ai-sdk/deepinfra": "2.0.41", - "@ai-sdk/gateway": "3.0.104", - "@ai-sdk/google": "3.0.63", - "@ai-sdk/google-vertex": "4.0.112", + "@ai-sdk/gateway": "3.0.157", + "@ai-sdk/google": "3.0.73", + "@ai-sdk/google-vertex": "4.0.128", "@ai-sdk/groq": "3.0.31", - "@ai-sdk/mistral": "3.0.27", - "@ai-sdk/openai": "3.0.53", + "@ai-sdk/mistral": "3.0.51", + "@ai-sdk/openai": "3.0.88", "@ai-sdk/openai-compatible": "2.0.48", "@ai-sdk/perplexity": "3.0.26", - "@ai-sdk/provider": "3.0.8", - "@ai-sdk/provider-utils": "4.0.23", + "@ai-sdk/provider": "3.0.14", + "@ai-sdk/provider-utils": "4.0.40", "@ai-sdk/togetherai": "2.0.41", "@ai-sdk/vercel": "2.0.39", - "@ai-sdk/xai": "3.0.92", - "@aws-sdk/credential-providers": "3.1025.0", + "@ai-sdk/xai": "3.0.102", + "@aws-sdk/credential-providers": "3.1057.0", "@clack/prompts": "1.0.0-alpha.1", "@effect/opentelemetry": "catalog:", "@effect/platform-node": "catalog:", + "@ff-labs/fff-bun": "0.9.4", "@gitlab/gitlab-ai-provider": "3.6.0", "@gitlab/opencode-gitlab-auth": "1.3.3", "@kilocode/kilo-gateway": "workspace:*", "@kilocode/kilo-indexing": "workspace:*", + "@kilocode/kilo-memory": "workspace:*", "@kilocode/kilo-telemetry": "workspace:*", "@kilocode/plugin": "workspace:*", "@kilocode/plugin-atomic-chat": "workspace:*", @@ -464,26 +597,35 @@ "@kilocode/sdk": "workspace:*", "@lydell/node-pty": "catalog:", "@modelcontextprotocol/sdk": "1.29.0", - "@morphllm/morphsdk": "0.2.166", "@npmcli/arborist": "9.4.0", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", + "@openauthjs/openauth": "catalog:", + "@opencode-ai/codemode": "workspace:*", "@opencode-ai/llm": "workspace:*", + "@opencode-ai/protocol": "workspace:*", + "@opencode-ai/schema": "workspace:*", "@opencode-ai/script": "workspace:*", + "@opencode-ai/server": "workspace:*", + "@opencode-ai/tui": "workspace:*", "@opencode-ai/ui": "workspace:*", "@openrouter/ai-sdk-provider": "2.9.0", "@opentelemetry/api": "1.9.0", + "@opentelemetry/context-async-hooks": "2.6.1", + "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", "@opentui/core": "catalog:", "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", "@parcel/watcher": "2.5.1", + "@pierre/diffs": "catalog:", "@secretlint/core": "10.2.2", "@secretlint/secretlint-rule-preset-recommend": "10.2.2", "@silvia-odwyer/photon-node": "0.3.4", "@solid-primitives/event-bus": "1.1.2", "@solid-primitives/scheduled": "1.5.2", + "@standard-schema/spec": "1.0.0", "@types/ws": "8.18.1", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", @@ -491,6 +633,7 @@ "bonjour-service": "1.3.0", "bun-pty": "0.4.8", "chardet": "2.1.1", + "chokidar": "4.0.3", "cli-sound": "1.1.3", "clipboardy": "4.0.0", "cross-spawn": "catalog:", @@ -499,7 +642,8 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.8.0", + "gitlab-ai-provider": "6.12.1", + "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", "htmlparser2": "8.0.2", @@ -509,11 +653,12 @@ "jsonc-parser": "3.3.1", "mammoth": "1.12.0", "mime-types": "3.0.2", + "minimatch": "10.0.3", "npm-package-arg": "13.0.2", "open": "10.1.2", - "opencode-gitlab-auth": "2.0.1", + "opencode-gitlab-auth": "2.1.0", "opencode-poe-auth": "0.0.1", - "opentui-spinner": "catalog:", + "partial-json": "0.1.7", "remeda": "catalog:", "ripgrep": "0.3.1", "semver": "^7.6.3", @@ -525,16 +670,18 @@ "tree-sitter-wasms": "^0.1.12", "turndown": "7.2.0", "ulid": "catalog:", - "venice-ai-sdk-provider": "2.0.1", + "venice-ai-sdk-provider": "2.1.1", "vscode-jsonrpc": "8.2.1", "web-tree-sitter": "0.25.10", "which": "6.0.1", "ws": "8.21.0", + "xdg-basedir": "5.1.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "yargs": "18.0.0", "zod": "catalog:", }, "devDependencies": { + "@babel/core": "7.28.4", "@effect/language-service": "0.84.2", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/core": "workspace:*", @@ -548,7 +695,9 @@ "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1", + "@standard-schema/spec": "1.0.0", "@tsconfig/bun": "catalog:", + "@types/babel__core": "7.20.5", "@types/bun": "catalog:", "@types/cross-spawn": "catalog:", "@types/mime-types": "3.0.1", @@ -570,8 +719,9 @@ }, "packages/plugin": { "name": "@kilocode/plugin", - "version": "7.3.54", + "version": "7.4.23", "dependencies": { + "@ai-sdk/provider": "3.0.8", "@kilocode/sdk": "workspace:*", "effect": "catalog:", "zod": "catalog:", @@ -586,9 +736,9 @@ "typescript": "catalog:", }, "peerDependencies": { - "@opentui/core": ">=0.2.15", - "@opentui/keymap": ">=0.2.15", - "@opentui/solid": ">=0.2.15", + "@opentui/core": ">=0.4.5", + "@opentui/keymap": ">=0.4.5", + "@opentui/solid": ">=0.4.5", }, "optionalPeers": [ "@opentui/core", @@ -598,7 +748,7 @@ }, "packages/plugin-atomic-chat": { "name": "@kilocode/plugin-atomic-chat", - "version": "7.3.54", + "version": "7.4.23", "dependencies": { "@kilocode/plugin": "workspace:*", }, @@ -610,9 +760,34 @@ "vitest": "^4.0.16", }, }, + "packages/protocol": { + "name": "@opencode-ai/protocol", + "version": "7.4.23", + "dependencies": { + "@opencode-ai/schema": "workspace:*", + "effect": "catalog:", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", + }, + }, + "packages/schema": { + "name": "@opencode-ai/schema", + "version": "7.4.23", + "dependencies": { + "effect": "catalog:", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", + }, + }, "packages/script": { "name": "@opencode-ai/script", - "version": "7.3.54", + "version": "7.4.23", "dependencies": { "semver": "^7.6.3", }, @@ -621,9 +796,24 @@ "@types/semver": "^7.5.8", }, }, + "packages/sdk-next": { + "name": "@opencode-ai/sdk-next", + "version": "7.4.23", + "dependencies": { + "@opencode-ai/client": "workspace:*", + "@opencode-ai/core": "workspace:*", + "@opencode-ai/server": "workspace:*", + "effect": "catalog:", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", + }, + }, "packages/sdk/js": { "name": "@kilocode/sdk", - "version": "7.3.54", + "version": "7.4.23", "dependencies": { "cross-spawn": "catalog:", }, @@ -636,10 +826,71 @@ "typescript": "catalog:", }, }, + "packages/server": { + "name": "@opencode-ai/server", + "version": "7.4.23", + "dependencies": { + "@opencode-ai/core": "workspace:*", + "@opencode-ai/protocol": "workspace:*", + "drizzle-orm": "catalog:", + "effect": "catalog:", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", + }, + }, + "packages/session-ui": { + "name": "@opencode-ai/session-ui", + "version": "7.4.23", + "dependencies": { + "@kilocode/sdk": "workspace:*", + "@kobalte/core": "catalog:", + "@opencode-ai/client": "workspace:*", + "@opencode-ai/core": "workspace:*", + "@opencode-ai/ui": "workspace:*", + "@pierre/diffs": "catalog:", + "@shikijs/stream": "catalog:", + "@shikijs/transformers": "3.9.2", + "@solid-primitives/bounds": "0.1.3", + "@solid-primitives/event-listener": "2.4.5", + "@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:", + "katex": "0.16.27", + "luxon": "catalog:", + "marked": "catalog:", + "marked-katex-extension": "5.1.6", + "marked-shiki": "catalog:", + "morphdom": "2.7.8", + "motion": "12.34.5", + "remeda": "catalog:", + "remend": "catalog:", + "shiki": "catalog:", + "solid-js": "catalog:", + "solid-list": "catalog:", + "strip-ansi": "7.1.2", + }, + "devDependencies": { + "@tsconfig/node22": "catalog:", + "@types/bun": "catalog:", + "@types/katex": "0.16.7", + "@types/luxon": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:", + "vite": "catalog:", + }, + }, "packages/storybook": { "name": "@opencode-ai/storybook", - "version": "7.3.54", + "version": "7.4.23", "devDependencies": { + "@opencode-ai/session-ui": "workspace:*", "@opencode-ai/ui": "workspace:*", "@solidjs/meta": "catalog:", "@storybook/addon-a11y": "^10.2.13", @@ -652,6 +903,7 @@ "@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", @@ -659,14 +911,43 @@ "vite": "catalog:", }, }, + "packages/tui": { + "name": "@opencode-ai/tui", + "version": "7.4.23", + "dependencies": { + "@kilocode/plugin": "workspace:*", + "@kilocode/sdk": "workspace:*", + "@opencode-ai/core": "workspace:*", + "@opencode-ai/ui": "workspace:*", + "@opentui/core": "catalog:", + "@opentui/keymap": "catalog:", + "@opentui/solid": "catalog:", + "clipboardy": "4.0.0", + "diff": "catalog:", + "effect": "catalog:", + "fuzzysort": "catalog:", + "open": "10.1.2", + "remeda": "catalog:", + "solid-js": "catalog:", + "strip-ansi": "7.1.2", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", + }, + }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "7.3.54", + "version": "7.4.23", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", "@pierre/diffs": "catalog:", + "@shikijs/stream": "catalog:", + "@shikijs/transformers": "3.9.2", + "@solid-primitives/bounds": "0.1.3", "@solid-primitives/event-listener": "2.4.5", "@solid-primitives/media": "2.3.3", "@solid-primitives/resize-observer": "2.1.3", @@ -677,7 +958,10 @@ "dompurify": "3.4.2", "fuzzysort": "catalog:", "katex": "0.16.27", + "luxon": "catalog:", "marked": "catalog:", + "marked-katex-extension": "5.1.6", + "marked-shiki": "catalog:", "mermaid": "11.15.0", "morphdom": "2.7.8", "motion": "12.34.5", @@ -688,37 +972,56 @@ "shiki": "catalog:", "solid-js": "catalog:", "solid-list": "catalog:", + "solid-sonner": "catalog:", "strip-ansi": "7.1.2", "virtua": "catalog:", }, "devDependencies": { + "@solidjs/meta": "catalog:", "@tailwindcss/vite": "catalog:", "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", "@types/katex": "0.16.7", + "@types/luxon": "catalog:", "@typescript/native-preview": "catalog:", + "happy-dom": "20.8.9", + "solid-js": "catalog:", "tailwindcss": "catalog:", + "tw-animate-css": "1.4.0", "typescript": "catalog:", "vite": "catalog:", "vite-plugin-icons-spritesheet": "3.0.1", "vite-plugin-solid": "catalog:", }, + "peerDependencies": { + "@solidjs/meta": "^0.29.0", + "solid-js": "^1.9.0", + }, }, }, "trustedDependencies": [ "esbuild", - "tree-sitter-powershell", "protobufjs", "web-tree-sitter", "tree-sitter-bash", ], "patchedDependencies": { - "mammoth@1.12.0": "patches/mammoth@1.12.0.patch", + "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", + "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", + "@ff-labs/fff-bun@0.9.4": "patches/@ff-labs%2Ffff-bun@0.9.4.patch", + "pacote@21.5.1": "patches/pacote@21.5.1.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", + "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", + "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", + "solid-js@1.9.12": "patches/solid-js@1.9.12.patch", + "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", + "virtua@0.49.1": "patches/virtua@0.49.1.patch", + "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", + "mammoth@1.12.0": "patches/mammoth@1.12.0.patch", }, "overrides": { - "@effect/platform-node-shared": "4.0.0-beta.46", + "@effect/platform-node-shared": "4.0.0-beta.74", "@opentui/core": "catalog:", "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", @@ -736,27 +1039,34 @@ "picomatch": ">=2.3.2", "smol-toml": ">=1.6.1", "solid-js": "catalog:", + "vite": "7.3.5", }, "catalog": { + "@anthropic-ai/sandbox-runtime": "0.0.63", "@cloudflare/workers-types": "4.20251008.0", - "@effect/opentelemetry": "4.0.0-beta.66", - "@effect/platform-node": "4.0.0-beta.66", - "@effect/sql-sqlite-bun": "4.0.0-beta.66", + "@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", + "@hono/standard-validator": "0.2.0", + "@hono/zod-validator": "0.4.2", "@kobalte/core": "0.13.11", "@lydell/node-pty": "1.2.0-beta.12", "@npmcli/arborist": "9.4.0", "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@opentui/core": "0.2.15", - "@opentui/keymap": "0.2.15", - "@opentui/solid": "0.2.15", - "@pierre/diffs": "1.1.22", + "@opentui/core": "0.4.5", + "@opentui/keymap": "0.4.5", + "@opentui/solid": "0.4.5", + "@pierre/diffs": "1.2.10", "@playwright/test": "1.59.1", + "@shikijs/stream": "4.2.0", "@solid-primitives/storage": "4.3.3", "@solidjs/meta": "0.29.4", "@solidjs/router": "0.15.4", "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020", "@tailwindcss/vite": "4.1.11", + "@tanstack/solid-virtual": "3.13.32", "@tsconfig/bun": "1.0.9", "@tsconfig/node22": "22.0.2", "@types/bun": "1.3.14", @@ -765,42 +1075,46 @@ "@types/node": "24.12.4", "@types/semver": "7.7.1", "@typescript/native-preview": "7.0.0-dev.20260316.1", - "ai": "6.0.168", + "ai": "6.0.235", "cross-spawn": "7.0.6", "diff": "8.0.4", "dompurify": "3.4.2", "drizzle-kit": "1.0.0-rc.2", "drizzle-orm": "1.0.0-rc.2", - "effect": "4.0.0-beta.66", + "effect": "4.0.0-beta.83", "fuzzysort": "3.1.0", "hono": "4.12.12", "hono-openapi": "1.1.2", + "ipaddr.js": "2.4.0", "luxon": "3.6.1", - "marked": "17.0.1", + "marked": "18.0.7", "marked-shiki": "1.2.1", - "opentui-spinner": "0.0.6", "remeda": "2.26.0", "remend": "1.3.0", "semver": "7.7.4", - "shiki": "3.20.0", + "shiki": "4.2.0", "solid-js": "1.9.12", "solid-list": "0.3.0", + "solid-sonner": "0.3.1", + "sst": "4.13.1", "tailwindcss": "4.1.11", "typescript": "5.8.2", "ulid": "3.0.1", - "virtua": "0.42.3", - "vite": "7.3.2", + "virtua": "0.49.1", + "vite": "7.3.5", "vite-plugin-solid": "2.11.10", "zod": "4.1.8", }, "packages": { + "@actions/artifact": ["@actions/artifact@5.0.1", "", { "dependencies": { "@actions/core": "^2.0.0", "@actions/github": "^6.0.1", "@actions/http-client": "^3.0.0", "@azure/storage-blob": "^12.29.1", "@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-dHJ5rHduhCKUikKTT9eXeWoUvfKia3IjR1sO/VTAV3DVAL4yMTRnl2iO5mcfiBjySHLwPNezwENAVskKYU5ymw=="], + "@actions/core": ["@actions/core@1.11.1", "", { "dependencies": { "@actions/exec": "^1.1.1", "@actions/http-client": "^2.0.1" } }, "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A=="], "@actions/exec": ["@actions/exec@1.1.1", "", { "dependencies": { "@actions/io": "^1.0.1" } }, "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w=="], "@actions/github": ["@actions/github@6.0.1", "", { "dependencies": { "@actions/http-client": "^2.2.0", "@octokit/core": "^5.0.1", "@octokit/plugin-paginate-rest": "^9.2.2", "@octokit/plugin-rest-endpoint-methods": "^10.4.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "undici": "^5.28.5" } }, "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw=="], - "@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], + "@actions/http-client": ["@actions/http-client@3.0.2", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^6.23.0" } }, "sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA=="], "@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="], @@ -810,51 +1124,51 @@ "@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "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-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="], - "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.96", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Mc4Ias2jRMD1jOB6xWtKNPdhECeuCZyIlbr9EAGfBnyBt++sS13ziZh9qv9TdyMCAZJ7xoQcpbchoRJcKwPdpA=="], + "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.112", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.81", "@ai-sdk/openai": "3.0.67", "@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-PsSh7a6qW+3kQXPs1kD4wDwuZby0t1PIaB6j/1aMKmPFJ5LxcIcULLMF/bjITLt5o/8lc0t6TXIwG0zlhH7uZw=="], - "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="], + "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.82", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-WKKou2wbhGGYV8PSALAPyV2YY4nfCqCPkyBzYtJtDA9yCcIFwsbtkTNgg7bqtLCVzeEsY7wwxRoCWy+EMfrw/A=="], - "@ai-sdk/azure": ["@ai-sdk/azure@3.0.49", "", { "dependencies": { "@ai-sdk/openai": "3.0.48", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wskgAL+OmrHG7by/iWIxEBQCEdc1mDudha/UZav46i0auzdFfsDB/k2rXZaC4/3nWSgMZkxr0W3ncyouEGX/eg=="], + "@ai-sdk/azure": ["@ai-sdk/azure@3.0.93", "", { "dependencies": { "@ai-sdk/deepseek": "2.0.50", "@ai-sdk/openai": "3.0.88", "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.40" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9nKNmRtopjE3+L+286/4sLBDfDvmdsL7fiQXG0TE38NywxkDDLUhdr0KSllkRQ+m1/8bO7x4rIVhHSjQQiqmnQ=="], - "@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.54", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.48", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-m+oLYlZUA0j5aeE8BrjDd2DSCx6HujqPB2C0YjQF7iGQ4sTCCmhHfH2o0uY7vlSPIJpA2ak6YyF7R14a5dq8fw=="], + "@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.60", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.54", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rnok3cThg6awBwaDSyiZpgRpbV7pqxGYrA89LODCo5cuEHeP2h0AM0lLHP7zIkclAdXfOm4wldKi/S2T/DGCOw=="], "@ai-sdk/cohere": ["@ai-sdk/cohere@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OqcCq2PiFY1dbK/0Ck45KuvE8jfdxRuuAE9Y5w46dAk6U+9vPOeg1CDcmR+ncqmrYrhRl3nmyDttyDahyjCzAw=="], - "@ai-sdk/deepgram": ["@ai-sdk/deepgram@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.29" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-awBjUhosRS9qfo5IHP1S+JMiVortq59mtlceVO4cQZ0ijbdwjv1z598umeXxggyjpgo4awFivU4uTzCtVQ+cAA=="], + "@ai-sdk/deepgram": ["@ai-sdk/deepgram@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.41" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3xjRxIjZKlZbJNP+6gl5xx5Msf8Kfr0U/bIEpQ57oeZBPj+RQfViwJr0HrfMdFQNXR3VK+MUTTM4VciaAzjr7w=="], "@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-y6RoOP7DGWmDSiSxrUSt5p18sbz+Ixe5lMVPmdE7x+Tr5rlrzvftyHhjWHfqlAtoYERZTGFbP6tPW1OfQcrb4A=="], - "@ai-sdk/deepseek": ["@ai-sdk/deepseek@2.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.29" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-IkBQPEfRUbPEcWzigT76KttqoDqM4FR8H8cp5pgAw5RdDN++nUoJa0uZan3LuOp4IC8BKGZABJCqB97CMIYLdQ=="], + "@ai-sdk/deepseek": ["@ai-sdk/deepseek@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.40" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-pnl22aGBIdpK0sXSJN4zDykIXTdyO9m3gFhno3SBPpdwT4Tq041Uy0i/EDWSxIiFa9JEokFGiCt4NNzcE5ntRA=="], - "@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.29" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-KjgM2IDMwm1qTKaW2n2qCr2ok9/wlhUC9ggNvyYYeCupEEGR8Gki1oFRA16PsBcyEr98k+kaKw4cF6D0XaVWWA=="], + "@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.41" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-dhLVS5JzIdj2shss7CiZOcYC1YBDnd8dDgQBumh3CltGJ7fTO4QirS3hYLU8fkbeqiROU5Oh7E7f95kdtTagCg=="], - "@ai-sdk/fireworks": ["@ai-sdk/fireworks@2.0.55", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.50", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.29" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cdlFOq4NuwZdgdwocae5ItT7uKh3mf4junJWGVKtOcs7ATY42VZLzIU2MFBgm2imGVZXwB6kcl3jd9sDBnJ8cw=="], + "@ai-sdk/fireworks": ["@ai-sdk/fireworks@2.0.72", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.63", "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.41" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-T9TdW3BOFaomog3a42UxG8F9S8dnn9UH4kfd/aM65x4uW3SW/IgOr5EGKnYikJcMwmSz7mWXZVGfh5dmHAxX9A=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.157", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.40", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jO0LUsIQC+FeJRQI1KFiNcfTGp6a0lXrymekaXfBIvhVEOyt0NUyc9wiVi/dYGHQmWFXUo8QgmghgGAGLOPTAA=="], - "@ai-sdk/google": ["@ai-sdk/google@3.0.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RfOZWVMYSPu2sPRfGajrauWAZ9BSaRopSn+AszkKWQ1MFj8nhaXvCqRHB5pBQUaHTfZKagvOmMpNfa/s3gPLgQ=="], + "@ai-sdk/google": ["@ai-sdk/google@3.0.73", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-o2MuIeyvZrFIeIbnbA8Thrr63irdyUBh0uWBZ2lY6yFeXuE/tcwyXF74bDKS4KvTu84uFpQfpbS/LXHGKKXz+g=="], - "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.112", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/google": "3.0.64", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cSfHCkM+9ZrFtQWIN1WlV93JPD+isGSdFxKj7u1L9m2aLVZajlXdcE41GL9hMt7ld7bZYE4NnZ+4VLxBAHE+Eg=="], + "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.128", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.77", "@ai-sdk/google": "3.0.73", "@ai-sdk/openai-compatible": "2.0.47", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jK8fixb4km2yfgvb9DUFQRpV/jiDB0v9gyxHoHfPydaQvz+CpAz8DTt1quyaM+Wg9G2R8Zo68CYmHbIkUqW2AA=="], "@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.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/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/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-sdk/openai": ["@ai-sdk/openai@3.0.88", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.40" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-6M4+bxK/UMijDYK2ia1hCKYma1iajFYxHDlhbinFUdhH2WbFsjoZRMpSjEMlTOBptqmh2J1JclKnprGulF8WwQ=="], "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z9MC6M4Oh/yUY/F/eszOtO8wc2nMz99XmZQKd2gWTtyIfe716xTfrKe3aYZKg20NZDtyjqPPKPSR+wqz7q1T7Q=="], "@ai-sdk/perplexity": ["@ai-sdk/perplexity@3.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-dXzrVsLR5f6tr+U04jq4AXoRroGFBTvODnLgss0SWbzNjGGQg3XqtQ9j7rCLo6o8qbYGuAHvqUrIpUCuiscuFg=="], - "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "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-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="], + "@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/togetherai": ["@ai-sdk/togetherai@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-k3p9e3k0/gpDDyTtvafsK4HYR4D/aUQW/kzCwWo1+CzdBU84i4L14gWISC/mv6tgSicMXHcEUd521fPufQwNlg=="], "@ai-sdk/vercel": ["@ai-sdk/vercel@2.0.39", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8eu3ljJpkCTP4ppcyYB+NcBrkcBoSOFthCSgk5VnjaxnDaOJFaxnPwfddM7wx3RwMk2CiK1O61Px/LlqNc7QkQ=="], - "@ai-sdk/xai": ["@ai-sdk/xai@3.0.92", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.48", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yqRMdEVfSkZCXs5mqeKfYOuQElo93igwz/4zWbYkhZWAW7tDh+5uqRef6MtQVYF/bTC1EsdbeO9DtsOtAZMOLA=="], + "@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=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], @@ -862,12 +1176,18 @@ "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], + "@anthropic-ai/sandbox-runtime": ["@anthropic-ai/sandbox-runtime@0.0.63", "", { "dependencies": { "@pondwader/socks5-server": "^1.0.10", "commander": "^12.1.0", "node-forge": "^1.4.0", "zod": "^3.24.1" }, "bin": { "srt": "dist/cli.js" } }, "sha512-rKxyYkfDczeDmPfZm6/cXigXwadwfVV7wBfmW0e5vfZER1tkDJXJ7rz7Yi0W5q+PFILG2kz/cAFpSOaQ76q7Jg=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.39.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg=="], "@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="], "@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=="], + + "@aws-crypto/sha1-browser": ["@aws-crypto/sha1-browser@5.2.0", "", { "dependencies": { "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg=="], + "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], @@ -878,63 +1198,85 @@ "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1005.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.19", "@aws-sdk/credential-provider-node": "^3.972.19", "@aws-sdk/eventstream-handler-node": "^3.972.10", "@aws-sdk/middleware-eventstream": "^3.972.7", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.20", "@aws-sdk/middleware-websocket": "^3.972.12", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/token-providers": "3.1005.0", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.9", "@smithy/eventstream-serde-browser": "^4.2.11", "@smithy/eventstream-serde-config-resolver": "^4.3.11", "@smithy/eventstream-serde-node": "^4.2.11", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.23", "@smithy/middleware-retry": "^4.4.40", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.39", "@smithy/util-defaults-mode-node": "^4.2.42", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-IV5vZ6H46ZNsTxsFWkbrJkg+sPe6+3m90k7EejgB/AFCb/YQuseH0+I3B57ew+zoOaXJU71KDPBwsIiMSsikVg=="], - "@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.1025.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/credential-provider-node": "^3.972.29", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.14", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-retry": "^4.4.46", "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.44", "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-ke7vyS7Dmo3St1a354AlpAjocZpG25Ql52XQ1AXRD3VQ791FBT7vX+EqGCfAEIWLPxpcldaW2Nny6P6klEOkkw=="], + "@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.1057.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-node": "^3.972.47", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-5MliYkp2u0+2arTp5fZIaxl+xmm90LEKv/VeSxhfNQW4t0fvWJrNO429/jchWQenNoDRrOGE59VfbuZUfwFujg=="], + + "@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.1104.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/credential-provider-node": "^3.972.78", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-lfUs0rHQl2eeo04+SbvaeS2XkqJnJggbJTKQlYP/GTQdMvZtGfQULcpWdaQvrfgTOgW6DKRpYR3UMJdJRyaooQ=="], - "@aws-sdk/core": ["@aws-sdk/core@3.974.20", "", { "dependencies": { "@aws-sdk/types": "^3.973.12", "@aws-sdk/xml-builder": "^3.972.29", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.6", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7sDi2B2N3mc3nf1nz6FyEx/FCrJ1N1QnBmraHHQNabFaeAh2IaOOLml48/rHOD1bICHgTRkbBgNTvUzEr5Z35g=="], + "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.933.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-bucket-endpoint": "3.930.0", "@aws-sdk/middleware-expect-continue": "3.930.0", "@aws-sdk/middleware-flexible-checksums": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-location-constraint": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-sdk-s3": "3.932.0", "@aws-sdk/middleware-ssec": "3.930.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/signature-v4-multi-region": "3.932.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-blob-browser": "^4.2.6", "@smithy/hash-node": "^4.2.5", "@smithy/hash-stream-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/md5-js": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-KxwZvdxdCeWK6o8mpnb+kk7Kgb8V+8AjTwSXUWH1UAD85B0tjdo1cSfE5zoR5fWGol4Ml5RLez12a6LPhsoTqA=="], - "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.45", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-4L/REssieLON1hVsTzZucP1p2u5jmPNejyh/9BCGZXr93IalBbhRPCrtKIKwMTu9yRGr/bcKzhrQocByKLSzLQ=="], + "@aws-sdk/client-sso": ["@aws-sdk/client-sso@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-zwGLSiK48z3PzKpQiDMKP85+fpIrPMF1qQOQW9OW7BGj5AuBZIisT2O4VzIgYJeh+t47MLU7VgBQL7muc+MJDg=="], - "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.46", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-+GPXVS2srMOlH74S+SmC1gVuP2TvUZ0siuC0onKO93q+udP+M72dmY8wJfVQ5CX9z/9X5A1HHwz5yRIGBtskvQ=="], + "@aws-sdk/core": ["@aws-sdk/core@3.932.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@aws-sdk/xml-builder": "3.930.0", "@smithy/core": "^3.18.2", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-AS8gypYQCbNojwgjvZGkJocC2CoEICDx9ZJ15ILsv+MlcCVLtUJSRSx3VzJOUY2EEIaGLRrPNlIqyn/9/fySvA=="], - "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.48", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-fA5loSdlocacRxyUXtpoHSMuk5rsIKRDzQYVMnMxjcmFeZshaJlJ8lymy/hYKji6sne/UmNGj5pxuEs6kq/Qcg=="], + "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.66", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-i4HTkP21eHaXA+XKoc6dYxlKPvYUqbusGGIsFRcSGTF8ln/q6RrWoRdVJ6UVODTCW59Ot1mVcbJXRAEf6kxa3w=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.67", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-rcIpk5kxUqDaaNa6Xk23pQ6ViY7jlqzmfFWCahQcBT97ddXaXYYwzCen9Tz1Jvo6aJft6wDl5bN44/Jw5B4oLA=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.69", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-nggwJtZ4eeNsUw5IeWBMXsi1ryct5idi0K+/SCRF3kybLubOMaNTb3XCihXpWMiVpyzyPeIrl0zTkzhBH9porA=="], "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/credential-provider-env": "^3.972.27", "@aws-sdk/credential-provider-http": "^3.972.29", "@aws-sdk/credential-provider-login": "^3.972.31", "@aws-sdk/credential-provider-process": "^3.972.27", "@aws-sdk/credential-provider-sso": "^3.972.31", "@aws-sdk/credential-provider-web-identity": "^3.972.31", "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-PuQ7e8WYzAPpzvFcajxf8c0LqSzakVHVlKw8M0oubk8Kf347YOCCqT1seQrHs5AdZuIh2RD9LX4O+Xa5ImEBfQ=="], - "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.52", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/nested-clients": "^3.997.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-9hu2oR0qH7Fst5Tzdx+UWxm+w5zCXtErTLtOOW5hwwQc170CLwOeniRxyFY6s9mHfGEfC5zFukNBdKBwJR8mhQ=="], + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.74", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-0AQfDcf99TNmqVKv0owHrw/TQs6i4ZE5t9qmz6NvO53bE/sA/tpXhXL9AAcEP1qHc6Zzjd1UMb69+/9zdhvY3g=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.933.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.932.0", "@aws-sdk/credential-provider-http": "3.932.0", "@aws-sdk/credential-provider-ini": "3.933.0", "@aws-sdk/credential-provider-process": "3.932.0", "@aws-sdk/credential-provider-sso": "3.933.0", "@aws-sdk/credential-provider-web-identity": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-L2dE0Y7iMLammQewPKNeEh1z/fdJyYEU+/QsLBD9VEh+SXcN/FIyTi21Isw8wPZN6lMB9PDVtISzBnF8HuSFrw=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.67", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-IlUEejorGTWKb4/Dm7K5Yw4QxUmXLThLhrvBmzVBqZFTbW72cv9LTcITmo1dsnYriALE4h68mOq4LB99x6sQ7Q=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.11", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/token-providers": "3.1103.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-gAQBkBZxUB84d71+pPcI9L+jh2ujhuAVxc/4FgGiWFDjkPBlMKxzd5XDtkSXTFX8Ro7ansnT88+XadasxMeCRw=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.73", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-SnlEmQa6SjOgs6iOPLUQl1Eyq4AKiAdPQlkOhFhqNfDtDCwibMGvL6QlkSmf3o6vAUSImzdPCxowT5dfQUZP1A=="], - "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.55", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.46", "@aws-sdk/credential-provider-http": "^3.972.48", "@aws-sdk/credential-provider-ini": "^3.972.53", "@aws-sdk/credential-provider-process": "^3.972.46", "@aws-sdk/credential-provider-sso": "^3.972.52", "@aws-sdk/credential-provider-web-identity": "^3.972.52", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/credential-provider-imds": "^4.3.7", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-zMGLa/dhESVqmCD7mmIFFKSwSFrJGScvCXcjvBZEVOOMauFS5JRQvLTMukFpMEFWiV6dTAlsen2ATDBulLPtbg=="], + "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1057.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.1057.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-cognito-identity": "^3.972.38", "@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-login": "^3.972.45", "@aws-sdk/credential-provider-node": "^3.972.47", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-rbrEHtz11g0kxsSkYr3fx2HABNNblp4AhB2MgPvJHgYOWfJ2eBviU7Mvoaef0PW8QH6lbZDfJcnM7eKvtvz3sw=="], - "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.46", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-VUoNFBIjWrUN8NbFiQiuxQEgFjvziAlBRPK+ddh27aj65gk0BYu6bLZnrdrNZwpW6vAihtSUtEMQ1PUJ32QRPA=="], + "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.31", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-/BRzvkp46mF6eXBL/l9WKPQQfifLlUPaWli6n9/T/WDLUg8he7TCyuNFnk6RvHP5j5W/kMj5Gxw7W778LJaXDA=="], - "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.52", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/nested-clients": "^3.997.20", "@aws-sdk/token-providers": "3.1066.0", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-nb2/n4o/HQf+FVpVbZe9vCTFngmuDoIsltMgLAtjixaKzvzhB4J8WSDFyWgnErgLHk55ctWH+I4PU+LIHhyffg=="], + "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@aws-sdk/util-arn-parser": "3.893.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-cnCLWeKPYgvV4yRYPFH6pWMdUByvu2cy2BAlfsPpvnm4RaVioztyvxmQj5PmVN5fvWs5w/2d6U7le8X9iye2sA=="], - "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.52", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/nested-clients": "^3.997.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-lKj6aRSGbqLmpYmM24bY7a1Xmfcq2vkE3hv8CSPYfc1yCu0BPu/XEJ1L4Fm61MsU6ULLNSG8UGsffNoFUBjESA=="], + "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.26", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-2eIvouTZoxPu5ClHY6ij13De1yhY8Rmllt0dlGeBNXX3wmR7fU1pvMCGb50fKm1GxcuntWK0t1T0cMjTyDoUQA=="], - "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1025.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.1025.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/credential-provider-cognito-identity": "^3.972.21", "@aws-sdk/credential-provider-env": "^3.972.24", "@aws-sdk/credential-provider-http": "^3.972.26", "@aws-sdk/credential-provider-ini": "^3.972.28", "@aws-sdk/credential-provider-login": "^3.972.28", "@aws-sdk/credential-provider-node": "^3.972.29", "@aws-sdk/credential-provider-process": "^3.972.24", "@aws-sdk/credential-provider-sso": "^3.972.28", "@aws-sdk/credential-provider-web-identity": "^3.972.28", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/types": "^3.973.6", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-hOMHzYetTwnpvfbLN8emaw+nnQrqlEV0I5rTrgRKTAx1anzEvls/rD1IXwOvX8Z+B9mgbK+yNFqO3wQkBghI1g=="], + "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-5HEQ+JU4DrLNWeY27wKg/jeVa8Suy62ivJHOSUf6e6hZdVIMx0h/kXS1fHEQNNiLu2IzSEP/bFXsKBaW7x7s0g=="], - "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.21", "", { "dependencies": { "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-mVC0hOmwGJmNFezZ+wM8Sqfap/LjsMavEf2Evl0YWrLAcrdZOEdjnY8nRvgakVViWJSGm2eJxLuPVHGdeV06kA=="], + "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.932.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-hyvRz/XS/0HTHp9/Ld1mKwpOi7bZu5olI42+T112rkCTbt1bewkygzEl4oflY4H7cKMamQusYoL0yBUD/QSEvA=="], - "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.17", "", { "dependencies": { "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-tdbnXbw73ww62ABWP0G0Z/euvFowEEvAoi/zG4NaZo7HJFpfGho/Z65HyVzkJLT1cMsUregr4pTyxljlarT0wA=="], + "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-x30jmm3TLu7b/b+67nMyoV0NlbnCVT5DI57yDrhXAPCtdgM1KtdLWt45UcHpKOm1JsaIkmYRh2WYu7Anx4MG0g=="], - "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.21", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-+SRZL54/T9Ryxa/NmHM7WZAliU6wnfzeosT/+4IVuTgq0zSCpPx2j3yaEP4JFZlvWvoOCbKgr+4tBqSAG/dl5Q=="], + "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-QIGNsNUdRICog+LYqmtJ03PLze6h2KCORXUs5td/hAEjVP5DMmubhtrGg1KhWyctACluUH/E/yrD14p4pRXxwA=="], - "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.20", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-8oOvo7XNDeOlwa5ys2Q0UTLCKmtvRiqf8rOU63lKniPmP3dnI5lnoy9dteZ79lxb9hmXCrO28aZMqds6C5AEoA=="], + "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-vh4JBWzMCBW8wREvAwoSqB2geKsZwSHTa0nSt0OMOLp2PdTYIZDi0ZiVMmpfnjcx9XbS6aSluLv9sKx4RrG46A=="], - "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.22", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-q4H/CoOYrTbyAW0d9RrHf9kTYKVXpAwoK0VEy3UT2Asad+6aa6vzQgz35dh20tRA7zlEo/Nsyjy9PVlHgdq0Vg=="], + "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.933.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@aws/lambda-invoke-store": "^0.2.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-qgrMlkVKzTCAdNw2A05DC2sPBo0KRQ7wk+lbYSRJnWVzcrceJhnmhoZVV5PFv7JtchK7sHVcfm9lcpiyd+XaCA=="], - "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.50", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-5JIDcDFWy3DUW95sOlXLbeb7RkGFUcNh1QidKsznqtlm5YGsXP0EGWaqzxBTvVmOhqKs2RmNmI6w9V/5dS3CLQ=="], + "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-arn-parser": "3.893.0", "@smithy/core": "^3.18.2", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-bYMHxqQzseaAP9Z5qLI918z5AtbAnZRRtFi3POb4FLZyreBMgCgBNaPkIhdgywnkqaydTWvbMBX4s9f4gUwlTw=="], - "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-SCW06Zjugn86pq7+dxGnFcyWJuEWHT753HTU/Vj/OzVxP+NoShwdAr4ynxAcvWL883OgRVbSqW3ohnjIxwXjjw=="], + "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-N2/SvodmaDS6h7CWfuapt3oJyn1T2CBz0CsDIiTDv9cSagXAVFjPdm2g4PFJqrNBeqdDIoYBnnta336HmamWHg=="], - "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.20", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.20", "@aws-sdk/signature-v4-multi-region": "^3.996.34", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-IYJuLpXp2DEILVQpQOy0PMpkftv0AHEOCn52o0atyOaumA0CdWQ3klPyXdViGYLbNpESsVFMVybvHUeZAuiGxA=="], + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@smithy/core": "^3.18.2", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-9BGTbJyA/4PTdwQWE9hAFIJGpsYkyEW20WON3i15aDqo5oRZwZmqaVageOD57YYqG8JDJjvcwKyDdR4cc38dvg=="], - "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.24", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-WY6uVMsq0EvxY4BcYhZmG2Ivd1EzNvZAqsXFlL3pTPMG0P4J83TYVQIs8P0nd5lc+Bp3llrYwggruvXzrfUtsQ=="], + "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.49", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-wGYJvu1/stdWuzGcNyh44u8aY7ArU8XFrMesBlzIYn70HWVCRBNEngQexDuPIMu0NEgsKGKmTc0uvnS/bF7KWw=="], - "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.34", "", { "dependencies": { "@aws-sdk/types": "^3.973.12", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-mx1L5qlumSOt/nKM3BFaHE2HVkWwz0i4Bw0pyYO42FfX/FeLlo8YI6csC0gSPprEk6fTIqI+CZN9RwUwKd5krQ=="], + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.41", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RDHqPGQWlF6tatA/Tp3rg6oIwtgN9IVderxE+9av2Y93Dfyu+mO1hZ5Bu2jpfZg2rwdNbsssnwM+sLafIczMlQ=="], + + "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/config-resolver": "^4.4.3", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-KL2JZqH6aYeQssu1g1KuWsReupdfOoxD6f1as2VC+rdwYFUu4LfzMsFfXnBvvQWWqQ7rZHWOw1T+o5gJmg7Dzw=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.932.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-NCIRJvoRc9246RZHIusY1+n/neeG2yGhBGdKhghmrNdM+mLLN6Ii7CKFZjx3DhxtpHMpl1HWLTMhdVrGwP2upw=="], "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1005.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/nested-clients": "^3.996.8", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-vMxd+ivKqSxU9bHx5vmAlFKDAkjGotFU56IOkDa5DaTu1WWwbcse0yFHEm9I537oVvodaiwMl3VBwgHfzQ2rvw=="], - "@aws-sdk/types": ["@aws-sdk/types@3.973.12", "", { "dependencies": { "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-43ajd1NF0RMgX5k0hxCNUyEdrtFUsb2aHT2QvpktSC/2Eyb2Jr/JPVqdp0XIoaHWikZJq5tNWSLO6kB5q2eMCA=="], + "@aws-sdk/types": ["@aws-sdk/types@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-we/vaAgwlEFW7IeftmCLlLMw+6hFs3DzZPJw7lVHbj/5HJ0bz9gndxEsS2lQoeJ1zhiiLqAqvXxmM43s0MBg0A=="], + + "@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.893.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA=="], - "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.19", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-W79LutZYmCV1WGe0UVGALMna3xLGP3wv2zLzrBEgc73CtzxKBxb5IpMedbS6Ej80LCknSgqFjsTtECG0IInckw=="], + "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-endpoints": "^3.2.5", "tslib": "^2.6.2" } }, "sha512-M2oEKBzzNAYr136RRc6uqw3aWlwCxqTP1Lawps9E1d2abRPvl1p1ztQmmXp1Ak4rv8eByIZ+yQyKQ3zPdRG5dw=="], - "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.7", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-M0D6oIpohdNHjc7udzTHEQyot0+0iuA36jc2I9Hps+f/GtKi2HO/pyijQnCnNcwZqLB5+rtn81z3eZK/GyjAmA=="], + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.8", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g=="], - "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.21", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-Y9MPH4VZaIebwxiVK6dMzSt5L04oyNiK3NNwFe4qP5B2Hfo+pmEVpSJSa+gARPtcJeRyehUMPu5/I9DLdW0cBg=="], + "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/types": "^4.9.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-q6lCRm6UAe+e1LguM5E4EqM9brQlDem4XDcQ87NzEvlTW6GzmNCO0w1jS0XgCFXQHjDxjdlNFX+5sRbHijwklg=="], - "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.36", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-wsmSmHTrK9lV+5SKBekp1J7W6PufdZE08X0xv5Lz1OdgYRmyuYDy/++SslKdXhcVQjxLtzMTZaLqqLZmTx6OaQ=="], + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.932.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-/kC6cscHrZL74TrZtgiIL5jJNbVsw9duGGPurmaVgoCbP7NnxyaSWEurbNV3VPNPhNE3bV3g4Ci+odq+AlsYQg=="], - "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.29", "", { "dependencies": { "@smithy/types": "^4.14.3", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-fk0niuGFxfi8yIJuMVM4mhwObkiQSuwZFj3tAPrLVx64Pk3BkrEIpqjzHKY4hKoEBUD6Jg/S74Zj9jy+5F3DnQ=="], + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA=="], + + "@aws/durable-execution-sdk-js": ["@aws/durable-execution-sdk-js@1.0.2", "", { "dependencies": { "@aws-sdk/client-lambda": "^3.943.0" } }, "sha512-KIYBVqV9gArkWdPEDYOjJqLlO+NecmCXOXadNBlOspJTmqztwuiyb6aZc468bYWSGuL4Me/gyTIK97ubkwFNCw=="], "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], @@ -944,35 +1286,47 @@ "@azu/style-format": ["@azu/style-format@1.0.1", "", { "dependencies": { "@azu/format-text": "^1.0.1" } }, "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g=="], - "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], + "@azure/abort-controller": ["@azure/abort-controller@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg=="], + + "@azure/core-auth": ["@azure/core-auth@1.11.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg=="], - "@azure/core-auth": ["@azure/core-auth@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg=="], + "@azure/core-client": ["@azure/core-client@1.11.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-JjQWO6akOck45PH/XBrxzsQGAiKrfFl4m5iggJ0ItMIz5omRufOXWpqCPpdjKN3vKDzlSUvFjaMb7Zwf0gvAdA=="], - "@azure/core-client": ["@azure/core-client@1.10.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-1D2LpsU7y9xrqKjdIbsB7PlrRePw0xsVV8p+AKTlzITrWmscajryfJCdDJB/oGwvDI5HmRo04eMMADB67uwAwQ=="], + "@azure/core-http-compat": ["@azure/core-http-compat@2.5.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2" }, "peerDependencies": { "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-BoSmXPx2er1Ai+wKlDvj29jIQespCNBwEmKyZVHO2kEFsWbGjAjwMCGzug3DJM5/QYIV3vej0S1zcU5bq9fa8w=="], - "@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.24.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.4", "tslib": "^2.6.2" } }, "sha512-PpLsoDQ3AMmKZ0VU+0GrmqMxgp/sExjlVm4R+nLWngeoEGAzOIPVifaxKGU5gMv+nWELUoHfvrolWD+ZS/nFJg=="], + "@azure/core-lro": ["@azure/core-lro@2.7.2", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw=="], - "@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="], + "@azure/core-paging": ["@azure/core-paging@1.7.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-7GEAoIsaoBr6KELNRb8nypowCqvk8dnCHFCYg4XD4lOQGY2GqjQg5IhkRjyBFRO18CGSMq05PaNqSOE9GQro3g=="], - "@azure/core-util": ["@azure/core-util@1.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A=="], + "@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.25.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.4", "tslib": "^2.6.2" } }, "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA=="], + + "@azure/core-tracing": ["@azure/core-tracing@1.4.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ=="], + + "@azure/core-util": ["@azure/core-util@1.14.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw=="], + + "@azure/core-xml": ["@azure/core-xml@1.6.0", "", { "dependencies": { "fast-xml-parser": "^5.5.9", "tslib": "^2.8.1" } }, "sha512-e7lX/dk//F6Qf7BB6PTY4+p2yuOQtyOeHGyapYHNwqSp2OnYpwQt49A/Nin2XmKBQ69pwagR4k/lQBq8lbHQkA=="], "@azure/identity": ["@azure/identity@4.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^5.5.0", "@azure/msal-node": "^5.1.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw=="], - "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], + "@azure/logger": ["@azure/logger@1.4.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA=="], - "@azure/msal-browser": ["@azure/msal-browser@5.13.0", "", { "dependencies": { "@azure/msal-common": "16.8.0" } }, "sha512-Ea23x0U8XNFY+qJ9T44zO2BbY+AHdb+WdjmYnx36OhJ/KO+PGU5pmsNHf1DCElYX+6wyVRJz1HFeCPC/cHbRug=="], + "@azure/msal-browser": ["@azure/msal-browser@5.18.0", "", { "dependencies": { "@azure/msal-common": "16.12.0" } }, "sha512-SPTeHYZghdEdRddJzNjhH+CI5MSQtquNYwGJnYXfOHIBRXCmrWimBS85OhwXpXFIlrCtNTbBPm5mPAWRNEoktA=="], - "@azure/msal-common": ["@azure/msal-common@16.8.0", "", {}, "sha512-5S4RHOcInL2Nu2U217tDZbWGI6StMfcWCrA7TWvWdJmXQ+cYrrIqr84AsN62fGh2MDBysiBJPt6CfWceJfloEA=="], + "@azure/msal-common": ["@azure/msal-common@16.12.0", "", {}, "sha512-hgLgfRdbG2AmhXPygebf1KYJEvse86+ZZLWufdiTKaGRYEUqOzHdlf6AS1IiuUCHWbynkgbHc451jSNkbfhWlg=="], - "@azure/msal-node": ["@azure/msal-node@5.2.4", "", { "dependencies": { "@azure/msal-common": "16.8.0", "jsonwebtoken": "^9.0.0" } }, "sha512-rpBUg9dA8UpC2WiFt3KeDKVQmmmVrfxdRnW+F1ebgou/jX/0tAvYuonaq5RUo8OaqzOrj4x/HaI8DmY56RXZ2Q=="], + "@azure/msal-node": ["@azure/msal-node@5.5.0", "", { "dependencies": { "@azure/msal-common": "16.12.0", "jsonwebtoken": "^9.0.0" } }, "sha512-A/2WIsuH0vsC6JVkkafjS4kHpi2LDR4AzDT0kJ+oIRtXYeYtvGQ2pwN2X88thQPhSek+82ela3MprsKXWQRrhQ=="], + + "@azure/storage-blob": ["@azure/storage-blob@12.33.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.3", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/core-xml": "^1.4.5", "@azure/logger": "^1.1.4", "@azure/storage-common": "^12.4.1", "events": "^3.0.0", "tslib": "^2.8.1" } }, "sha512-2SX8oP8PyblUcAFZSg39c8Ls+tFjavM6sBeV+qpw33mRzRhI/5hrFJmJ/x0H9xx5l6ECPvgSP8uPxqTeVbHNIA=="], + + "@azure/storage-common": ["@azure/storage-common@12.5.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.4.0", "@azure/core-rest-pipeline": "^1.24.0", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.1.4", "events": "^3.3.0", "tslib": "^2.8.1" } }, "sha512-bttzuhQiCIwrkzjPDA+AtAR7dg19L/CC6ztcqJ5LfvWpXuys9mHp0UQ0udYnoUvv9SCT9KTR5kqFvFr0e6k0lQ=="], "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], - "@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], + "@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], - "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], @@ -1004,7 +1358,7 @@ "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], - "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + "@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], @@ -1014,20 +1368,24 @@ "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="], - "@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="], + "@babel/preset-typescript": ["@babel/preset-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ=="], "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], + "@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], - "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], + "@bufbuild/protobuf": ["@bufbuild/protobuf@2.13.0", "", {}, "sha512-acq7c49vxfm1ggJ95P70TX7ABDM0vxr1SYD3BB0o0jnBLB4OAqeHyKuN+cD3w80gXEDQ2zxHpR6CUeA+O/aU9g=="], + + "@bufbuild/protoplugin": ["@bufbuild/protoplugin@2.13.0", "", { "dependencies": { "@bufbuild/protobuf": "2.13.0", "@typescript/vfs": "^1.6.2", "typescript": "5.4.5" } }, "sha512-32eMChKaL/A8Hh5AfMmXSdnuyznN85uoEjoyWiWeRrvtQOtpqX/v1R9PDe0g9vMIgzznK9inMT3CUaal0kjLUQ=="], + "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.1.1", "", { "dependencies": { "@changesets/config": "^3.1.4", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA=="], "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.10", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A=="], @@ -1036,7 +1394,7 @@ "@changesets/changelog-github": ["@changesets/changelog-github@0.5.2", "", { "dependencies": { "@changesets/get-github-info": "^0.7.0", "@changesets/types": "^6.1.0", "dotenv": "^8.1.0" } }, "sha512-HeGeDl8HaIGj9fQHo/tv5XKQ2SNEi9+9yl1Bss1jttPqeiASRXhfi0A2wv8yFKCp07kR1gpOI5ge6+CWNm1jPw=="], - "@changesets/cli": ["@changesets/cli@2.31.0", "", { "dependencies": { "@changesets/apply-release-plan": "^7.1.1", "@changesets/assemble-release-plan": "^6.0.10", "@changesets/changelog-git": "^0.2.1", "@changesets/config": "^3.1.4", "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/get-release-plan": "^4.0.16", "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@changesets/write": "^0.4.0", "@inquirer/external-editor": "^1.0.2", "@manypkg/get-packages": "^1.1.3", "ansi-colors": "^4.1.3", "enquirer": "^2.4.1", "fs-extra": "^7.0.1", "mri": "^1.2.0", "package-manager-detector": "^0.2.0", "picocolors": "^1.1.0", "resolve-from": "^5.0.0", "semver": "^7.5.3", "spawndamnit": "^3.0.1", "term-size": "^2.1.0" }, "bin": { "changeset": "bin.js" } }, "sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg=="], + "@changesets/cli": ["@changesets/cli@2.31.1", "", { "dependencies": { "@changesets/apply-release-plan": "^7.1.1", "@changesets/assemble-release-plan": "^6.0.10", "@changesets/changelog-git": "^0.2.1", "@changesets/config": "^3.1.4", "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/get-release-plan": "^4.0.16", "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@changesets/write": "^0.4.0", "@inquirer/external-editor": "^1.0.2", "@manypkg/get-packages": "^1.1.3", "ansi-colors": "^4.1.3", "enquirer": "^2.4.1", "fs-extra": "^7.0.1", "mri": "^1.2.0", "package-manager-detector": "^0.2.0", "picocolors": "^1.1.0", "resolve-from": "^5.0.0", "semver": "^7.5.3", "spawndamnit": "^3.0.1", "term-size": "^2.1.0" }, "bin": { "changeset": "bin.js" } }, "sha512-uO05WTcRBwuVOJVSW8Cmpqw6q0WDL53ajGCMyszutvOe5toOnunbpM4jZzf+qxBOz7i0AzopZ8diBuewjmF40w=="], "@changesets/config": ["@changesets/config@3.1.4", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/logger": "^0.1.1", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", "micromatch": "^4.0.8" } }, "sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q=="], @@ -1074,25 +1432,25 @@ "@corvu/utils": ["@corvu/utils@0.4.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.11" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA=="], - "@docsearch/css": ["@docsearch/css@4.6.3", "", {}, "sha512-nlOwcXcsNAptQl4vlL4MA78qNJKO0Qlds5GuBjCoePgkebTXLSf8Qt1oyZ3YBshYupKXG9VRGEsk1zr23d+bzQ=="], + "@docsearch/css": ["@docsearch/css@4.7.0", "", {}, "sha512-Sk5xkdRFeE7PeWjG9l4AfTwdvMfr9wHiwNNCpHXT4v4SNyNMKdHGvEILc31BgaVFGDDNbv5u/a73tofRiwbEZw=="], - "@docsearch/js": ["@docsearch/js@4.6.3", "", {}, "sha512-qUIX2b4Apew3tv4F0qhmgShsl/Lfw4m6mqv/5/5dWNxwTcDdLMp2s3YwZ+NMGh3IKCg0pBaXm7Q5VdyU5Rj+cQ=="], + "@docsearch/js": ["@docsearch/js@4.7.0", "", {}, "sha512-x5lCqu1tetgsJFkjQ6VSocbHldsRkGEgwg5N98Vx21sq/V5wcmj4u226PY9k+TEpIgQ772zlYbPLTPicWyGnpA=="], "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], "@effect/language-service": ["@effect/language-service@0.84.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-l04qNxpiA8rY5yXWckRPJ7Mk5MNerXuNymSFf+IdflfI5i8jgL1bpBNLuP6ijg7wgjdHc/KmTnCj2kT0SCntuA=="], - "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.66", "", { "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.66" }, "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-LU3ejAzJS+4P+Qtfn9ULnsGcIPmx1tUUB2ZswFRL+EolD8US7zMljHTwGuQRUBJOjDwt7wFCMN5AR512vdY8FQ=="], + "@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/platform-node": ["@effect/platform-node@4.0.0-beta.66", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.66", "mime": "^4.1.0", "undici": "^8.0.2" }, "peerDependencies": { "effect": "^4.0.0-beta.66", "ioredis": "^5.7.0" } }, "sha512-s/0RgaQFuszzdorRnX1PwEQNnSOi+JgMJo3zEe9O2NR3sosMhTr0Uk+1AF6bUOI9uJ2CPT3KpTIIU7q5/TpOkg=="], + "@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-shared": ["@effect/platform-node-shared@4.0.0-beta.46", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.46" } }, "sha512-Yzci82XbZ1W3tuiownsJawrJZTGeTrTZKLD0uxdBWCBzlVyqDwoSwRwO5qh33DurJj9B7iS8MDf14fpGRBPNGQ=="], + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.74", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.74" } }, "sha512-C6C2hXixNcZXLaFF2u7B/FtOsqpdY7luaPuiGFBJza0P7EnYDkwaT3kB6lv7l/qctmkADc24qOsSCWIKRbC4jg=="], - "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.66", "", { "peerDependencies": { "effect": "^4.0.0-beta.66" } }, "sha512-UYsrAb/5T0ZRypeN9Kmv3/ZInibGCjM6dtoiAWtfG+xKyuq8N05wmuVCXB0+XgVmUBxDWjw/S1fu4ivS0vZVuw=="], + "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.83", "", { "peerDependencies": { "effect": "^4.0.0-beta.83" } }, "sha512-6OaxLsWffxkh9pXYUSyj/AxjVb9URY2rG9U6atjxClWy30Jx77R9Pm3Rrc7cQ63kQurePavEw1bQbzQ/SILiQQ=="], "@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/runtime": ["@emnapi/runtime@1.11.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg=="], + "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], @@ -1148,7 +1506,7 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], @@ -1158,9 +1516,9 @@ "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.6", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA=="], - "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], + "@eslint/js": ["@eslint/js@9.39.5", "", {}, "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], @@ -1172,9 +1530,9 @@ "@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="], - "@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.0.3", "", { "dependencies": { "fast-json-stringify": "^6.0.0" } }, "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ=="], + "@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.1.0", "", { "dependencies": { "fast-json-stringify": "^7.0.0" } }, "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw=="], - "@fastify/forwarded": ["@fastify/forwarded@3.0.1", "", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="], + "@fastify/forwarded": ["@fastify/forwarded@3.0.2", "", {}, "sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA=="], "@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="], @@ -1182,11 +1540,29 @@ "@fastify/rate-limit": ["@fastify/rate-limit@10.3.0", "", { "dependencies": { "@lukeed/ms": "^2.0.2", "fastify-plugin": "^5.0.0", "toad-cache": "^3.7.0" } }, "sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q=="], - "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], + "@ff-labs/fff-bin-darwin-arm64": ["@ff-labs/fff-bin-darwin-arm64@0.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xyivu2xB++O5xXDx5Qm50JsU2aXt8YgXlGVhH/HE7UMYDrE6L6f1RYdYs8Y0bn0D3D0+bFBrN5ELPszK9E4Wbw=="], + + "@ff-labs/fff-bin-darwin-x64": ["@ff-labs/fff-bin-darwin-x64@0.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xLooAhCnTDCipPSMMZz7kGF3lhRHx6aP5fb6DJ0Ipyw/w/UWJb+xITJFszUl/QnIBoJ/qjDc93/FZMo1dk6gVA=="], + + "@ff-labs/fff-bin-linux-arm64-gnu": ["@ff-labs/fff-bin-linux-arm64-gnu@0.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-m5+8vA+1veaUUWonwva1WsU6m1HRm8CpYUzr06KDB65mewlmPbqz7+Fh7hjEfiD8C4mHVHe6RysULvAH1yhsdw=="], + + "@ff-labs/fff-bin-linux-arm64-musl": ["@ff-labs/fff-bin-linux-arm64-musl@0.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-EMeWm7CSTVkizy4ZEzUkLDP024tVcbCUthduuIhekFQRDsiaAze0YboIylWb9HBHJCZlCCoZrWAl4nnJbsX7AA=="], + + "@ff-labs/fff-bin-linux-x64-gnu": ["@ff-labs/fff-bin-linux-x64-gnu@0.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-pglE0uLkhnlE6bStXqfgUjYTSj+2sVwXaPfoA0QksidAsQor6NRt8004mygzC9DPubgHq5B9QezPfEwigKaP9Q=="], + + "@ff-labs/fff-bin-linux-x64-musl": ["@ff-labs/fff-bin-linux-x64-musl@0.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-VNKxgl8qs3aTfXViX7lqRK1aLu311h8dtBFqG4Scv+9Oi7WprybUp5L7IZ8sxKERaDAaiJMXHodXa1c90QdK8w=="], - "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], + "@ff-labs/fff-bin-win32-arm64": ["@ff-labs/fff-bin-win32-arm64@0.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-uFEt0aNL54vQxq1ivjxRuo+thnhS4wLqa4INl4VXnXJUmwB42XXxD+gsj7vzhBLLx4cFf0aWgy/+TVDR8yjZtQ=="], - "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + "@ff-labs/fff-bin-win32-x64": ["@ff-labs/fff-bin-win32-x64@0.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-Yd2Eyxj+slWv+0QDW9/xBpu9FXq+hwD0rXQD5184/88d+xwWCLKhEP2w8I6OO9XCg+kLT79UJb+k0WwXUtBtMw=="], + + "@ff-labs/fff-bun": ["@ff-labs/fff-bun@0.9.4", "", { "optionalDependencies": { "@ff-labs/fff-bin-darwin-arm64": "0.9.4", "@ff-labs/fff-bin-darwin-x64": "0.9.4", "@ff-labs/fff-bin-linux-arm64-gnu": "0.9.4", "@ff-labs/fff-bin-linux-arm64-musl": "0.9.4", "@ff-labs/fff-bin-linux-x64-gnu": "0.9.4", "@ff-labs/fff-bin-linux-x64-musl": "0.9.4", "@ff-labs/fff-bin-win32-arm64": "0.9.4", "@ff-labs/fff-bin-win32-x64": "0.9.4" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-7HUraaK/g5dStAnuKAuzsXVOQvqqX0ylo5G+DxYwsCjCDc42bjoEAAHqz/3Sn3raUNw97KMoz87XR9QyrLEfVw=="], + + "@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="], "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], @@ -1204,7 +1580,7 @@ "@hey-api/types": ["@hey-api/types@0.1.2", "", {}, "sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA=="], - "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], + "@hono/node-server": ["@hono/node-server@1.19.17", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ=="], "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], @@ -1218,66 +1594,74 @@ "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], - "@iconify/utils": ["@iconify/utils@3.1.3", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw=="], + "@iconify/utils": ["@iconify/utils@3.1.4", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw=="], "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], - "@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=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.2" }, "os": "darwin", "cpu": "arm64" }, "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.2" }, "os": "darwin", "cpu": "x64" }, "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w=="], + + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "os": "freebsd" }, "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg=="], - "@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=="], + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg=="], - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw=="], - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ=="], - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA=="], - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw=="], - "@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.3.2", "", { "os": "linux", "cpu": "none" }, "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w=="], - "@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.3.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ=="], - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w=="], - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw=="], - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ=="], - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.2" }, "os": "linux", "cpu": "arm" }, "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA=="], - "@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=="], + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ=="], - "@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=="], + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.2" }, "os": "linux", "cpu": "ppc64" }, "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA=="], - "@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.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.2" }, "os": "linux", "cpu": "none" }, "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ=="], - "@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.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.2" }, "os": "linux", "cpu": "s390x" }, "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw=="], - "@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=="], + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA=="], - "@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=="], + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w=="], - "@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=="], + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="], - "@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=="], + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="], - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "cpu": "none" }, "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q=="], - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w=="], - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw=="], - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="], "@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=="], - "@internationalized/date": ["@internationalized/date@3.12.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw=="], + "@internationalized/date": ["@internationalized/date@3.12.3", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q=="], "@internationalized/number": ["@internationalized/number@3.6.7", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg=="], "@ioredis/commands": ["@ioredis/commands@1.10.0", "", {}, "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q=="], + "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], + + "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="], + "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], @@ -1316,6 +1700,8 @@ "@kilocode/kilo-jetbrains": ["@kilocode/kilo-jetbrains@workspace:packages/kilo-jetbrains"], + "@kilocode/kilo-memory": ["@kilocode/kilo-memory@workspace:packages/kilo-memory"], + "@kilocode/kilo-telemetry": ["@kilocode/kilo-telemetry@workspace:packages/kilo-telemetry"], "@kilocode/kilo-ui": ["@kilocode/kilo-ui@workspace:packages/kilo-ui"], @@ -1332,7 +1718,9 @@ "@kobalte/core": ["@kobalte/core@0.13.11", "", { "dependencies": { "@floating-ui/dom": "^1.5.1", "@internationalized/date": "^3.4.0", "@internationalized/number": "^3.2.1", "@kobalte/utils": "^0.9.1", "@solid-primitives/props": "^3.1.8", "@solid-primitives/resize-observer": "^2.0.26", "solid-presence": "^0.1.8", "solid-prevent-scroll": "^0.1.4" }, "peerDependencies": { "solid-js": "^1.8.15" } }, "sha512-hK7TYpdib/XDb/r/4XDBFaO9O+3ZHz4ZWryV4/3BfES+tSQVgg2IJupDnztKXB0BqbSRy/aWlHKw1SPtNPYCFQ=="], - "@kobalte/utils": ["@kobalte/utils@0.9.1", "", { "dependencies": { "@solid-primitives/event-listener": "^2.2.14", "@solid-primitives/keyed": "^1.2.0", "@solid-primitives/map": "^0.4.7", "@solid-primitives/media": "^2.2.4", "@solid-primitives/props": "^3.1.8", "@solid-primitives/refs": "^1.0.5", "@solid-primitives/utils": "^6.2.1" }, "peerDependencies": { "solid-js": "^1.8.8" } }, "sha512-eeU60A3kprIiBDAfv9gUJX1tXGLuZiKMajUfSQURAF2pk4ZoMYiqIzmrMBvzcxP39xnYttgTyQEVLwiTZnrV4w=="], + "@kobalte/utils": ["@kobalte/utils@0.9.2", "", { "dependencies": { "@solid-primitives/event-listener": "^2.2.14", "@solid-primitives/keyed": "^1.2.0", "@solid-primitives/map": "^0.4.7", "@solid-primitives/media": "^2.2.4", "@solid-primitives/props": "^3.1.8", "@solid-primitives/refs": "^1.0.5", "@solid-primitives/utils": "^6.2.1" }, "peerDependencies": { "solid-js": "^1.8.8" } }, "sha512-jRVXr+zsVHxzDXRoh+CDeXzvCsFJ6uiHhqqNQ26Cw9ZsZ3D6nqPUBt1gGVtj2ZPmRL3a9Uk1v8D1aJ8/I12Dow=="], + + "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], "@kwsites/file-exists": ["@kwsites/file-exists@1.1.1", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw=="], @@ -1378,20 +1766,18 @@ "@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@changesets/types": "^4.0.1", "@manypkg/find-root": "^1.1.0", "fs-extra": "^8.1.0", "globby": "^11.0.0", "read-yaml-file": "^1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="], - "@markdoc/markdoc": ["@markdoc/markdoc@0.5.7", "", { "optionalDependencies": { "@types/linkify-it": "^3.0.1", "@types/markdown-it": "12.2.3" }, "peerDependencies": { "@types/react": "*", "react": "*" }, "optionalPeers": ["@types/react", "react"] }, "sha512-NxreNThm7foFgMMQD6zgk7rKkcFMmdC8J5r+Zn4FKoN75F5YjvwdihwF11VhrBfL3CXnD4+YG1VYwvBL+igzvw=="], + "@markdoc/markdoc": ["@markdoc/markdoc@0.5.9", "", { "optionalDependencies": { "@types/linkify-it": "^3.0.1", "@types/markdown-it": "12.2.3" }, "peerDependencies": { "@types/react": "*", "react": "*" }, "optionalPeers": ["@types/react", "react"] }, "sha512-req3Wpi1xXIs7C+n2nsKv7oGeC0xREkpd+D+JRGZ/pIMXEuQSlT+s/I760fqdmiysvkx10EC3MGlScokRRcaSQ=="], "@markdoc/next.js": ["@markdoc/next.js@0.5.0", "", { "dependencies": { "js-yaml": "^4.1.0" }, "peerDependencies": { "@markdoc/markdoc": "*", "next": "*", "react": "*" } }, "sha512-gzWvRc2dlxArQn1mDS4SHed46vkAoKBbXkELihQRYv1eVSA0u9eoNEdJXNuBL4Ajn9j2dPpwZp6nAVjN4nzZlA=="], "@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=="], - "@mermaid-js/parser": ["@mermaid-js/parser@1.1.1", "", { "dependencies": { "@chevrotain/types": "~11.1.1" } }, "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw=="], + "@mermaid-js/parser": ["@mermaid-js/parser@1.2.0", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA=="], "@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=="], - "@morphllm/morphsdk": ["@morphllm/morphsdk@0.2.166", "", { "dependencies": { "@vscode/ripgrep": "^1.17.0", "ai": ">=5.0.0", "diff": "^7.0.0", "isomorphic-git": "^1.25.10", "openai": "^4.52.7", "zod": ">=3.23.0" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.25.0", "@google/generative-ai": ">=0.21.0" }, "optionalPeers": ["@anthropic-ai/sdk", "@google/generative-ai"] }, "sha512-OawLa20p50rMv4EjEdHtIhi4G/AgECsOZgghPJc0159Ln6NevAjc3PynIcpS+nnk0T8yoYL6HKlLKvdzR0F0Fg=="], - "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="], @@ -1404,27 +1790,29 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], + "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], - "@next/env": ["@next/env@16.2.9", "", {}, "sha512-ki5VxxXfzD/9TDe13wyeTKIjQTAwBVpnr8KhRDUr8ltMUq1/NBpWNT5tiPoxiGl+PHM4X2ahSOiPk6iAimIzPg=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" } }, "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HkfxNYUCmcct0Xsqib5KxqMSHV4AHJq857BNRchyBDs4YS19aHzVfn1kDuBYKqLLQBjXgnkIsjV2Kd4d2wzYhw=="], + "@next/env": ["@next/env@16.3.0", "", {}, "sha512-o9r1S0BNiNreHP9Vs+Qnqd9kviDkJh8xIACY7UFZSmiGbbQRzPBBosvHzAU4TULHOIuOj/18RSsyz2qrREmIFw=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-7IAtK4MeybpqRV9GRABWEhJ62mOS+rzWOzOTFie4cSEtm12xsoOMJRcECoZx3FHPzFAqN/IJtHqWAFOLfl152w=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-55hpqq18bEVAlxedlTt3tFqZmKg2nUXT1kn1G/BGEy0R13h3LwtwHPVzzjG6P4LLeOHE32PFDQUVaJEWvBEZBw=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-hBD75iWpUtkL9SmQmcRhmLomn9jgkPzCEkbOcLgHymPEKzv+6ONy13RRiIEz/iEObjkS2Jlb5gYS2XGoS3X4rw=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-SOi96kSaF5T+0wW4koiM1bWzSPwjzTesC1p3df+FjdOi5LIQkBK/blxh7HdoKnNuI4PURF1OO7TZqtfnbWDSgw=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-qZTI3pf9SGc/obr8NkQAekBxmp1QK+kVm+VAf3BALLfFAj+1kUhkTxmrWpVos9R/UYIA8AWX2p6cGI5WdwzVUA=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-P0gZAoPMF4dyTRzhmkV4PrqVzSOB6t4mC1oI3c4dqijJ+OVEVx5clIXAKR4/uQpsqw2KKM/0D5tVumcR2r5blg=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.9", "", { "os": "linux", "cpu": "x64" }, "sha512-xm0HfRNX+UkH4R3c18ynswjj5o5uEj/7iI9p9omdtTSIsRCzQqkGMA+10nzJ4EHnYC3as65IMhbbl5fWRUWHYg=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-tXXGKJw0m37O0eKJARVTX/TheKPhz0QFVtVVZXmOig+9YKLQOSP6hvf2pxv5DO7CLEJyTHx3Pg043CDQkv1G4Q=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.9", "", { "os": "linux", "cpu": "x64" }, "sha512-QumimHkGEG6vM3PfEDWKyKen03NcqLOkeKB1EfcPe7VxzmEiCa4jNnMyBn/US5zcd/VE1CI+O8Ovb3lfjVHfGw=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pjGxK5EY7yWml78ALejFkWmgHsU7wbFQrISiugpH6FbUJhgEvw3xFZ/EBAtLl7QtL0WdQKiG9eWJ3mOKGTukHw=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-hzQpKZvw8rAwI6A2uQh6SacCSvNAXaIkPNsWwzqqfRiIMiXMfH936skDhz1OO6KpvdKkJrgHHtqQOq5PIXOvdQ=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-sjo++Xx+lomlPs3HRsHWhVDyGG6ms1kGW5EtHLERdII8AyG1i+f6aq68xHREO6AEMlhjTNEWBSmfJfqm9orf7g=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.9", "", { "os": "win32", "cpu": "x64" }, "sha512-qr2VL3Ce5QrwgO2yh1ujSBawrimjVKX8FGF/cOynmdYKJY0BdHpGVNIRK1tqONB10Vkm25Ub1BD2bkjWs4+96w=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-C5JSgiO54wURdaxdEUIXqkz04uMqC9UmPX1gtDrV/5Tf1UowdWYI8uA5hfFbPolTlp0q4KZ60xlHePNibf0VIw=="], - "@nodable/entities": ["@nodable/entities@2.2.0", "", {}, "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-fDOggsweNb5SSw0ZKVk6U+gxSyGFFlIBY/LBc1r8GUj4u/6t6oArL+Pmkg0MBnsgR+KkdsURilVH4F3GXUGepA=="], + + "@nodable/entities": ["@nodable/entities@3.0.0", "", {}, "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -1476,10 +1864,12 @@ "@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@9.2.2", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ=="], - "@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="], + "@octokit/plugin-request-log": ["@octokit/plugin-request-log@1.0.4", "", { "peerDependencies": { "@octokit/core": ">=3" } }, "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA=="], "@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@10.4.1", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg=="], + "@octokit/plugin-retry": ["@octokit/plugin-retry@3.0.9", "", { "dependencies": { "@octokit/types": "^6.0.3", "bottleneck": "^2.15.3" } }, "sha512-r+fArdP5+TG6l1Rv/C9hVoty6tldw6cE2pRHNGmFPdyfrc696R6JjrQ3d7HdVqGwuzfyrcaLAKD7K8TX8aehUQ=="], + "@octokit/request": ["@octokit/request@8.4.1", "", { "dependencies": { "@octokit/endpoint": "^9.0.6", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw=="], "@octokit/request-error": ["@octokit/request-error@5.1.1", "", { "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", "once": "^1.4.0" } }, "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g=="], @@ -1490,22 +1880,44 @@ "@octokit/webhooks-types": ["@octokit/webhooks-types@7.6.1", "", {}, "sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw=="], + "@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/client": ["@opencode-ai/client@workspace:packages/client"], + + "@opencode-ai/codemode": ["@opencode-ai/codemode@workspace:packages/codemode"], + "@opencode-ai/core": ["@opencode-ai/core@workspace:packages/core"], "@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"], + "@opencode-ai/http-recorder": ["@opencode-ai/http-recorder@workspace:packages/http-recorder"], + "@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@1.17.4", "", { "dependencies": { "@opencode-ai/sdk": "1.17.4", "effect": "4.0.0-beta.74", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.3.4", "@opentui/keymap": ">=0.3.4", "@opentui/solid": ">=0.3.4" }, "optionalPeers": ["@opentui/core", "@opentui/keymap", "@opentui/solid"] }, "sha512-jqXxfYnunCwyiEEsdVNDsKUhKznXytAntwzeW3uLd2zbIYsbVCiq5nG5ZSlo4mz1IXgRLWzXDYk1OnmfG15V2A=="], + "@opencode-ai/plugin": ["@opencode-ai/plugin@1.18.14", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "1.18.14", "effect": "4.0.0-beta.83", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.4.5", "@opentui/keymap": ">=0.4.5", "@opentui/solid": ">=0.4.5" }, "optionalPeers": ["@opentui/core", "@opentui/keymap", "@opentui/solid"] }, "sha512-M7gmzsQTE6z81fMZb8AaNd3u/FdQ8h7hhtQ59rWHnZMFMWusHXZM2S2BL1Z3E8kGWeveVluOVfTKtxJhCurXtw=="], + + "@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"], + + "@opencode-ai/schema": ["@opencode-ai/schema@workspace:packages/schema"], "@opencode-ai/script": ["@opencode-ai/script@workspace:packages/script"], - "@opencode-ai/sdk": ["@opencode-ai/sdk@1.17.4", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-OdHkBoNIQOjQsPnFtkcxAp9HsLAKn/MQiq7wIQURkRARmw8yhFqGiMPYbNm+UfLn6bMkx97rdPkWQARejMyiXQ=="], + "@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.14", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-Yd8vPDT5DW++Hs2904Q/Vvk/m1U79aKFSRusgAfBQ+AGhfoT40LdeL5eiJtVy0xBUIZF/8DEZjkWaeof9Y59PA=="], + + "@opencode-ai/sdk-next": ["@opencode-ai/sdk-next@workspace:packages/sdk-next"], + + "@opencode-ai/server": ["@opencode-ai/server@workspace:packages/server"], + + "@opencode-ai/session-ui": ["@opencode-ai/session-ui@workspace:packages/session-ui"], "@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"], + "@opencode-ai/tui": ["@opencode-ai/tui@workspace:packages/tui"], + "@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"], "@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=="], @@ -1534,25 +1946,39 @@ "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="], - "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="], + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], + + "@opentui/core": ["@opentui/core@0.4.5", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.5", "@opentui/core-darwin-x64": "0.4.5", "@opentui/core-linux-arm64": "0.4.5", "@opentui/core-linux-arm64-musl": "0.4.5", "@opentui/core-linux-x64": "0.4.5", "@opentui/core-linux-x64-musl": "0.4.5", "@opentui/core-win32-arm64": "0.4.5", "@opentui/core-win32-x64": "0.4.5" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-JsgRTPkA6e+Vxmumxai6SElOSlRQkbzNKHlCfemlArRiLhfC1IZ9RXJo2QH4xSu+uBOWAM90uss73/pPlkdEig=="], + + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8KUG0oRidnR+oW1RSZJ72/PhZLl+qRRMk5U/mieF4c0SJ5V3tYACpBZAKzQfHNd1f7QzD8FHZct1lPpQgtmkWg=="], + + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-R2bocsg55gwjOqCp/MWFgFYzRmsduKegB6nzgFAPCvAD/L5Jf30xpWJWFlSg3x8vxe1L9WJ84dfqa4M7mZZ3wA=="], + + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-R4MZ25a4CzOAGVjW9aj1hUfzQGVfCJwrwBDbNs2SXaIvzcZqkxCVtU4FoQ5LsaD0j/BdNQVg2CIfFkFsm1fDuQ=="], - "@opentui/core": ["@opentui/core@0.2.15", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.15", "@opentui/core-darwin-x64": "0.2.15", "@opentui/core-linux-arm64": "0.2.15", "@opentui/core-linux-x64": "0.2.15", "@opentui/core-win32-arm64": "0.2.15", "@opentui/core-win32-x64": "0.2.15" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-YGHttdZWScMcSvtYgZkLR6VhUO1OoUiQzwYjZgIusf5eCkPLD8PapH+PTMVqAiX16CHO6JxfMlkHv5qDiHAccQ=="], + "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ieqdyKI6EIYPalYAETB2wsdP83hr5Ifi+dFnBFUmdEEFHsoKwBmn2S7bsTOYlX7Bg03F4/YPIg+IvRpeC+cUJw=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-s25f9GmZd6wxNM5ExRmwwnLT+NLCKxnTWuO9aObOlqsXfLMGHQZrb6YwgAn/PSTua98KmH7GJCVWdPgZ/P+0RQ=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-SNyuQoxMKI1vuJhgxSSW96adWM6LqFl2SoS3GM4tGeneGOanVVG2Y06PvlytXvF4cKik97t0rqkVMRetmOs93w=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-GyaipN+nOcEr8rcTO2mqKTGmOBk0C300I69fLtubD3BadHcMI1DVNlQrcf/J1mkQEuMYbmBTi/1hT1ybWGr2Mw=="], + "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mKVKcIcPiSVVZZsdPSBoWwoa2/TCeQAaMDeHF7PFw2kt5bTXZPP7xxWfRQLCNIcA1eaGl59UuwUWHDR2Ve548Q=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-h+uyufselGT4afKMP8Lg4yUl5Kp+DJBlhu3XpWXhphE5Pnq5+f0uGBr4P+34CNcWxMsDnvagSQLFRCS4rGrOWA=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-GHTTsqeR45q2Iek9Rb7ty+x/hAKn2jZ1ujlCgPR8LBKyF7h0E1dNFryoZ7ehMc3kJndP1sKn836IemKFqxuDdQ=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.15", "", { "os": "linux", "cpu": "x64" }, "sha512-jx+NImPq4wSp3Apfe7tlixiEJNnRyECTRJRWhGF6ZJz4PwFfgK2UHZKYR0DZHbV8nYawoDNQPJDXEWcoZShnMg=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-Y8T/yXCDGagRGiQrtmuB6AhRcPucKFs/Dre3v8kJwNYqDccI4FzUPKclZ7djfmRZNjl7JUqPhZZP/PwDpQocMg=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-2SQQLvf3sgmToxrNika9AdcccKrjPJEn5jW6sSv0oEixNBzUzW41vSZZG4LM/V3lL8eg0LoYDnRZeKLB4gwSqQ=="], + "@opentui/keymap": ["@opentui/keymap@0.4.5", "", { "dependencies": { "@opentui/core": "0.4.5" }, "peerDependencies": { "@opentui/react": "0.4.5", "@opentui/solid": "0.4.5", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-S1wzKHhF70zT6bH+VBFY+lSeTImLcIFW28JNQiME8MoPcy6KGPs7rKFSHrb/U7P8rsTJeRfW5A4d1Cy6PKodDg=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.15", "", { "os": "win32", "cpu": "x64" }, "sha512-SVMVgnC7LVEm+yVZKdmmhRBj/xAT94PanT+UCcHxaCWK+OLmv/AX+ohHq2m0odup6iXcEqj+7mAltO9fgJLFIg=="], + "@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=="], - "@opentui/keymap": ["@opentui/keymap@0.2.15", "", { "dependencies": { "@opentui/core": "0.2.15" }, "peerDependencies": { "@opentui/react": "0.2.15", "@opentui/solid": "0.2.15", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-wqQp6y7P2jZZJiOMwupxjGryuSWCs+njjglwW/xny9J17gomBmUvTIcIIWNG0Jv+EGO9ScBzCScGlwBHFhHyYw=="], + "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], - "@opentui/solid": ["@opentui/solid@0.2.15", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.15", "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-CViepAjsCWXwrLndMt+qlLo7cooVX7DXwSJHNizw7mfrRJtOPzSYJZCIk1vF4IJTWffCHygoYMe3uSeKvzAcbw=="], + "@oslojs/binary": ["@oslojs/binary@1.0.0", "", {}, "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ=="], + + "@oslojs/crypto": ["@oslojs/crypto@1.0.1", "", { "dependencies": { "@oslojs/asn1": "1.0.0", "@oslojs/binary": "1.0.0" } }, "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ=="], + + "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], + + "@oslojs/jwt": ["@oslojs/jwt@0.2.0", "", { "dependencies": { "@oslojs/encoding": "0.4.1" } }, "sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg=="], "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.127.0", "", { "os": "android", "cpu": "arm" }, "sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ=="], @@ -1596,43 +2022,43 @@ "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], - "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.20.0", "", { "os": "android", "cpu": "arm" }, "sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg=="], + "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA=="], - "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.20.0", "", { "os": "android", "cpu": "arm64" }, "sha512-QqslZAuFQG8Q9xm7JuIn8JUbvywhSBMVhuQHtYW+auirZJloS41oxUUaBXk7uUhZJgp44c5zQLeVvmFaDQB+2Q=="], + "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.24.2", "", { "os": "android", "cpu": "arm64" }, "sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg=="], - "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.20.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MUcavykj2ewlR+kc5arpg4tC2RvzJkUxWtNv74pf7lcNk00GpIpN43vXMj+j6r4eMmfZhlb8hueKoIb8e9kAGQ=="], + "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.24.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w=="], - "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.20.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-BGB16nRUK5Etiv//ihPyzj8Lj1px0mhh4YIfe0FDf045ywknfSm0GEbiRESpr6Q4K82AvnyaRIhhluHByvS4bg=="], + "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.24.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q=="], - "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.20.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JZgtePaqj3qmD5XFHJaSLWzHRxQu0LaPkdoM1KJXYADvAaa83ijXHclV3ej3CueeW0wxfIAbGCZVP45J0CA7uQ=="], + "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.24.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw=="], - "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.20.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hOQ/p3ry3v3SchUBXicrrnszaI/UmYzM4wtS4RGfwgVUX7a+HbyQSzJ5aOzu+o6XZkFkS3ZXN4PZAzhOb77OSg=="], + "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg=="], - "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.20.0", "", { "os": "linux", "cpu": "arm" }, "sha512-2ArPksaw0AqeuGBfoS715VF+JvJQAhD2niWgjE5hVO+L+nAfikVQopvngCMX9x4BD8itWoQ3dnikrQyl5Ho5Jg=="], + "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA=="], - "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.20.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0bJnmYFp62JdZ4nVMDUZ/C58BCZOCcqgKtnUlp7L9Ojf/czIN+3j72YlLPeWLkzlr6SlYvIQA4SGV/HyO0d+qg=="], + "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA=="], - "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.20.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-wKHHzPKZo7Ufhv/Bt6yxT7FOgnIgW4gwXcJUipkShGp68W3wGVqvr1Sr0fY65lN0Oy6y41+g2kIDvkgZaMMUkw=="], + "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw=="], - "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.20.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RN8goF7Ie0B79L4i4G6OeBocTgSC56vJbQ65VJje+oXnldVpLnOU7j/AQ/dP94TcCS+Yh6WG8u3Qt4ETteXFNQ=="], + "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.24.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA=="], - "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.20.0", "", { "os": "linux", "cpu": "none" }, "sha512-5l1yU6/xQEqLZRzxqmMxJfWPslpwCmBsdDGaBvABPehxquCXDC7dd7oraNdKSJUMDXSM7VvVj8H2D2FTjU7oWw=="], + "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw=="], - "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.20.0", "", { "os": "linux", "cpu": "none" }, "sha512-xHEvkbgz6UC+A3JOyDQy76LkUaxsNSfIr3/GV8slwZsnuooJiIB34gzJfsyvR4JdCYNUUPsRJc/w/oWkODu+hg=="], + "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w=="], - "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.20.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-aWPDUUmSeyHvlW+SoEUd+JIJsQhVhu6a5tBpDRMu058naPAchTgAVGCFy35zjbnFlt0i8hLWziff6HX0D3LU4g=="], + "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.24.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ=="], - "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.20.0", "", { "os": "linux", "cpu": "x64" }, "sha512-x2YeSimvhJjKLVD8KSu8f/rqU1potcdEMkApIPJqjZWN7c2Fpt4g2X32WDg1p+XDAmyT7nuQGe0vnhvXeLbH+g=="], + "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg=="], - "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.20.0", "", { "os": "linux", "cpu": "x64" }, "sha512-kcRLEIxpZefeYfLChjpgFf3ilBzRDZ+yobMrpRsQlSrxuFGtm3U6PMU7AaEpMqo3NfDGVyJJseAjnRLzMFHjwQ=="], + "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw=="], - "@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.20.0", "", { "os": "none", "cpu": "arm64" }, "sha512-HHcfnApSZGtKhTiHqe8OZruOZe5XuFQH5/E0Yhj3u8fnFvzkM4/k6WjacUf4SvA0SPEAbfbgYmVPuo0VX/fIBQ=="], + "@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.24.2", "", { "os": "none", "cpu": "arm64" }, "sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg=="], - "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.20.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-Tn0y1XOFYHNfK1wp1Z5QK8Rcld/bsOwRISQXfqAZ5IBpv8Gz1IvV39fUWNprqNdRizgcvFhOzWwFun2zkJsyBg=="], + "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.24.2", "", { "dependencies": { "@emnapi/core": "1.11.2", "@emnapi/runtime": "1.11.2", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q=="], - "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.20.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPi25YNPe4YenS8MgsQU2+bIFHxxpLx1LVna2444cEHqNPhNjvWf9zqj4aWE43H9LpAsTmkkAlA3eL5ElBU3mA=="], + "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.24.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg=="], - "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.20.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Wb14jWEW8huH6It9F6sXd9vrYmIS7pMrgkU6sxpLxkP+9z+wRgs71hUEhRpcn8FOXAFa27FVWfY2tRpbfTzfLw=="], + "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw=="], "@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.21.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-P20j3MLqfwIT+94qGU3htC7dWp4pXGZW1p1p7FRUzu1aopq7c9nPCgf0W/WjktqQ57+iuTq9mbSlwWinl6+H1A=="], @@ -1712,9 +2138,11 @@ "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="], - "@pierre/diffs": ["@pierre/diffs@1.1.22", "", { "dependencies": { "@pierre/theme": "0.0.28", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-1Iv7kdl6OABFCd1n2HQbGUiRHouXPaHoIjcb7Lwg8zeJKY5ph+cESFcGEyIiwW0NCbKGtYS2bTnXmI+Eze5dwg=="], + "@pierre/diffs": ["@pierre/diffs@1.2.10", "", { "dependencies": { "@pierre/theme": "1.0.3", "@pierre/theming": "0.0.1", "@shikijs/transformers": "^3.0.0 || ^4.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0 || ^4.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-rPeAmDWarxFVTQpaf4y6wTxjZxU44xKJKoJti2zU21P06DVd9nRHZX+xSIObLB307Qjpaesyb1x/j0z94t7vLw=="], - "@pierre/theme": ["@pierre/theme@0.0.28", "", {}, "sha512-1j/H/fECBuc9dEvntdWI+l435HZapw+RCJTlqCA6BboQ5TjlnE005j/ROWutXIs8aq5OAc82JI2Kwk4A1WWBgw=="], + "@pierre/theme": ["@pierre/theme@1.0.3", "", {}, "sha512-sWHv11TMoqKxKDgTIk5VbhQjdPhs8DCcBxbjh3mRlS3YOM/OcrWoGX6MM8eBGn9cUu3M46Py0JnxsG2nJaFTuA=="], + + "@pierre/theming": ["@pierre/theming@0.0.1", "", { "peerDependencies": { "@pierre/theme": "^1.0.0", "@shikijs/themes": "^3.0.0 || ^4.0.0", "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0", "shiki": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["@pierre/theme", "@shikijs/themes", "react", "react-dom", "shiki"] }, "sha512-1thlEtJbqdyLzc1ZS2KQa1q7FzDGHT4dTEdKHoyQjOMeWWOmbVG5/ndEfOKfAb5Fzkz8cNJrOjFLiZoDH/A03A=="], "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], @@ -1722,9 +2150,21 @@ "@playwright/test": ["@playwright/test@1.57.0", "", { "dependencies": { "playwright": "1.57.0" }, "bin": { "playwright": "cli.js" } }, "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA=="], - "@posthog/core": ["@posthog/core@1.32.3", "", { "dependencies": { "@posthog/types": "1.386.3" } }, "sha512-vwOEMfZvGv5XxNWV7p9I52NSmvFNMhyW2IHpIoUHW5jLkgUrknzJW1H/qxVGSIrNNVQkfsoaDFzDhJdg10pgrA=="], + "@pondwader/socks5-server": ["@pondwader/socks5-server@1.0.10", "", {}, "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg=="], + + "@posthog/browser-common": ["@posthog/browser-common@0.4.0", "", { "dependencies": { "@posthog/core": "^1.46.8", "@posthog/types": "^1.402.0" } }, "sha512-W9DCGVks15docUMPvJ2nd8NS16Gn74bsGWuaeg31beEKFSjdW8wvnQ1ETY6WSql5pYxZb3GdJmEUZVVstKSrBQ=="], - "@posthog/types": ["@posthog/types@1.386.3", "", {}, "sha512-LqJoiQi2eyWn7rCUgnn+D+F3Efp6+04o72bjSX6kWHx0nFaYNC/nJuAIRliDTY/X7GPIUAaHAcSjbMI/9wfX1Q=="], + "@posthog/core": ["@posthog/core@1.46.9", "", { "dependencies": { "@posthog/types": "^1.402.2" } }, "sha512-EXO6y5ih+jBkTCpUCuYgQmajuDZuvy6vMvflkub6pLQyi0GPlCPWVSvZZkOeQw9e2MxoD5GteeGCt9R8+UJ/yQ=="], + + "@posthog/types": ["@posthog/types@1.402.2", "", {}, "sha512-ZZTiS4dLwF4/D0YTzS3gGSLFnhuNOY5yu4d9VGS9trGe5GW6FjIXo20p18K5BPW2RZSYSld8sdnSAY3AUXleUQ=="], + + "@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=="], + + "@protobuf-ts/protoc": ["@protobuf-ts/protoc@2.11.1", "", { "bin": { "protoc": "protoc.js" } }, "sha512-mUZJaV0daGO6HUX90o/atzQ6A7bbN2RSuHtdwo8SSF2Qoe3zHwa4IHyCN1evftTeHfLmdz+45qo47sL+5P8nyg=="], + + "@protobuf-ts/runtime": ["@protobuf-ts/runtime@2.11.1", "", {}, "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ=="], + + "@protobuf-ts/runtime-rpc": ["@protobuf-ts/runtime-rpc@2.11.1", "", { "dependencies": { "@protobuf-ts/runtime": "^2.11.1" } }, "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ=="], "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], @@ -1742,61 +2182,61 @@ "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], - "@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="], + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="], "@qdrant/js-client-rest": ["@qdrant/js-client-rest@1.17.0", "", { "dependencies": { "@qdrant/openapi-typescript-fetch": "1.2.6", "undici": "^6.23.0" }, "peerDependencies": { "typescript": ">=4.7" } }, "sha512-aZFQeirWVqWAa1a8vJ957LMzcXkFHGbsoRhzc8AkGfg6V0jtK8PlG8/eyyc2xhYsR961FDDx1Tx6nyE0K7lS+A=="], "@qdrant/openapi-typescript-fetch": ["@qdrant/openapi-typescript-fetch@1.2.6", "", {}, "sha512-oQG/FejNpItrxRHoyctYvT3rwGZOnK4jr3JdppO/c78ktDvkWiPXPHNsrDf33K9sZdRb6PR7gi4noIapu5q4HA=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.61.1", "", { "os": "android", "cpu": "arm" }, "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.4", "", { "os": "android", "cpu": "arm" }, "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.61.1", "", { "os": "android", "cpu": "arm64" }, "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.4", "", { "os": "android", "cpu": "arm64" }, "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.61.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.61.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.61.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.61.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.61.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.61.1", "", { "os": "linux", "cpu": "arm" }, "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.61.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.61.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.61.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.61.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.61.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.61.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.61.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.61.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.61.1", "", { "os": "none", "cpu": "arm64" }, "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.4", "", { "os": "none", "cpu": "arm64" }, "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.61.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.61.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.61.1", "", { "os": "win32", "cpu": "x64" }, "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.61.1", "", { "os": "win32", "cpu": "x64" }, "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q=="], "@secretlint/config-creator": ["@secretlint/config-creator@10.2.2", "", { "dependencies": { "@secretlint/types": "^10.2.2" } }, "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ=="], @@ -1822,19 +2262,23 @@ "@secretlint/types": ["@secretlint/types@10.2.2", "", {}, "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg=="], - "@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=="], + "@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=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og=="], - "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-OFx8fHAZuk7I42Z9YAdZ95To6jDePQ9Rnfbw9uSRTSbBhYBp1kEOKv/3jOimcj3VRUKusDYM6DswLauwfhboLg=="], + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-hTorK1dffPkpbMUk6Z+828PgRo7d07HbnizoP0hNPFjhxMHctj0Px/qoHeGMYafc6ju+u9iMldN4JbVzNQM++g=="], - "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-Yx3gy7xLzM0ZOjqoxciHjA7dAt5tyzJE3L4uQoM83agahy+PlW244XJSrmJRSBvGYELDhYXPacD4R/cauV5bzQ=="], + "@shikijs/langs": ["@shikijs/langs@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0" } }, "sha512-bwrVRlJ0wUhZxAbVdvBbv2TTC9yLsh4C/IO5Ofz0T8MQntgDvyVnkbjw9vi50r1kx7RCIJdnJnjZAwmAsXFLZQ=="], - "@shikijs/langs": ["@shikijs/langs@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0" } }, "sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA=="], + "@shikijs/primitive": ["@shikijs/primitive@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-NOq+DtUkVBJtZMVXL5A0vI0Xk8nvDYaXetFHSJFlOqjDZIVhIPRYFdGkSoElDqNuegikcc3A76SNUa8dTqtAYA=="], - "@shikijs/themes": ["@shikijs/themes@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0" } }, "sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ=="], + "@shikijs/stream": ["@shikijs/stream@4.2.0", "", { "dependencies": { "@shikijs/core": "4.2.0" }, "peerDependencies": { "react": "^19.0.0", "solid-js": "^1.9.0", "vue": "^3.2.0" }, "optionalPeers": ["react", "solid-js", "vue"] }, "sha512-OaMUUStdIZ+l1GJad9uVACR3Xvgwo4y+RmEuDMU62cgFMMg1IBCaIFmvzAR2HiCpGtwoc/qPfpNnP+ivgrPXZg=="], - "@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="], + "@shikijs/themes": ["@shikijs/themes@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0" } }, "sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w=="], - "@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], + "@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/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=="], @@ -1858,82 +2302,94 @@ "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="], - "@smithy/config-resolver": ["@smithy/config-resolver@4.5.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-AXbvUX9aNY2qCLOMCikpl1Df5w2CNFEqbEb6XafG81FJbAbB8avIT7BOx1KDqiO86J/38qKQ3YuakfAfY3iBkQ=="], + "@smithy/config-resolver": ["@smithy/config-resolver@4.6.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-XMDfb7LTrlUeI9PpfMa6xzRv+yMtYFsr1LqTsR3CGpSSaaPIR5ldlAiXyLjFYmbh1/DD5NxHmDybdh6lJjJM+Q=="], - "@smithy/core": ["@smithy/core@3.24.6", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug=="], + "@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.8", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-5cAM+KZC02sTqDt6NaLXyu50M/GNMd1eTzDVR8Lb0BBsVtu7RWHo47VPPEEv1vt3Yub6uzr+M5FHC+GtoT0USg=="], + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w=="], "@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.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-BQao/dBhLCJqo953N1hadkcF3M/9G+i6qIgnMupfdpBQomwyhfV7Xfc5jjpCkm8HxfzaWAGrM/2nNnzronFqVQ=="], + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-t+RmwgPwqlhzHxH9EHubIkyN2eSAuy5d4LR7gz/HsUZRHkXpHRuzjiPvD7fLIVefhgSakGoiiiJ1jUursfFkMQ=="], + + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.5.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-XSqzMmc3iUO+jC50lQET3l8fX8qvt9apyQMobmHy8o9Ax4eDcIqWvXKdzGJJanJZNeqfH+Itwo6l4tspmKQekA=="], + + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-xIWOCcsrQPlWzxXfdPer0cHzvEDycVKk76F1Oxl73qXq8lZuGd+lzJYWo3vkrXnrwqFya95urMmYMjDIY7hG8g=="], - "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-OUoNRXJGZMM4ivoU7QIzOvCLbavD1YnadNEairrtYhTi+gmGhyn3c2wToL9CxEs4Cw2Ab/KeQM39T1K+/e9YdQ=="], + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], - "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-M6FeKRMi3oecpTy4EL5n1hLPWydw+xInFYQIzjbGYGBnFtW7IlJjnXrKr/Ev1GpMtmw44QCmrl8+ACEFPmRsIg=="], + "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-B3yUUqflj/L1HY1QvSLSmU+NOtG5mroO4+dXj3sjqWKxdO//wrlSpkymV+dkVpwUJvwCXtSAybdzs2WIiCArjQ=="], - "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g=="], + "@smithy/hash-node": ["@smithy/hash-node@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-9NJHd8scrdVORvnaMHIHgPipGxLiTK3FMNUfWl0tHn2fwJIEk4rZ3OM793yIWV8L+WKu+AWQZBcVyd5qrVXy+A=="], - "@smithy/hash-node": ["@smithy/hash-node@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-lIZyQ7gDxURrnfkjalM0lKmDnfZYuPzNBYlkza3czPTQNVYsg4e0o90Zx/RpxhamKKOGsQGCsopp0ULsJqltNQ=="], + "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-ZhloOl0+kyQHlmaEhYvsPbNs9ekLHsbMt82W70v0ChIQKbm1HaUAozWDph2QmA8L/PtiP6cwy+6nPTffVNjBMg=="], - "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-jUH1Eth7Sgn4KPBX5OKYDRpNjzul7AzsIhxKXT1rHXPTSfY00/7Kb9RtNil5SDAlPPsxaUiesR/rql2wjackmw=="], + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-fEXR2jKLnu1U9sL/Obdkxs8f3M1pLSDjM30cMmpz2L61oGnlUdx1Z9Wj+9+mID7kKldEVOUQfuXqHrK4RP90FA=="], - "@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-Uu5RVG+BjnS74dzSlQbK50ONlE3DD9YXdZwlBbZRCjvEp5KrBu/FPwO3SyDBYf6l12Q0DxSFdjeBiIh+7gNcmg=="], - "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-nfpYCrzSFAgfIXmIHFTjOGNeTV3DVF5E5rfi3ZuNfsOjKSpePBOJF3rjyXlWYND0anvxVoqioIwClWCNdKt4Og=="], + "@smithy/md5-js": ["@smithy/md5-js@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-XdPyIiKajNJ5HoLkBYe3EP+Tc9LmyIz/o7bfcCvr7TR7wmtJqAezvyMSV7X+vjTh6l/y3dC81tIc2PirpMpbZw=="], - "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.5.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-zdG5bJZOiM2PRgL2lwcgui6uwZ+s5y6Qsk/rk05Q69sZJT6oi1x+v8Kn++V/q9VY94EgOtEe5kivpu+eGau0wQ=="], + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-Q+J8iTUHr5qtoejFJCb9TTavDh7xQFVFRjy1n1edq5BFJq5b4mfziqrEqJlAscHHVdsqyFTNJXcV+b9i3oad9w=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.6.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-MWppaYUlc+W4cU2JZnYuMFeOxCWbKO4A57BWti6aCb7hRBK3+CL6llADGpX084hjImsqr3EvCGewArOj7G81eA=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.6.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-nU2N3LY6Vn46rwga1+gZQJ1Zsu+3/Qj2X+d5gKb8gSvLmojZd0sg9XWYqMDH59VeadVKOGOhMrhyzUo7ln4lGw=="], - "@smithy/middleware-serde": ["@smithy/middleware-serde@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-I3fPVYKKEog3a3qdqt1nttP1NBuQOAlNoQxEp6j5pMogSx0HHfid63difhcDgslV6p1XsTXG6D6ieTe13ycJtQ=="], + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.7.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-JOVvyFbNiXUuT8Id+uNIEzbkqi8IOa6zqEQgzpoQ1xYIQQFTFL/mXIiBRLlA129aoBB1EeiElkhmLZyok5axYA=="], - "@smithy/middleware-stack": ["@smithy/middleware-stack@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-QhNiWfg47Kl4SJHmuQvnlzCtlD1eX1J7d/vuuttIE17Ra2YUKp9Srv5lCwa3OvoYaSNWMKYn0PjGIsfCLMJsEA=="], + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-nwYV4tBUU5kBOYec2p8vKz5pzMVWVEcalVY4ZXguUdZZEOm9LQZhT0pKsnYSQhldvWegaFKWAR45Mi4XUfv5cQ=="], - "@smithy/node-config-provider": ["@smithy/node-config-provider@4.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-M+gG6eQ0y073mSmNB+erRXJvwpsqsN72ol2w6vcd8FEKeG7pqYK0JvzfVqONkPj2ElBB2pg+cU13I850b//Wag=="], + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-IivXdOXykxp8Eq+qDL91mxEVd9CoR7wEIJqhJiwbHnmJVX6SZm2hxZt6uSDQOEh5tYHQdj6WuqmpNhV7P8C1Ww=="], - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.7", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-ZAFvHXrEk6K180EVhmZVg8GU5pUH5BSFqRs27JW3j1qEFx9YyYwWFx17x/MHcjALYimGAji7qEOlF1++be+G5A=="], + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.5.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-N4XTMCO77u8jXq9Ms1pbFM3M9zD+ScKnvB+M7//VyawM8L6xW3/mqAPp/uMOYprW+q/b1Qen6qUKVwxTb2ymiQ=="], - "@smithy/property-provider": ["@smithy/property-provider@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-0rhHv1Ww27kajF6qewme2aRtJmKFtSwE6EZ2dj5KxdX/R3ANsUugqTnH0tvpZwGiQ3MOMhetuCGFAeKVv3/Onw=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], - "@smithy/protocol-http": ["@smithy/protocol-http@5.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-H6S7NyaaL+7qO8kIL7VQ7KyrGnKXdllGzJqvtp3hvDen25UOydKV51qGDVK0UciW125jV3CoLJQy/ihc0OEC6A=="], + "@smithy/property-provider": ["@smithy/property-provider@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-xsj0qoE3ya7Fu3YoyKPINtGWsY33IO2ypcOJpLAGMbzGYTNr0wqK0HnCroHRIoJTsHapnsFABUhzlNfHo/diwg=="], - "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.5.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-In8gYD2R66EKlGAq9QrNKVrMOGaGBD7LUNp2kUjeQ4V9zNktFIXBPmrCySr4YYo+jVeVL6CnWj26sOamcF0qIg=="], + "@smithy/protocol-http": ["@smithy/protocol-http@5.5.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-iPaxIe9mTyidyhM6WF2/gSLPezyCwSlZcqlDBj2KCoSS994iw4yf1se1xmZkWsY98ZpwjDR/ZMubOSVx+Nrokw=="], - "@smithy/signature-v4": ["@smithy/signature-v4@5.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-Ojg4B6oIDlIr1R86xCDJt1zJWnYa0VINmqdjfe9qxWjdRivHalZ3iSlQgVqYbW0MdpFOC5XfHEWsnbmdnpIILQ=="], + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.6.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-apuIuit7x6ZM0nJxuKf24MCkflwnSyD6wePOPGkhntEl0gLaIYLlZTQtzCB+j3kWOTpqx0D1o6JZ15NTk4f8xQ=="], - "@smithy/smithy-client": ["@smithy/smithy-client@4.13.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-tAf35/JW/DvMlACcazcoIOKOV0JBqyOvxjPTEME9W+m9wLcE0G1rwADc7Ntu38rY5C9OH8jZjpo4tbtjmIjEBQ=="], + "@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - "@smithy/types": ["@smithy/types@4.14.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ=="], + "@smithy/smithy-client": ["@smithy/smithy-client@4.14.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-UzWzH88hntrIn1wMPlmt/6vmQL+FS+pYgqvBsp4QQdgt/DUFZZD0S0xRO7kCfAfz9fqn9boVb+buFWU4LD9PJg=="], - "@smithy/url-parser": ["@smithy/url-parser@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-9MRJzwUrlswwHogOR7raDcykuzojZn74qGdQdbEQLVaixlvJuMiIT0g/CejKcmAIgrUVs8brBrnGtmYmBc0iuA=="], + "@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - "@smithy/util-base64": ["@smithy/util-base64@4.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-V6ApAGvCQnb7Wy1Sy60AQc+7UOEaNQxvAXBLdMi5Zzm66cmX0srvfAxDmg7BGuJ+9H9ez0PPWS/AeFgWxwGavA=="], + "@smithy/url-parser": ["@smithy/url-parser@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-0VynNn3o4qSpRbgtG6RbT09wsN2HjsIkBvxHUX99p0t3sFj7hvqsOtPQBubawSNH4PWfwXgkD82vG166ROta7w=="], - "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-+3vGcNHuvzuFLVWL9/wJgucOuQWufhuGhb3oxVDj9SWFGtwkOmtC2nFUwVC2IJoPe45uhs6TAb8bgE4IXDSPzA=="], + "@smithy/util-base64": ["@smithy/util-base64@4.5.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-80e25qWKD6hJh5BenAutni2Z6dj2AiZKmht21pcROmzWzQja8yl+KqsuDSia++hwk1Ne6RrDEBwP6I+A4R0XAg=="], - "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-T15zTQJ/xKYdS0/3CFckhz1QBbhxmhk/xjL6FKvHKgkJPN4E985If2FI9CcV2kh2v0sfiWMfXVEOKFbqgw4m4w=="], + "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-gTqO1MkKFsaKgJoGelxgWBdUlHqc9g1z6ItBVFsQU1yTO4W6BEIq/NZIDf/UTi6ueojTr2fJRetusBIbctA6KQ=="], - "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-sms/ty2CJwHOiGzEaAVWizTVK5KusXpAYqCUeXIa+hWtNKLwjimH4z11mc07d0Fe3DT3lmZJIZWOMcVQ/N4hBQ=="], + "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-BTwIXGhunFzjNYpKrRJJjgfEONT8m/wyBB0IL4BWB9WqJSQa/CEMh2cPHqbqMpKwnAljPuhhbGwF2Z0qm2C04A=="], - "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-dRCZKu05AL7KQWrVuRJPotfjCRnvGkCjV56XNP067CRfyTtvgi/Ygu44qrBKb814Hsa52bWwDJ+Vt3pd04BjPA=="], + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-HxG8CH8HXUO6JqH+ftXCJm1XrISkcZNrOLGkMYiz90wT3FPBZZE/syDNYJ/iKQEi91gN2J/SnyEhHjcJPEVSFw=="], - "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-tTR8tayMoa0WeRhtMH7j3WpHUtggBXjh7rBdf7j6POYI69R85gpWBW6B32kaJRnlQU8+0gOGAzJj50S7SU1Egw=="], + "@smithy/util-config-provider": ["@smithy/util-config-provider@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-GB3CQ1o09Dc3aUOswcy3q3xbe5ib5tNyIDmvn8i8Nno01gogbqn1BNIHGawSHApEFVtL1OqjkoiQ9lMXpgY5ig=="], - "@smithy/util-endpoints": ["@smithy/util-endpoints@3.5.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-kaB41eVUYC7ajVWUsZRqagxwRaa3VupjQ/Z2Z2v/Vffh/gJ/fFOS25s6mTyR2Lw1FrnBbRWo1iShR9BhekpPeQ=="], + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.5.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-en8XtoFeELWwI0pwpmUbf63bJ9WRusVozGQ59mE9YuUuW+Q/FFRfdbd/EZIf/p1oZXHerbj30ITuer7vE47TbQ=="], - "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-ooo5MQdstAtIlgS0bchoMkVsQ3x1wLLPtFilpeIV8wVtpwZYY8PoSdlvR79+yw0aJU9hjd8stKsmzIxrmAQ6fw=="], + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-MAN0jEah3skK3+94QiikeLbuhnZ9GuW+JCjuIUpXGUvJG2U4N8sscypWDMUJUkKc6DYJ0UaGI5Xvb17crXwyiw=="], - "@smithy/util-middleware": ["@smithy/util-middleware@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-TrAgOcL63TRi7G92arTzq0n+VDrmZifwP1I1T9y2xU3lJpybsHdm33S2d3xaFfG0c8zJNIF9yYRqLSe6rbhH/A=="], + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.6.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-7dFFv+DA2Ln8s60s+JIRHHfl/Fch8HZ3FY9VjnM6I3EtigDP+bUUexNQZuexVYJXUejpQ48/JIHSiSmugFDD/A=="], - "@smithy/util-retry": ["@smithy/util-retry@4.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-E/kFnvWQL6rIPr0Ucjk8oDgJSkKx2bv0nJkJ/cB3ywys7xCqeL1AXP9liHjgYONdQ+MKw/xT06IQK3vgbtu2Ww=="], + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-x9/taZ/r/nEL6SBfdV+M18VpJU16yzehn2q1XHNmXWEp3g5U4NBl2oMBVw7YKdmH2HiKQ/ZqDW5JjBDNzgLKrg=="], - "@smithy/util-stream": ["@smithy/util-stream@4.6.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-g+hQ45sPnaIDU4CnaG8EufmeWwziQlcpIvPG6hVY7v65RcUgasM63J/WNfSsXEcZ1zFu9rS/r/qqfDxkIrQtDw=="], + "@smithy/util-middleware": ["@smithy/util-middleware@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-VuChjNaV9VteddlugHsGqFXgjQK8WIt9JxYAtdsi1i8A1PwrVAVC6DVVRqr9XbGeWnHvvWb0ywWLMjYUZwiHYg=="], + + "@smithy/util-retry": ["@smithy/util-retry@4.5.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-oM/8/5H9aG1omF2CzDxE7oxiJQhkonS2Vz1Kd6/76IhvPzCoroA3RVhr0EhAhZmyY6th3WfLK0OpRrphJ3YbgQ=="], + + "@smithy/util-stream": ["@smithy/util-stream@4.7.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-r/r4fqkU5fkJQaGpTrVssAHuPIwDSHrwz1KQ3rb1riYkL71OsYciHun1i0PWrJLLt+nPvroHXcaz39RdNW3XBw=="], "@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.5.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-fbrKxazlmJDe70WVjM6LOiNA2IKiz8lzsF1rHoIiEj9ONf5uQTgN1srg8X+AveJHGYavqEhsDEGZP7gM1k43Rg=="], + "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], + "@solid-primitives/bounds": ["@solid-primitives/bounds@0.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/resize-observer": "^2.1.3", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UbiyKMdSPmtijcEDnYLQL3zzaejpwWDAJJ4Gt5P0hgVs6A72piov0GyNw7V2SroH7NZFwxlYS22YmOr8A5xc1Q=="], + "@solid-primitives/event-bus": ["@solid-primitives/event-bus@1.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-l+n10/51neGcMaP3ypYt21bXfoeWh8IaC8k7fYuY3ww2a8S1Zv2N2a7FF5Qn+waTu86l0V8/nRHjkyqVIZBYwA=="], "@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.5", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-nwRV558mIabl4yVAhZKY8cb6G+O1F0M6Z75ttTu5hk+SxdOnKSGj+eetDIu7Oax1P138ZdUU01qnBPR8rnxaEA=="], @@ -1946,9 +2402,9 @@ "@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/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=="], + "@solid-primitives/props": ["@solid-primitives/props@3.2.4", "", { "dependencies": { "@solid-primitives/utils": "^6.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-MXXdvi2TSB6d+0N6ueA/HP1j/Kh9SEc4WdF1ZDMLwPagxW6pIHHSS9xbRO0RjqSVpNP4j0nxCWT1hx3CkJNhNA=="], - "@solid-primitives/refs": ["@solid-primitives/refs@1.1.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-aam02fjNKpBteewF/UliPSQCVJsIIGOLEWQOh+ll6R/QePzBOOBMcC4G+5jTaO75JuUS1d/14Q1YXT3X0Ow6iA=="], + "@solid-primitives/refs": ["@solid-primitives/refs@1.1.4", "", { "dependencies": { "@solid-primitives/utils": "^6.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-bLjwIs6ZPu8NQnuw04sU3Zc8qKSpbc0umUU/O4SHf6oWOdO4+dHY8vb1T7C4b/Tg103+9WGr6sEE0+NFlbaB/A=="], "@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/static-store": "^0.1.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AiyTknKcNBaKHbcSMuxtSNM8FjIuiSuFyFghdD0TcCMU9hKi9EmsC5pjfjDwxE+5EueB1a+T/34PLRI5vbBbKw=="], @@ -1956,11 +2412,11 @@ "@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA=="], - "@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-uxez7SXnr5GiRnzqO2IEDjOJRIXaG+0LZLBizmUA1FwSi+hrpuMzVBwyk70m4prcl8X6FDDXUl9O8hSq8wHbBQ=="], + "@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.4", "", { "dependencies": { "@solid-primitives/utils": "^6.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-LgtVaVBtB7EbmS4+M0b8xY5Iq6pUWXBsIC4VgtrFKDGDdyCaDt88sHk0fUlx1Enxm/XZnZyLXJABRoa39RjJqA=="], - "@solid-primitives/trigger": ["@solid-primitives/trigger@1.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Za2JebEiDyfamjmDwRaESYqBBYOlgYGzB8kHYH0QrkXyLf2qNADlKdGN+z3vWSLCTDcKxChS43Kssjuc0OZhng=="], + "@solid-primitives/trigger": ["@solid-primitives/trigger@1.2.4", "", { "dependencies": { "@solid-primitives/utils": "^6.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ju0e+ZOD7hpOp7nptJimvDSZHWFvIvF9iBWMvuwt30smX7c5wmB8Kmc0AdDuv0wOTi06PQ1J5IrP1WJbH2yUBQ=="], - "@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="], + "@solid-primitives/utils": ["@solid-primitives/utils@6.4.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ISSB5QX1qP2ynrheIpYwc4oKR5Ny4siNuUyf1qZniy+Il+p/PtDB0QK1Dnle8noiHpwRD3gpPdubOC3qI/Zamg=="], "@solidjs/meta": ["@solidjs/meta@0.29.4", "", { "peerDependencies": { "solid-js": ">=1.8.4" } }, "sha512-zdIWBGpR9zGx1p1bzIPqF5Gs+Ks/BH8R6fWhmUa/dcK1L2rUC8BAcZJzNRYBQv74kScf1TSOs0EY//Vd/I0V8g=="], @@ -1970,79 +2426,79 @@ "@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.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], "@storybook/addon-a11y": ["@storybook/addon-a11y@10.2.10", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.2.10" } }, "sha512-1S9pDXgvbHhBStGarCvfJ3/rfcaiAcQHRhuM3Nk4WGSIYtC1LCSRuzYdDYU0aNRpdCbCrUA7kUCbqvIE3tH+3Q=="], "@storybook/addon-docs": ["@storybook/addon-docs@10.2.10", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.2.10", "@storybook/icons": "^2.0.1", "@storybook/react-dom-shim": "10.2.10", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.2.10" } }, "sha512-2wIYtdvZIzPbQ5194M5Igpy8faNbQ135nuO5ZaZ2VuttqGr+IJcGnDP42zYwbAsGs28G8ohpkbSgIzVyJWUhPQ=="], - "@storybook/addon-links": ["@storybook/addon-links@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.4.4" }, "optionalPeers": ["@types/react", "react"] }, "sha512-sWydPWLgduT24p/NJ/hXHcHsPlAyzQP+cOtCGliSI989K9yBP/TOL3A8sz7LIDfukI9DVAsylPhJ1jDSiAEI1w=="], + "@storybook/addon-links": ["@storybook/addon-links@10.5.7", "", { "dependencies": { "@storybook/global": "^5.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.5.7" }, "optionalPeers": ["@types/react", "react"] }, "sha512-17PxEOocLhAEaPeQ4q+8yul/LF9YEIePS1arknCAS7U1pQXTe0uj+R0pB6uPLVflM5gECQMiP4WzIj4tEiL6+A=="], - "@storybook/addon-onboarding": ["@storybook/addon-onboarding@10.4.4", "", { "peerDependencies": { "storybook": "^10.4.4" } }, "sha512-ZTWGm8VXQUTepV4aEmIgHxdY7JMtn57H3uYnM3HD+qR8fmAcpLPoJ9ffXaMWUwsjK6SeferQyDTRN3q3Jd5+mg=="], + "@storybook/addon-onboarding": ["@storybook/addon-onboarding@10.5.7", "", { "peerDependencies": { "storybook": "^10.5.7" } }, "sha512-GDssSoWmmGnz6OnUvAofMcrUuTMD53Y2rnbshjXvhtPnjKw4dufXFjo6C7yZQ6vyoNzTS9HyQwCHlxlhyCKJjQ=="], "@storybook/addon-themes": ["@storybook/addon-themes@10.2.10", "", { "dependencies": { "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.2.10" } }, "sha512-j7ixCgzpWeTU7K4BkNHtEg3NdmRg9YW7ynvv0OjD3vaz4+FUVWOq7PPwb3SktLS1tOl4UA13IpApD8nSpBiY6A=="], - "@storybook/addon-vitest": ["@storybook/addon-vitest@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2" }, "peerDependencies": { "@vitest/browser": "^3.0.0 || ^4.0.0", "@vitest/browser-playwright": "^4.0.0", "@vitest/runner": "^3.0.0 || ^4.0.0", "storybook": "^10.4.4", "vitest": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["@vitest/browser", "@vitest/browser-playwright", "@vitest/runner", "vitest"] }, "sha512-VPpBwf1Elr+0g33am8ZE6aHhLB+r1TPxUsnDuCVNhxGjRxMFyQkAE8+jPJFPvS/YIUGMbVXarzaV7PcI/sJuVQ=="], + "@storybook/addon-vitest": ["@storybook/addon-vitest@10.5.7", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2" }, "peerDependencies": { "@vitest/browser": "^3.0.0 || ^4.0.0", "@vitest/browser-playwright": "^4.0.0", "@vitest/runner": "^3.0.0 || ^4.0.0", "storybook": "^10.5.7", "vitest": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["@vitest/browser", "@vitest/browser-playwright", "@vitest/runner", "vitest"] }, "sha512-7NK7Kzazc2vb2h8nGlH15QlUn5J6/LV0xF70VziAK0bxu3r1JupLCoBvckX39vHzFXiAZljXZtt1Wq/OidBxow=="], - "@storybook/builder-vite": ["@storybook/builder-vite@10.4.4", "", { "dependencies": { "@storybook/csf-plugin": "10.4.4", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.4.4", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-VyuZ4mEvhhVXjJa1qXMWKH8ohnas0rgEuJDf6u4aJ54XeENFebPUEAHde1Qo2PflJ4rUdVdXieOZzKbYwP5RAQ=="], + "@storybook/builder-vite": ["@storybook/builder-vite@10.5.7", "", { "dependencies": { "@storybook/csf-plugin": "10.5.7", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.5.7", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-fShF/aQaITqcJuMCLr42BGNUAbhDi4IboqvlbZqXAwgrrTslnZEUnY8GcEcvpZmjl11VwlmazhMJdH50fIgBPg=="], "@storybook/csf-plugin": ["@storybook/csf-plugin@10.2.10", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.2.10", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-aFvgaNDAnKMjuyhPK5ialT22pPqMN0XfPBNPeeNVPYztngkdKBa8WFqF/umDd47HxAjebq+vn6uId1xHyOHH3g=="], "@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="], - "@storybook/icons": ["@storybook/icons@2.0.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-KZBCpXsshAIjczYNXR/rlxEtCUX/eAbpFNwKi8bcOomrLA4t/SyPz5RF+lVPO2oZBUE4sAkt43mfJUevQDSEEw=="], + "@storybook/icons": ["@storybook/icons@2.1.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg=="], "@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.2.10", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.2.10" } }, "sha512-TmBrhyLHn8B8rvDHKk5uW5BqzO1M1T+fqFNWg88NIAJOoyX4Uc90FIJjDuN1OJmWKGwB5vLmPwaKBYsTe1yS+w=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], - "@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="], + "@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "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-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="], - "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.0", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "postcss": "^8.5.10", "tailwindcss": "4.3.0" } }, "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w=="], + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.3", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "postcss": "^8.5.16", "tailwindcss": "4.3.3" } }, "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg=="], "@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="], "@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=="], + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.10.0", "", { "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" }, "peerDependencies": { "@testing-library/dom": ">=10 <11" } }, "sha512-HQwu0KaB2zyT0iLzBL+8CLyZDL3KlZlZJ+2iyc9uCUnlJVskJU/UlPuVCyIPhtukjPQdT2QNoR5nCP5FqTmmDQ=="], - "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], + "@testing-library/user-event": ["@testing-library/user-event@14.6.3", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g=="], - "@textlint/ast-node-types": ["@textlint/ast-node-types@15.7.1", "", {}, "sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA=="], + "@textlint/ast-node-types": ["@textlint/ast-node-types@15.8.0", "", {}, "sha512-5CiH9COYmovWmExQgs7763DzX6Gy9zjkjJ7JxCC95wyTcjwQn/8poNF6fv3qzRlmx8CRRde8DHr9FcgAAiPzgw=="], - "@textlint/linter-formatter": ["@textlint/linter-formatter@15.7.1", "", { "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", "@textlint/module-interop": "15.7.1", "@textlint/resolver": "15.7.1", "@textlint/types": "15.7.1", "chalk": "^4.1.2", "debug": "^4.4.3", "js-yaml": "^4.1.1", "lodash": "^4.18.1", "pluralize": "^2.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "table": "^6.9.0", "text-table": "^0.2.0" } }, "sha512-TdwZ/debWYFD05K3CcoHtwvnCrza29wZxD+BjDTk/V5N7iRqkK1dTTHSD4A8AIgROLiDkHJmIKQbasbmsg8AvA=="], + "@textlint/linter-formatter": ["@textlint/linter-formatter@15.8.0", "", { "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", "@textlint/module-interop": "15.8.0", "@textlint/resolver": "15.8.0", "@textlint/types": "15.8.0", "debug": "^4.4.3", "js-yaml": "^4.3.0", "lodash": "^4.18.1", "pluralize": "^2.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "table": "^6.9.0", "text-table": "^0.2.0" } }, "sha512-+oU3A235NATv6Lzi4xa4kJ65PuNJlIxesaO4AvDhDWA9FWm7y4XKWaoQCW1esgaQQ6dwnUiFKArQ8TcJ86mC4w=="], - "@textlint/module-interop": ["@textlint/module-interop@15.7.1", "", {}, "sha512-Jg+sQW2L/cRJypk59wtcMUVVpt8vmit5ZMT3gUnFwevP3A6Qp1HfOtUy9ObT4hBX3lOSGT/ekcCDxR1pL7uH1g=="], + "@textlint/module-interop": ["@textlint/module-interop@15.8.0", "", {}, "sha512-rt+OR1WYGoLOY8HkA/aBPrqufF6yUUEsKEAh7XohTsT3lp9IyZFT6zOIbjul9P4FAzsmSPkcrYjVx3Bz/IUfkg=="], - "@textlint/resolver": ["@textlint/resolver@15.7.1", "", {}, "sha512-8XnO0pgF6mXnm41VvWmBbEIdGPhiCUt31uLZkOis1ECeg/1SoUcIT6Mx/F0e1rukq8l0UlOSeY9a31CsvRMK0g=="], + "@textlint/resolver": ["@textlint/resolver@15.8.0", "", {}, "sha512-E88tzfX3K8Jykk+38aJ9cy8RquD8ABVOPTO2rFEESq0wcg8x6/ypdAS8ZgR7OKiGqlRF0hkO/m5PbQwVfKM3VA=="], - "@textlint/types": ["@textlint/types@15.7.1", "", { "dependencies": { "@textlint/ast-node-types": "15.7.1" } }, "sha512-Vye/GmFNBTgVzZFtIFJTmLB+s2A7oIADxNG6r9UhfPuY+Czv0z5G3xeyFZZudPlfxURsKUyPIU5XsjOFqVp33A=="], + "@textlint/types": ["@textlint/types@15.8.0", "", { "dependencies": { "@textlint/ast-node-types": "15.8.0" } }, "sha512-Anhc6y5736YIsvqae0U6k0YmB2M/QVHkEeOv2aydAn/WIkdI69dCOiDbe3/+RagS3qstFTSFWJzNRA2lUjv19w=="], "@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="], @@ -2056,19 +2512,19 @@ "@tufjs/models": ["@tufjs/models@4.1.0", "", { "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^10.1.1" } }, "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww=="], - "@turbo/darwin-64": ["@turbo/darwin-64@2.9.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-t7QiPflaEyBE4oayeZtSmu4mEfjgIrcNlNNl1z1dmIVPqEdtA7+CfTf8d7KXsOGPh6aNgWjKxyvQg9uGfDQF+A=="], + "@turbo/darwin-64": ["@turbo/darwin-64@2.10.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-wBM3ObqOWnKUDmg7QfUFDkDHPFUAJmrYlYqmEM8jMPAPA/I6wRJIbWimeQUqhOiQ8xPKhzyWM+xaiUP0wz8FEQ=="], - "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-d23147mC9BsCPA9mJ0h/ubcpbRgcJBXbcG3+Vq7YLhjz3IXuvQsJ1UXH8f4MD76ZjJ4m/E4aRdJV+MW88CDfbw=="], + "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.10.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/Cq0joWnuMjDPfhjbFP4sv+C/7gkQ415zlaO4XUzD5EZxbtrKgXKvuuydMvogG8GeUnN1aDltW71RlmEfpjbyw=="], - "@turbo/linux-64": ["@turbo/linux-64@2.9.14", "", { "os": "linux", "cpu": "x64" }, "sha512-P3ZKB5tuUDdDQWuAsACGUR1qv9W7BNWxdxqVJ0kZNuNNPRaVYTPPikLcp79+GiEcW3npsR+KyP38lnQiBc5aSA=="], + "@turbo/linux-64": ["@turbo/linux-64@2.10.2", "", { "os": "linux", "cpu": "x64" }, "sha512-mMsf5IIhiKuceEXNstd25IbadjBXZ0amxzFOqliEzJX6HyeeHdBQPVSY583PWqYDyqM/FB8d5ZjkthfBSeuH3Q=="], - "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZRTlzcUMrrPv9ZuDzRF9n60Ym13bKeG9jDB8WjxyLhWNzV+AJQN+zdpIk3NJYf2zQsGUm1mNar2P0elRzLw25g=="], + "@turbo/linux-arm64": ["@turbo/linux-arm64@2.10.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Wcng1i2kaKmXutmwxT9MUoYZvdaIekXAdlGr4+0TpgbhGLw7nDuEcRBFrxb5BbRoX1d1q8SpdRxLc45TvDZIdQ=="], - "@turbo/windows-64": ["@turbo/windows-64@2.9.14", "", { "os": "win32", "cpu": "x64" }, "sha512-exanwN6sIduZwykYeiTQj8kCmOhazP5WOz3bvXMcYtjhL6Z3iRWLewKrXCBq0bqwSP3iBMb/AerRCnHI4lx46A=="], + "@turbo/windows-64": ["@turbo/windows-64@2.10.2", "", { "os": "win32", "cpu": "x64" }, "sha512-SsNhM7Ho7EpAdwtrJKBOic9Hso23vu6Dp0gAfLOvUFjPzurr/sGQlXZEvr6z89ne4RDOypTwz5CBDrixpMKtXw=="], - "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-fVdCsnmYoKICsycbWuuGp6Jvi51/3G/UluFWuAUCvR8PIW5IJkAk5BM9UF8PSm0Q2IphWHFZjYEgjHsh3B9y/g=="], + "@turbo/windows-arm64": ["@turbo/windows-arm64@2.10.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gf+S7ICAdimT/n02bOuVWKvhHnct/HYjZg3oBNIz5hZ9ZyWHbQim9J3P5Qip8WpX0ksxF7eaBVziJCuLnjhqDg=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], @@ -2122,7 +2578,7 @@ "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], - "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], + "@types/d3-geo": ["@types/d3-geo@3.1.1", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w=="], "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], @@ -2134,7 +2590,7 @@ "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], - "@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="], + "@types/d3-random": ["@types/d3-random@3.0.4", "", {}, "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA=="], "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], @@ -2162,7 +2618,7 @@ "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], - "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="], "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], @@ -2172,6 +2628,8 @@ "@types/linkify-it": ["@types/linkify-it@3.0.5", "", {}, "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw=="], + "@types/luxon": ["@types/luxon@3.7.1", "", {}, "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg=="], + "@types/markdown-it": ["@types/markdown-it@12.2.3", "", { "dependencies": { "@types/linkify-it": "*", "@types/mdurl": "*" } }, "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], @@ -2206,15 +2664,15 @@ "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], - "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], + "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="], - "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/react-dom": ["@types/react-dom@19.2.4", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw=="], "@types/sarif": ["@types/sarif@2.1.7", "", {}, "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ=="], "@types/scheduler": ["@types/scheduler@0.26.0", "", {}, "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA=="], - "@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], + "@types/semver": ["@types/semver@7.8.0", "", {}, "sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ=="], "@types/ssri": ["@types/ssri@7.1.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-odD/56S3B51liILSk5aXJlnYt99S6Rt9EFDDqGtJM26rKHApHcwyU/UoYHrzKkdkHMAIquGWCuHtQTbes+FRQw=="], @@ -2224,7 +2682,7 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@types/vscode": ["@types/vscode@1.120.0", "", {}, "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA=="], + "@types/vscode": ["@types/vscode@1.125.0", "", {}, "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA=="], "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], @@ -2236,25 +2694,25 @@ "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.61.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.61.0", "@typescript-eslint/type-utils": "8.61.0", "@typescript-eslint/utils": "8.61.0", "@typescript-eslint/visitor-keys": "8.61.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.61.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.66.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.66.0", "@typescript-eslint/type-utils": "8.66.0", "@typescript-eslint/utils": "8.66.0", "@typescript-eslint/visitor-keys": "8.66.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.66.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.61.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.61.0", "@typescript-eslint/types": "8.61.0", "@typescript-eslint/typescript-estree": "8.61.0", "@typescript-eslint/visitor-keys": "8.61.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.66.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.66.0", "@typescript-eslint/types": "8.66.0", "@typescript-eslint/typescript-estree": "8.66.0", "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.61.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.61.0", "@typescript-eslint/types": "^8.61.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.66.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.66.0", "@typescript-eslint/types": "^8.66.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.61.0", "", { "dependencies": { "@typescript-eslint/types": "8.61.0", "@typescript-eslint/visitor-keys": "8.61.0" } }, "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.66.0", "", { "dependencies": { "@typescript-eslint/types": "8.66.0", "@typescript-eslint/visitor-keys": "8.66.0" } }, "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.61.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.66.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.61.0", "", { "dependencies": { "@typescript-eslint/types": "8.61.0", "@typescript-eslint/typescript-estree": "8.61.0", "@typescript-eslint/utils": "8.61.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.66.0", "", { "dependencies": { "@typescript-eslint/types": "8.66.0", "@typescript-eslint/typescript-estree": "8.66.0", "@typescript-eslint/utils": "8.66.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.61.0", "", {}, "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.66.0", "", {}, "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.61.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.61.0", "@typescript-eslint/tsconfig-utils": "8.61.0", "@typescript-eslint/types": "8.61.0", "@typescript-eslint/visitor-keys": "8.61.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.66.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.66.0", "@typescript-eslint/tsconfig-utils": "8.66.0", "@typescript-eslint/types": "8.66.0", "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.61.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.61.0", "@typescript-eslint/types": "8.61.0", "@typescript-eslint/typescript-estree": "8.61.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.66.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.66.0", "@typescript-eslint/types": "8.66.0", "@typescript-eslint/typescript-estree": "8.66.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.61.0", "", { "dependencies": { "@typescript-eslint/types": "8.61.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.66.0", "", { "dependencies": { "@typescript-eslint/types": "8.66.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg=="], "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260316.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260316.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260316.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260316.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260316.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260316.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260316.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260316.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-s+QGNx+3zxTZBuZw3oNOFlHqpbmg0cTgBd/b6SRZ5mo3vFChkhflYqRW2IvTvU9a3PPX3bQAkQ/gWbDZCmNC3Q=="], @@ -2272,9 +2730,11 @@ "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260316.1", "", { "os": "win32", "cpu": "x64" }, "sha512-LvpV1hyQS0U9yMLHgWexhC7oSeBpcNbIJtYC6Iyvu63Mb6J/cP0k2fQmnAVB2yesMMQFtuY6v2YIx17vE0Ymfw=="], - "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.6", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og=="], + "@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="], + + "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.8", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-bLMpVcWZNzq6lYOybwFwOAR1IXKcHnhUNqYeHjl1bET/qE3jFPFH+p8Wrh3rU4xwdnifPxmKNESBYnvnmc75aA=="], - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="], "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], @@ -2296,32 +2756,6 @@ "@vscode/codicons": ["@vscode/codicons@0.0.44", "", {}, "sha512-F7qPRumUK3EHjNdopfICLGRf3iNPoZQt+McTHAn4AlOWPB3W2kL4H0S7uqEqbyZ6rCxaeDjpAn3MCUnwTu/VJQ=="], - "@vscode/ripgrep": ["@vscode/ripgrep@1.18.0", "", { "optionalDependencies": { "@vscode/ripgrep-darwin-arm64": "1.18.0", "@vscode/ripgrep-darwin-x64": "1.18.0", "@vscode/ripgrep-linux-arm": "1.18.0", "@vscode/ripgrep-linux-arm64": "1.18.0", "@vscode/ripgrep-linux-ia32": "1.18.0", "@vscode/ripgrep-linux-ppc64": "1.18.0", "@vscode/ripgrep-linux-riscv64": "1.18.0", "@vscode/ripgrep-linux-s390x": "1.18.0", "@vscode/ripgrep-linux-x64": "1.18.0", "@vscode/ripgrep-win32-arm64": "1.18.0", "@vscode/ripgrep-win32-ia32": "1.18.0", "@vscode/ripgrep-win32-x64": "1.18.0" } }, "sha512-ns5lWe44tSfbTMbVUsyB+I1819PVSw4AdpgK0RNkzfWfwy6+3IUNSxwSrfTno1/oWaS/hERNz+XLWVyga2aJBQ=="], - - "@vscode/ripgrep-darwin-arm64": ["@vscode/ripgrep-darwin-arm64@1.18.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-r3ktHSvbFycQNF6sl7sNDPocpsI7J+mEzh1IaZFkY0spm3k2Z9t8hPAeOK7+p0l6p6/swkQC14XWX01low+94Q=="], - - "@vscode/ripgrep-darwin-x64": ["@vscode/ripgrep-darwin-x64@1.18.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-25b4gWbL138dGuQU244ebCKKc0q05ULBMoFSz9oAEUHNeqK/lOJViDS7DRvbDazzAzSEdan391Znks/R5mkaTQ=="], - - "@vscode/ripgrep-linux-arm": ["@vscode/ripgrep-linux-arm@1.18.0", "", { "os": "linux", "cpu": "arm" }, "sha512-GDAvufNDHu8zqLEmXstalQF0Wh6wQvdsBi/Vg3Yi3CK4a8XoFXqqXVEHEZ9xQz3t0NfoSEc9JbvK9DDS6FxyxQ=="], - - "@vscode/ripgrep-linux-arm64": ["@vscode/ripgrep-linux-arm64@1.18.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-lQ/5zTG++U0E3IhVgS4EPTTn/U4okncaRMM5GOFfOYZywS4nuD31GhkHbNYlDk5CuDC68+hYJ0/eQeyCKJDA+g=="], - - "@vscode/ripgrep-linux-ia32": ["@vscode/ripgrep-linux-ia32@1.18.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-YWLkSUtFd4Jh5EepIhA9RJSfv3uMAVMo+2rBIGHPBnvgLrZciIs2cDKei1/p6Wc/aCzUoHyMAg2R6tw4ZCBKGg=="], - - "@vscode/ripgrep-linux-ppc64": ["@vscode/ripgrep-linux-ppc64@1.18.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-quXVY8fwQ8O/lvU1yrSqSl3jlUzysRSb+AfUfCL/tRtphxsKlFvPAejryZ6vg4Bgvn8XL74xb4qMCDmWgYrT5w=="], - - "@vscode/ripgrep-linux-riscv64": ["@vscode/ripgrep-linux-riscv64@1.18.0", "", { "os": "linux", "cpu": "none" }, "sha512-f5kBQBrWfQt8Q7OhSORuNDei5dkYagBj3y4jImSUXGMy8B/Ke7SltSRcUtjPv166FAFfHCAmWuZp3+cWnX2/Vw=="], - - "@vscode/ripgrep-linux-s390x": ["@vscode/ripgrep-linux-s390x@1.18.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-rTOcJFGGcl2c07RUOWUo4U1ndnemKhY6A9hnMB18uk7jSgJc0d/QLBGWMWpumdtoJtpizn/wIv5mXIisJukusQ=="], - - "@vscode/ripgrep-linux-x64": ["@vscode/ripgrep-linux-x64@1.18.0", "", { "os": "linux", "cpu": "x64" }, "sha512-mQ3bVrUpnD2vs7QT0vX90Lt0cnUq467uFtEktIdsJJmW296RoSULRGqWgzG1AKxyBpNDD6l4ZO4qKf6SgyC23Q=="], - - "@vscode/ripgrep-win32-arm64": ["@vscode/ripgrep-win32-arm64@1.18.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-vfTIjq1OHnzUjxZcHVQAMbnggp8dpGf+0QKFOZHwWPqFwXxQC8eCWM+5NUdoJ6yrElCeMzoUTXoK/LdZaniB+Q=="], - - "@vscode/ripgrep-win32-ia32": ["@vscode/ripgrep-win32-ia32@1.18.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-//rfAE+BOw5AC2EMmepmiE36jUuevtQYNQqqlw1s3m9FlRxjxEut97RkRPHAu9BG4mSojatZx+kXZXNdyI9caQ=="], - - "@vscode/ripgrep-win32-x64": ["@vscode/ripgrep-win32-x64@1.18.0", "", { "os": "win32", "cpu": "x64" }, "sha512-KNPvtElldqILHdnAetujPaowkNbpqJy3ssIGGN6F6Kve9Qi+nNLI2DN01O83JjCEVQbCzl8Ov3QZ9Eov3BR8Dg=="], - "@vscode/test-cli": ["@vscode/test-cli@0.0.12", "", { "dependencies": { "@types/mocha": "^10.0.10", "c8": "^10.1.3", "chokidar": "^3.6.0", "enhanced-resolve": "^5.18.3", "glob": "^10.3.10", "minimatch": "^9.0.3", "mocha": "^11.7.4", "supports-color": "^10.2.2", "yargs": "^17.7.2" }, "bin": { "vscode-test": "out/bin.mjs" } }, "sha512-iYN0fDg29+a2Xelle/Y56Xvv7Nc8Thzq4VwpzAF/SIE6918rDicqfsQxV6w1ttr2+SOm+10laGuY9FG2ptEKsQ=="], "@vscode/test-electron": ["@vscode/test-electron@2.5.2", "", { "dependencies": { "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.5", "jszip": "^3.10.1", "ora": "^8.1.0", "semver": "^7.6.2" } }, "sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg=="], @@ -2376,7 +2810,7 @@ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -2388,7 +2822,7 @@ "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": ["ai@6.0.235", "", { "dependencies": { "@ai-sdk/gateway": "3.0.157", "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.40", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Uyeuvowo20XChpFC0lUaQq8xf0Kg7OElA56fEjUYoeWMJIzqshDTz4S57KXobcJ4m+iDQxMeJoUj06CIxSLx7A=="], "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=="], @@ -2406,10 +2840,16 @@ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], - "anynum": ["anynum@1.0.0", "", {}, "sha512-xjR9/zBVnUOP6ztMIIgShjsxui80nQUQH+5xJnvrYLs+90bF25/KJqaAi8mk+B4RDtX1Nspi6fmp4YTEts8SfA=="], + "anynum": ["anynum@1.0.1", "", {}, "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A=="], "apache-arrow": ["apache-arrow@18.1.0", "", { "dependencies": { "@swc/helpers": "^0.5.11", "@types/command-line-args": "^5.2.3", "@types/command-line-usage": "^5.0.4", "@types/node": "^20.13.0", "command-line-args": "^5.2.1", "command-line-usage": "^7.0.1", "flatbuffers": "^24.3.25", "json-bignum": "^0.0.3", "tslib": "^2.6.2" }, "bin": { "arrow2csv": "bin/arrow2csv.js" } }, "sha512-v/ShMp57iBnBp4lDgV8Jx3d3Q5/Hac25FWmQ98eMahUiHPXcvwIMKJD0hBIgclm/FCG+LwPkAKtkRO1O/W0YGg=="], + "archiver": ["archiver@7.0.1", "", { "dependencies": { "archiver-utils": "^5.0.2", "async": "^3.2.4", "buffer-crc32": "^1.0.0", "readable-stream": "^4.0.0", "readdir-glob": "^1.1.2", "tar-stream": "^3.0.0", "zip-stream": "^6.0.1" } }, "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ=="], + + "archiver-utils": ["archiver-utils@5.0.2", "", { "dependencies": { "glob": "^10.0.0", "graceful-fs": "^4.2.0", "is-stream": "^2.0.1", "lazystream": "^1.0.0", "lodash": "^4.17.15", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA=="], + + "arctic": ["arctic@2.3.4", "", { "dependencies": { "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", "@oslojs/jwt": "0.2.0" } }, "sha512-+p30BOWsctZp+CVYCt7oAean/hWGW42sH5LAcRQX56ttEkFJWbzXBhmSpibbzwSJkRrotmsA+oAoJoVsU0f5xA=="], + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], @@ -2432,7 +2872,7 @@ "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], - "async-lock": ["async-lock@1.4.1", "", {}, "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="], + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], "async-mutex": ["async-mutex@0.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA=="], @@ -2440,22 +2880,22 @@ "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], - "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=="], + "autoprefixer": ["autoprefixer@10.5.4", "", { "dependencies": { "browserslist": "^4.28.6", "caniuse-lite": "^1.0.30001806", "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-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA=="], "ava": ["ava@5.3.1", "", { "dependencies": { "acorn": "^8.8.2", "acorn-walk": "^8.2.0", "ansi-styles": "^6.2.1", "arrgv": "^1.0.2", "arrify": "^3.0.0", "callsites": "^4.0.0", "cbor": "^8.1.0", "chalk": "^5.2.0", "chokidar": "^3.5.3", "chunkd": "^2.0.1", "ci-info": "^3.8.0", "ci-parallel-vars": "^1.0.1", "clean-yaml-object": "^0.1.0", "cli-truncate": "^3.1.0", "code-excerpt": "^4.0.0", "common-path-prefix": "^3.0.0", "concordance": "^5.0.4", "currently-unhandled": "^0.4.1", "debug": "^4.3.4", "emittery": "^1.0.1", "figures": "^5.0.0", "globby": "^13.1.4", "ignore-by-default": "^2.1.0", "indent-string": "^5.0.0", "is-error": "^2.2.2", "is-plain-object": "^5.0.0", "is-promise": "^4.0.0", "matcher": "^5.0.0", "mem": "^9.0.2", "ms": "^2.1.3", "p-event": "^5.0.1", "p-map": "^5.5.0", "picomatch": "^2.3.1", "pkg-conf": "^4.0.0", "plur": "^5.1.0", "pretty-ms": "^8.0.0", "resolve-cwd": "^3.0.0", "stack-utils": "^2.0.6", "strip-ansi": "^7.0.1", "supertap": "^3.0.1", "temp-dir": "^3.0.0", "write-file-atomic": "^5.0.1", "yargs": "^17.7.2" }, "peerDependencies": { "@ava/typescript": "*" }, "optionalPeers": ["@ava/typescript"], "bin": { "ava": "entrypoints/cli.mjs" } }, "sha512-Scv9a4gMOXB6+ni4toLuhAm9KYWEjsgBglJl+kMGI5+IVDt120CCDZyB5HNU9DjmLI2t4I0GbnxGLmmRfGTJGg=="], - "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - - "avvio": ["avvio@9.2.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ=="], + "avvio": ["avvio@9.3.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A=="], "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], - "axe-core": ["axe-core@4.12.1", "", {}, "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA=="], + "axe-core": ["axe-core@4.13.0", "", {}, "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A=="], - "axios": ["axios@1.17.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw=="], + "axios": ["axios@1.19.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw=="], "azure-devops-node-api": ["azure-devops-node-api@12.5.0", "", { "dependencies": { "tunnel": "0.0.6", "typed-rest-client": "^1.8.4" } }, "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og=="], + "b4a": ["b4a@1.8.1", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw=="], + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.7", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ=="], "babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="], @@ -2464,9 +2904,19 @@ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "bare-events": ["bare-events@2.9.1", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg=="], + + "bare-fs": ["bare-fs@4.8.0", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q=="], + + "bare-path": ["bare-path@3.1.1", "", {}, "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ=="], + + "bare-stream": ["bare-stream@2.13.3", "", { "dependencies": { "b4a": "^1.8.1", "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-abort-controller", "bare-buffer", "bare-events"] }, "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ=="], + + "bare-url": ["bare-url@2.4.7", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-o8CRCiJtib+ycO3mE4A5UChtGX4dDP2XxsWVu9P+Zc3H8tcmKwNVEDoDTXmwN+uuMhfKeT7/i7Y26xS8W7ohoA=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.37", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.12", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA=="], "before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="], @@ -2476,6 +2926,8 @@ "bin-links": ["bin-links@6.0.2", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-frE1t78WOwJ45PKV2cF2tNPjTcs9L1J9s6VkrV59wanRP4GlaomuxYPVma7BwthMg8WnfSory4w5PTE6FZZ81w=="], + "binary": ["binary@0.3.0", "", { "dependencies": { "buffers": "~0.1.1", "chainsaw": "~0.1.0" } }, "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg=="], + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "binaryextensions": ["binaryextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw=="], @@ -2486,33 +2938,37 @@ "blueimp-md5": ["blueimp-md5@2.19.0", "", {}, "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w=="], - "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=="], + "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], "bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="], "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + "bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="], + "boundary": ["boundary@2.0.0", "", {}, "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA=="], "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], - "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browser-stdout": ["browser-stdout@1.3.1", "", {}, "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="], - "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + "browserslist": ["browserslist@4.28.7", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="], "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + "buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="], "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], "buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="], - "bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="], + "buffers": ["buffers@0.1.1", "", {}, "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="], + + "bun-ffi-structs": ["bun-ffi-structs@0.2.4", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-AJzsqoVFs1KBbJbWHIYrVZLDC3NhTqqh25awRXqzoLzmBAKr5oqk6+CwuYHAekKx+VBCYVohBoKuRq40dV+TYg=="], "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="], @@ -2528,8 +2984,6 @@ "cacache": ["cacache@20.0.4", "", { "dependencies": { "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", "glob": "^13.0.0", "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^13.0.0" } }, "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA=="], - "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], @@ -2538,7 +2992,7 @@ "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], - "caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="], + "caniuse-lite": ["caniuse-lite@1.0.30001807", "", {}, "sha512-daRXJ9EB/rdRgu7kV+TTl1YUKtlsMWblPl2sLnpg9DZae16QCegol6A1SmCE31Lm9mXC1sRWGt/krouH+/dl7Q=="], "cbor": ["cbor@8.1.0", "", { "dependencies": { "nofilter": "^3.1.0" } }, "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg=="], @@ -2546,6 +3000,8 @@ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chainsaw": ["chainsaw@0.1.0", "", { "dependencies": { "traverse": ">=0.3.0 <0.4" } }, "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "chalk-template": ["chalk-template@0.4.0", "", { "dependencies": { "chalk": "^4.1.2" } }, "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg=="], @@ -2556,6 +3012,8 @@ "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], + "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=="], "cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], @@ -2576,8 +3034,6 @@ "classcat": ["classcat@5.0.5", "", {}, "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="], - "clean-git-ref": ["clean-git-ref@2.0.1", "", {}, "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw=="], - "clean-stack": ["clean-stack@4.2.0", "", { "dependencies": { "escape-string-regexp": "5.0.0" } }, "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg=="], "clean-yaml-object": ["clean-yaml-object@0.1.0", "", {}, "sha512-3yONmlN9CSAkzNwnRCiJQ7Q2xK5mWuEfL3PuTZcAUzhObbXsfsnMptJzXwz93nc5zn9V9TwCVMmV7w4xsm43dw=="], @@ -2586,7 +3042,7 @@ "cli-sound": ["cli-sound@1.1.3", "", { "dependencies": { "find-exec": "^1.0.3" }, "bin": { "cli-sound": "dist/esm/cli.js" } }, "sha512-dpdF3KS3wjo1fobKG5iU9KyKqzQWAqueymHzZ9epus/dZ40487gAvS6aXFeBul+GiQAQYUTAtUWgQvw6Jftbyg=="], - "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], "cli-truncate": ["cli-truncate@3.1.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^5.0.0" } }, "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA=="], @@ -2620,12 +3076,14 @@ "command-line-usage": ["command-line-usage@7.0.4", "", { "dependencies": { "array-back": "^6.2.2", "chalk-template": "^0.4.0", "table-layout": "^4.1.1", "typical": "^7.3.0" } }, "sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg=="], - "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], + "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], "common-path-prefix": ["common-path-prefix@3.0.0", "", {}, "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w=="], + "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=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "concordance": ["concordance@5.0.4", "", { "dependencies": { "date-time": "^3.1.0", "esutils": "^2.0.3", "fast-diff": "^1.2.0", "js-string-escape": "^1.0.1", "lodash": "^4.17.15", "md5-hex": "^3.0.1", "semver": "^7.3.2", "well-known-symbols": "^2.0.0" } }, "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw=="], @@ -2646,7 +3104,7 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - "core-js": ["core-js@3.49.0", "", {}, "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg=="], + "core-js": ["core-js@3.50.0", "", {}, "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw=="], "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], @@ -2656,6 +3114,8 @@ "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], + "crc32-stream": ["crc32-stream@6.0.0", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^4.0.0" } }, "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g=="], + "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -2770,8 +3230,6 @@ "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], - "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], - "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], "defu": ["defu@6.1.6", "", {}, "sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug=="], @@ -2800,8 +3258,6 @@ "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], - "diff3": ["diff3@0.0.3", "", {}, "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g=="], - "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], "dingbat-to-unicode": ["dingbat-to-unicode@1.0.1", "", {}, "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w=="], @@ -2840,9 +3296,9 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "effect": ["effect@4.0.0-beta.66", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-4arEr62cziFa8BBVDUwJCJJmaVepXf/kRg7KtC0h8+bufngscrHbwWFhr9c+HonwOF+31U3iD3xUJmw9KzX7Dw=="], + "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=="], - "electron-to-chromium": ["electron-to-chromium@1.5.372", "", {}, "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.402", "", {}, "sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA=="], "emittery": ["emittery@1.2.1", "", {}, "sha512-sFz64DCRjirhwHLxofFqxYQm6DCp6o0Ix7jwKQvuCHPn4GMRZNuBZyLPu9Ccmk/QSCAMZt6FOUqA8JZCQvA9fw=="], @@ -2854,11 +3310,11 @@ "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "engine.io-client": ["engine.io-client@6.6.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-client": ["engine.io-client@6.6.6", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.21.0", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q=="], "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], - "enhanced-resolve": ["enhanced-resolve@5.24.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ=="], + "enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="], "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], @@ -2872,13 +3328,13 @@ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], + "es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - "es-toolkit": ["es-toolkit@1.47.1", "", {}, "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q=="], + "es-toolkit": ["es-toolkit@1.50.0", "", {}, "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w=="], "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], @@ -2890,7 +3346,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], + "eslint": ["eslint@9.39.5", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.6", "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw=="], "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], @@ -2918,6 +3374,8 @@ "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], @@ -2926,15 +3384,15 @@ "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=="], + "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], "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=="], + "express-rate-limit": ["express-rate-limit@8.6.2", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A=="], - "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + "exsolve": ["exsolve@1.1.1", "", {}, "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], @@ -2942,7 +3400,7 @@ "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], - "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=="], @@ -2950,25 +3408,27 @@ "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], + "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - "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-json-stringify": ["fast-json-stringify@7.0.1", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^4.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA=="], "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], "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=="], + "fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="], - "fast-xml-builder": ["fast-xml-builder@1.2.0", "", { "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" } }, "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q=="], + "fast-xml-builder": ["fast-xml-builder@1.3.0", "", { "dependencies": { "path-expression-matcher": "^1.6.2", "xml-naming": "^0.3.0" } }, "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ=="], - "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=="], + "fast-xml-parser": ["fast-xml-parser@5.10.1", "", { "dependencies": { "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", "is-unsafe": "^2.0.0", "path-expression-matcher": "^1.6.2", "strnum": "^2.4.1", "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw=="], "fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="], - "fastify": ["fastify@5.8.5", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^9.14.0 || ^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q=="], + "fastify": ["fastify@5.11.2", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^7.0.0", "find-my-way": "^9.6.0", "light-my-request": "^6.0.0", "pino": "^9.14.0 || ^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-i/eJG7nXR9OkbFgoX4jFiPOHoRq0rXqDACVAXELh5Fdg6BFBErIVZ8MyibGCwup9Go1pYrxZJ0ogIub8vVdEcQ=="], "fastify-plugin": ["fastify-plugin@5.1.0", "", {}, "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw=="], @@ -2980,7 +3440,7 @@ "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], - "fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="], + "fflate": ["fflate@0.4.9", "", {}, "sha512-zdxgIEddhfsyCaWpJ2SdXEP8ZMrKJ6+5jl4OupODcywU0IhRk6gdXuVGcPICyfx2H97hVK7xmJtRLPjkxAX8Vw=="], "figures": ["figures@5.0.0", "", { "dependencies": { "escape-string-regexp": "^5.0.0", "is-unicode-supported": "^1.2.0" } }, "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg=="], @@ -2994,7 +3454,7 @@ "find-exec": ["find-exec@1.0.3", "", { "dependencies": { "shell-quote": "^1.8.1" } }, "sha512-gnG38zW90mS8hm5smNcrBnakPEt+cGJoiMkJwCU0IYnEb0H2NQk0NIljhNW+48oniCriFek/PH6QXbwsJo/qug=="], - "find-my-way": ["find-my-way@9.6.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ=="], + "find-my-way": ["find-my-way@9.7.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ=="], "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], @@ -3008,11 +3468,9 @@ "flatbuffers": ["flatbuffers@24.12.23", "", {}, "sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA=="], - "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + "flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="], - "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], - - "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + "follow-redirects": ["follow-redirects@1.16.0", "", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], @@ -3030,7 +3488,7 @@ "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], - "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=="], + "framer-motion": ["framer-motion@12.43.0", "", { "dependencies": { "motion-dom": "^12.43.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-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -3050,9 +3508,9 @@ "fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="], - "gaxios": ["gaxios@7.1.5", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg=="], + "gaxios": ["gaxios@7.3.0", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-RB5vLV+vvQeoFPCX4QMK6/hjVkbIamPp1QSUD0CiZcnj12qbpiL+pLbYtgD+oZkWl0tl9z+o2Utp+MpM3QRhBA=="], - "gcp-metadata": ["gcp-metadata@8.1.3", "", { "dependencies": { "gaxios": "7.1.3", "google-logging-utils": "1.1.3", "json-bigint": "^1.0.0" } }, "sha512-ziTrzUhhpL9Zk5k0HHzgP/KIpWDJT0VMBC/ynt/QIBvTW+UUcSivQRl6VlwTf/EilDxtSWklHoRsKy1c4k+59w=="], + "gcp-metadata": ["gcp-metadata@8.1.4", "", { "dependencies": { "gaxios": "7.1.3", "google-logging-utils": "1.1.3", "json-bigint": "^1.0.0" } }, "sha512-iJ9KMsiu+xKtNRX0PmGLSaIU3bUBAyzWTyqKemKPzNPsmmsBCQYmlNg+brEbES7IHSXtdVwzBPzx1vz3FAaipw=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], @@ -3066,7 +3524,7 @@ "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], - "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], + "get-tsconfig": ["get-tsconfig@4.14.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A=="], "ghostty-web": ["ghostty-web@0.4.0", "", {}, "sha512-0puDBik2qapbD/QQBW9o5ZHfXnZBqZWx/ctBiVtKZ6ZLds4NYb+wZuw1cRLXZk9zYovIQ908z3rvFhexAvc5Hg=="], @@ -3074,7 +3532,7 @@ "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], - "gitlab-ai-provider": ["gitlab-ai-provider@6.8.0", "", { "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-KwHASXkHtDcgrzTXZVp9Dyx6t8m9nK0R2fCm47MWcxxQ1kOBt3f2LZugtu1kOby8i4Sbd+kvBSYM66PGkDclng=="], + "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=="], "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], @@ -3086,7 +3544,7 @@ "google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="], - "google-logging-utils": ["google-logging-utils@1.1.4", "", {}, "sha512-LXxE6ND+JHhDPYtPFt3oeWrjxFPrnRZCZ4At6lpbi1ZDSAN7bmGET9s53vvARbxT93p+xpeDUbc/nCR4C+7vcw=="], + "google-logging-utils": ["google-logging-utils@1.2.0", "", {}, "sha512-WE9av4wKDZgRjBwgVUabocx8T6/7o3Ca1Fat46FXDhXVAFibzNadedcOXrdgd1Kzmk8tsk/9ZH89Wyf/SqeZ3A=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], @@ -3102,12 +3560,10 @@ "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], - "happy-dom": ["happy-dom@20.10.3", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-Hjdiy8RziuCcn5z04QI/rlsNuQoG8P0xxjgvsSMpi89cvIXIOcucQtiHS1yHSShxoBcSCeYqAskINmTiy/mlfw=="], + "happy-dom": ["happy-dom@20.11.1", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-XSt8tMzbW9ymE7687xztkO1ckR7qJNQ3LywY9vlYGhGi3zXrGBHuUo2Cl1ztZaICW+1eAGdkLbj6iwVqDT33kg=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], @@ -3120,6 +3576,8 @@ "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + "heap-snapshot-toolkit": ["heap-snapshot-toolkit@1.1.3", "", {}, "sha512-joThu2rEsDu8/l4arupRDI1qP4CZXNG+J6Wr348vnbLGSiBkwRdqZ6aOHl5BzEiC+Dc8OTbMlmWjD0lbXD5K2Q=="], + "hono": ["hono@4.12.12", "", {}, "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q=="], "hono-openapi": ["hono-openapi@1.1.2", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-toUcO60MftRBxqcVyxsHNYs2m4vf4xkQaiARAucQx3TiBPDtMNNkoh+C4I1vAretQZiGyaLOZNWn1YxfSyUA5g=="], @@ -3176,13 +3634,13 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + "ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="], "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], "ioredis": ["ioredis@5.11.1", "", { "dependencies": { "@ioredis/commands": "1.10.0", "cluster-key-slot": "1.1.1", "debug": "4.4.3", "denque": "2.1.0", "redis-errors": "1.2.0", "redis-parser": "3.0.0", "standard-as-callback": "2.1.0" } }, "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A=="], - "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], + "ip-address": ["ip-address@10.4.0", "", {}, "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ=="], "ipaddr.js": ["ipaddr.js@2.4.0", "", {}, "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ=="], @@ -3190,8 +3648,6 @@ "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], - "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], - "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], @@ -3226,10 +3682,10 @@ "is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw=="], - "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], - "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + "is-unsafe": ["is-unsafe@2.0.0", "", {}, "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA=="], + "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], @@ -3242,8 +3698,6 @@ "isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], - "isomorphic-git": ["isomorphic-git@1.38.4", "", { "dependencies": { "async-lock": "^1.4.1", "clean-git-ref": "^2.0.1", "crc-32": "^1.2.0", "diff3": "0.0.3", "ignore": "^5.1.4", "minimisted": "^2.0.0", "pako": "^1.0.10", "pify": "^4.0.1", "readable-stream": "^4.0.0", "sha.js": "^2.4.12", "simple-get": "^4.0.1" }, "bin": { "isogit": "cli.cjs" } }, "sha512-Ud5vs6Ac+ET+iOZWZB1j2RruVeGQSQc7U7QUhPq6iGqzifaqOVHCgRpG/8c0LwIP39R+Mr+lzR4escmCuhjONQ=="], - "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], @@ -3258,9 +3712,9 @@ "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + "jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], - "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], + "js-base64": ["js-base64@3.9.2", "", {}, "sha512-6zayE8QlUdiweYI6cETD/XBSqFcoCUlufn/29PJR99r82x1yDnIprRca0YvAYpAW+ez0GuQkVBC6xG5QkD7OjA=="], "js-string-escape": ["js-string-escape@1.0.1", "", {}, "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg=="], @@ -3268,7 +3722,7 @@ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], + "js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="], "jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="], @@ -3296,7 +3750,7 @@ "json-stringify-nice": ["json-stringify-nice@1.1.4", "", {}, "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw=="], - "json-with-bigint": ["json-with-bigint@3.5.8", "", {}, "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw=="], + "json-with-bigint": ["json-with-bigint@3.5.10", "", {}, "sha512-Vcx+JVNEBts/xfcoCS69sKrOhOk/3TVlvlT+XzUOefVKnnrbYSCKpDCm10pohsJFtsJVYnwa/cXRZ4eElzaM6w=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], @@ -3318,6 +3772,8 @@ "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + "jwt-decode": ["jwt-decode@3.1.2", "", {}, "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="], + "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=="], @@ -3336,6 +3792,8 @@ "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], + "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], + "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], @@ -3368,7 +3826,7 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - "linkify-it": ["linkify-it@5.0.1", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg=="], + "linkify-it": ["linkify-it@5.0.2", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q=="], "load-json-file": ["load-json-file@7.0.1", "", {}, "sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ=="], @@ -3412,12 +3870,14 @@ "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], - "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], + "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], "lucide-solid": ["lucide-solid@0.576.0", "", { "peerDependencies": { "solid-js": "^1.4.7" } }, "sha512-WX1JQJXCE8k4YWfj6pLx57i6AyzmH1LRyGP6LUDzIPgPIki9Pk4sIhSfTfF+jCBj8qRcp5aIz+tUURLJrzGAFw=="], + "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=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -3430,9 +3890,13 @@ "map-age-cleaner": ["map-age-cleaner@0.1.3", "", { "dependencies": { "p-defer": "^1.0.0" } }, "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w=="], - "markdown-it": ["markdown-it@14.2.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ=="], + "markdown-it": ["markdown-it@14.3.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.5.0", "linkify-it": "^5.0.2", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw=="], + + "marked": ["marked@18.0.7", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-iDVQ5ldaiKXn6b2JroX5kgRfmwgqolW7NpaEzTl1k/2Zh1njIEN9yniyLV/mOvWwtsE8OGgkjsCYvijuPk1dtA=="], + + "marked-katex-extension": ["marked-katex-extension@5.1.6", "", { "peerDependencies": { "katex": ">=0.16 <0.17", "marked": ">=4 <18" } }, "sha512-vYpLXwmlIDKILIhJtiRTgdyZRn5sEYdFBuTmbpjD7lbCIzg0/DWyK3HXIntN3Tp8zV6hvOUgpZNLWRCgWVc24A=="], - "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], + "marked-shiki": ["marked-shiki@1.2.1", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-yHxYQhPY5oYaIRnROn98foKhuClark7M373/VpLxiy5TrDu9Jd/LsMwo8w+U91Up4oDb9IXFrP0N1MFRz8W/DQ=="], "matcher": ["matcher@5.0.0", "", { "dependencies": { "escape-string-regexp": "^5.0.0" } }, "sha512-s2EMBOWtXFc8dgqvoAzKJXxNHibcdJMV0gwqKUaw9E2JBJuGUK7DrNKrA6g/i+v72TT16+6sVm5mS3thaMLQUw=="], @@ -3442,9 +3906,9 @@ "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], - "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], + "mdurl": ["mdurl@2.1.0", "", {}, "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg=="], - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + "media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], "mem": ["mem@9.0.2", "", { "dependencies": { "map-age-cleaner": "^0.1.3", "mimic-fn": "^4.0.0" } }, "sha512-F2t4YIv9XQUBHt6AOJ0y7lSmP1+cY7Fm1DRh9GClTGzKST7UWLMx6ly9WZdLH/G/ppM5RL4MlQfRT71ri9t19A=="], @@ -3486,12 +3950,10 @@ "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], - "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "minimisted": ["minimisted@2.0.1", "", { "dependencies": { "minimist": "^1.2.5" } }, "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA=="], - "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], "minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="], @@ -3506,9 +3968,11 @@ "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + "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=="], - "mocha": ["mocha@11.7.6", "", { "dependencies": { "browser-stdout": "^1.3.1", "chokidar": "^4.0.1", "debug": "^4.3.5", "diff": "^7.0.0", "escape-string-regexp": "^4.0.0", "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", "minimatch": "^9.0.5", "ms": "^2.1.3", "picocolors": "^1.1.1", "serialize-javascript": "^6.0.2", "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", "workerpool": "^9.2.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" }, "bin": { "mocha": "bin/mocha.js", "_mocha": "bin/_mocha" } }, "sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA=="], + "mocha": ["mocha@11.8.0", "", { "dependencies": { "browser-stdout": "^1.3.1", "chokidar": "^4.0.1", "debug": "^4.3.5", "diff": "^7.0.0", "escape-string-regexp": "^4.0.0", "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", "minimatch": "^9.0.5", "ms": "^2.1.3", "picocolors": "^1.1.1", "serialize-javascript": "^6.0.2", "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", "workerpool": "^9.2.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" }, "bin": { "mocha": "bin/mocha.js", "_mocha": "bin/_mocha" } }, "sha512-VyCeUdGN3A9lmCTTgG4yuvY9ixxaDk+xt2R/7/+1AP6EqNG+G9OKkzBwhVtVYoNX8YsxNSgAl8mOv3IAeOpFbw=="], "morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="], @@ -3522,19 +3986,19 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msgpackr": ["msgpackr@1.12.1", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ=="], + "msgpackr": ["msgpackr@2.0.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA=="], "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=="], "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=="], "mute-stream": ["mute-stream@0.0.8", "", {}, "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="], "nanoevents": ["nanoevents@7.0.1", "", {}, "sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q=="], - "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "nanoid": ["nanoid@3.3.17", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g=="], "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], @@ -3542,9 +4006,9 @@ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "next": ["next@16.2.9", "", { "dependencies": { "@next/env": "16.2.9", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.9", "@next/swc-darwin-x64": "16.2.9", "@next/swc-linux-arm64-gnu": "16.2.9", "@next/swc-linux-arm64-musl": "16.2.9", "@next/swc-linux-x64-gnu": "16.2.9", "@next/swc-linux-x64-musl": "16.2.9", "@next/swc-win32-arm64-msvc": "16.2.9", "@next/swc-win32-x64-msvc": "16.2.9", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-MEOJiq/UvuezAdqVSceHbqDgZt1kDw2tpGVOlsdIoJsQdbN2JY2hpVG4xnXGkbdJUOEWhnRfiu/O4Hpc9Juwww=="], + "next": ["next@16.3.0", "", { "dependencies": { "@next/env": "16.3.0", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.3.0", "@next/swc-darwin-x64": "16.3.0", "@next/swc-linux-arm64-gnu": "16.3.0", "@next/swc-linux-arm64-musl": "16.3.0", "@next/swc-linux-x64-gnu": "16.3.0", "@next/swc-linux-x64-musl": "16.3.0", "@next/swc-win32-arm64-msvc": "16.3.0", "@next/swc-win32-x64-msvc": "16.3.0", "sharp": "^0.35.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-NEdGOzH+08eTXMUp9UYkA99Nhi5N6Thrhc1jgFOQgfgnGK/dA2hRwBpXep+exdFQrnwlRf/3Wixyp8lLBUpE2A=="], - "node-abi": ["node-abi@3.92.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ=="], + "node-abi": ["node-abi@3.94.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g=="], "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], @@ -3554,6 +4018,8 @@ "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + "node-forge": ["node-forge@1.4.0", "", {}, "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ=="], + "node-gyp": ["node-gyp@12.4.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw=="], "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], @@ -3562,7 +4028,7 @@ "node-html-parser": ["node-html-parser@7.1.0", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ=="], - "node-releases": ["node-releases@2.0.47", "", {}, "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og=="], + "node-releases": ["node-releases@2.0.53", "", {}, "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ=="], "node-sarif-builder": ["node-sarif-builder@3.4.0", "", { "dependencies": { "@types/sarif": "^2.1.7", "fs-extra": "^11.1.1" } }, "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg=="], @@ -3592,16 +4058,20 @@ "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - "nypm": ["nypm@0.6.7", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.2.4" }, "bin": { "nypm": "./dist/cli.mjs" } }, "sha512-s3ds97SD5pd1dULE+tHUk1DrV0cSHOnsfpcdGATJ8JpBo21DoKqN9exTH4/2nhPQNOLomBdTFMicN94S4DrZrQ=="], + "nypm": ["nypm@0.6.9", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.2.4" }, "bin": { "nypm": "./dist/cli.mjs" } }, "sha512-zxlE2yvSWZWmHcNdT3+5zV2lrCogeE9YOklHrR3dFjqutq5wO7GFDYLFDRXLsYnJzwvy/im9fYoxePvS0VTW0w=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "obug": ["obug@2.1.3", "", {}, "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg=="], + "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + "oidc-token-hash": ["oidc-token-hash@5.2.0", "", {}, "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw=="], + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], @@ -3620,11 +4090,11 @@ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], - "opencode-gitlab-auth": ["opencode-gitlab-auth@2.0.1", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-1EMZHdbADLMVaTVLQ6C/V8uVMDr6MP++osj2lmOecowtn46AafP/w6ADkV4AN/ddjA1rob5cWpMuf/iME6DI6A=="], + "opencode-gitlab-auth": ["opencode-gitlab-auth@2.1.0", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-ZCDYaY0V8Se6hOH2tqZqqcskrd0xLTgfiGhU0J1igkUP52oFtN9eSwxOPLT0ctvNXUq8b+zOmJ4sskAQoC/IUA=="], "opencode-poe-auth": ["opencode-poe-auth@0.0.1", "", { "dependencies": { "open": "^10.0.0", "poe-oauth": "*" }, "peerDependencies": { "@opencode-ai/plugin": "*" } }, "sha512-cXqTlS6AXHzo1oBdosnxbT47ZJEZ9WXn050X8Re6wZ1vaNnTpB/l2fMQt90evT7RBK0fB8UjXQUDMKyd7bbiqg=="], - "opentui-spinner": ["opentui-spinner@0.0.6", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.1.49", "@opentui/react": "^0.1.49", "@opentui/solid": "^0.1.49", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-xupLOeVQEAXEvVJCvHkfX6fChDWmJIPHe5jyUrVb8+n4XVTX8mBNhitFfB9v2ZbkC1H2UwPab/ElePHoW37NcA=="], + "openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="], "option": ["option@0.2.4", "", {}, "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A=="], @@ -3636,7 +4106,7 @@ "oxc-parser": ["oxc-parser@0.127.0", "", { "dependencies": { "@oxc-project/types": "^0.127.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.127.0", "@oxc-parser/binding-android-arm64": "0.127.0", "@oxc-parser/binding-darwin-arm64": "0.127.0", "@oxc-parser/binding-darwin-x64": "0.127.0", "@oxc-parser/binding-freebsd-x64": "0.127.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.127.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.127.0", "@oxc-parser/binding-linux-arm64-gnu": "0.127.0", "@oxc-parser/binding-linux-arm64-musl": "0.127.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-musl": "0.127.0", "@oxc-parser/binding-linux-s390x-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-musl": "0.127.0", "@oxc-parser/binding-openharmony-arm64": "0.127.0", "@oxc-parser/binding-wasm32-wasi": "0.127.0", "@oxc-parser/binding-win32-arm64-msvc": "0.127.0", "@oxc-parser/binding-win32-ia32-msvc": "0.127.0", "@oxc-parser/binding-win32-x64-msvc": "0.127.0" } }, "sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA=="], - "oxc-resolver": ["oxc-resolver@11.20.0", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.20.0", "@oxc-resolver/binding-android-arm64": "11.20.0", "@oxc-resolver/binding-darwin-arm64": "11.20.0", "@oxc-resolver/binding-darwin-x64": "11.20.0", "@oxc-resolver/binding-freebsd-x64": "11.20.0", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.20.0", "@oxc-resolver/binding-linux-arm-musleabihf": "11.20.0", "@oxc-resolver/binding-linux-arm64-gnu": "11.20.0", "@oxc-resolver/binding-linux-arm64-musl": "11.20.0", "@oxc-resolver/binding-linux-ppc64-gnu": "11.20.0", "@oxc-resolver/binding-linux-riscv64-gnu": "11.20.0", "@oxc-resolver/binding-linux-riscv64-musl": "11.20.0", "@oxc-resolver/binding-linux-s390x-gnu": "11.20.0", "@oxc-resolver/binding-linux-x64-gnu": "11.20.0", "@oxc-resolver/binding-linux-x64-musl": "11.20.0", "@oxc-resolver/binding-openharmony-arm64": "11.20.0", "@oxc-resolver/binding-wasm32-wasi": "11.20.0", "@oxc-resolver/binding-win32-arm64-msvc": "11.20.0", "@oxc-resolver/binding-win32-x64-msvc": "11.20.0" } }, "sha512-CblytBiV/a/ZXY34dsVU2NxhIOxMXst8CvDCtyBelVITgd7PLrKzbEbA6oKLdPjvDKDzCiW48qzmzZ+mYaqn+g=="], + "oxc-resolver": ["oxc-resolver@11.24.2", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.24.2", "@oxc-resolver/binding-android-arm64": "11.24.2", "@oxc-resolver/binding-darwin-arm64": "11.24.2", "@oxc-resolver/binding-darwin-x64": "11.24.2", "@oxc-resolver/binding-freebsd-x64": "11.24.2", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-musl": "11.24.2", "@oxc-resolver/binding-openharmony-arm64": "11.24.2", "@oxc-resolver/binding-wasm32-wasi": "11.24.2", "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" } }, "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw=="], "oxlint": ["oxlint@1.60.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.60.0", "@oxlint/binding-android-arm64": "1.60.0", "@oxlint/binding-darwin-arm64": "1.60.0", "@oxlint/binding-darwin-x64": "1.60.0", "@oxlint/binding-freebsd-x64": "1.60.0", "@oxlint/binding-linux-arm-gnueabihf": "1.60.0", "@oxlint/binding-linux-arm-musleabihf": "1.60.0", "@oxlint/binding-linux-arm64-gnu": "1.60.0", "@oxlint/binding-linux-arm64-musl": "1.60.0", "@oxlint/binding-linux-ppc64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-musl": "1.60.0", "@oxlint/binding-linux-s390x-gnu": "1.60.0", "@oxlint/binding-linux-x64-gnu": "1.60.0", "@oxlint/binding-linux-x64-musl": "1.60.0", "@oxlint/binding-openharmony-arm64": "1.60.0", "@oxlint/binding-win32-arm64-msvc": "1.60.0", "@oxlint/binding-win32-ia32-msvc": "1.60.0", "@oxlint/binding-win32-x64-msvc": "1.60.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.18.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-tnRzTWiWJ9pg3ftRWnD0+Oqh78L6ZSwcEudvCZaER0PIqiAnNyXj5N1dPwjmNpDalkKS9m/WMLN1CTPUBPmsgw=="], @@ -3652,7 +4122,7 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], + "p-map": ["p-map@7.0.6", "", {}, "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg=="], "p-timeout": ["p-timeout@5.1.0", "", {}, "sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew=="], @@ -3684,13 +4154,15 @@ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="], + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - "path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="], + "path-expression-matcher": ["path-expression-matcher@1.6.2", "", {}, "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ=="], "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], @@ -3714,7 +4186,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], @@ -3742,27 +4214,25 @@ "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], - "poe-oauth": ["poe-oauth@0.0.9", "", {}, "sha512-eT/f/1zE/Zk5zSLKz47LmPhYy7pDbUHIoFRIxjqlxLG1rmqQyYEo8V8bX4ObSFk5+qKEvW02t/9ZfSSW3VUcGw=="], + "poe-oauth": ["poe-oauth@0.0.14", "", {}, "sha512-EHteyy5PsT8cl9zVjYcCQsm6FmLgqfwBkGj6IzUA83d9aWc57bH+jj2ItwJlf6RBhK+DN5N8YVlwEc0gOLb5fw=="], "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], - "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=="], + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], "postcss-selector-parser": ["postcss-selector-parser@7.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg=="], "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], - "posthog-js": ["posthog-js@1.386.6", "", { "dependencies": { "@posthog/core": "^1.32.3", "@posthog/types": "^1.386.3", "core-js": "^3.38.1", "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.28.2", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.1.0" } }, "sha512-xRcbToRtU07Ojk+VaCCos6I/zD60sNKfWxdeZih0xbncgegIqLZJmBzi4HdkSkXrf14b6HhwMI/s2GxWha5ADQ=="], + "posthog-js": ["posthog-js@1.413.3", "", { "dependencies": { "@posthog/browser-common": "^0.4.0", "@posthog/core": "^1.46.9", "@posthog/types": "^1.402.2", "core-js": "^3.49.0", "dompurify": "^3.4.12", "fflate": "^0.4.8", "preact": "^10.29.3", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.3.0", "web-vitals-soft-navs": "npm:web-vitals@6.0.0" } }, "sha512-UoAymtZYVr84Ldp0ITXmlK4Q3GE8vU7GW8wC+aCput5RrKVJ8wkAPc12n3ElmKRlvLL+oD+Fotjc7n3wYG0RGQ=="], "posthog-node": ["posthog-node@4.4.0", "", { "dependencies": { "axios": "^1.7.4" } }, "sha512-4FEFBuc4FDFkTEiCSIo7DFV0jpprlqfR/SzjVgg7E2rRvfxTW4J47IvCwSCo5C9lBeV0ujDubY/YArHG37aJIw=="], "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], - "preact": ["preact@10.29.2", "", {}, "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ=="], + "preact": ["preact@10.29.8", "", { "peerDependencies": { "preact-render-to-string": ">=5" }, "optionalPeers": ["preact-render-to-string"] }, "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q=="], "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=="], @@ -3782,7 +4252,7 @@ "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], - "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], + "process-warning": ["process-warning@5.1.0", "", {}, "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw=="], "proggy": ["proggy@4.0.0", "", {}, "sha512-MbA4R+WQT76ZBm/5JUpV9yqcJt92175+Y0Bodg3HgiXzrmKu7Ggq+bpn6y6wHH+gN9NcyKn3yg1+d47VaKwNAQ=="], @@ -3792,7 +4262,7 @@ "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], - "protobufjs": ["protobufjs@7.6.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw=="], + "protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="], "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=="], @@ -3804,11 +4274,11 @@ "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], - "pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="], + "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="], "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], - "qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], + "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], @@ -3822,7 +4292,7 @@ "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + "range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="], "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=="], @@ -3832,11 +4302,11 @@ "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], - "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], "react-docgen-typescript": ["react-docgen-typescript@2.4.0", "", { "peerDependencies": { "typescript": ">= 4.3.x" } }, "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg=="], - "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], @@ -3850,11 +4320,13 @@ "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "readdir-glob": ["readdir-glob@1.1.3", "", { "dependencies": { "minimatch": "^5.1.0" } }, "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], - "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], + "recast": ["recast@0.23.19", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-T98lym7kH+pnZmRaD8yDRdaNqyUbwnbEBx0MuchrzMFOEMray4AO3ZJoTUZ5r78Ao78X/OhzW0DL8GB85w/I2w=="], "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], @@ -3904,7 +4376,7 @@ "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], - "rollup": ["rollup@4.61.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.61.1", "@rollup/rollup-android-arm64": "4.61.1", "@rollup/rollup-darwin-arm64": "4.61.1", "@rollup/rollup-darwin-x64": "4.61.1", "@rollup/rollup-freebsd-arm64": "4.61.1", "@rollup/rollup-freebsd-x64": "4.61.1", "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", "@rollup/rollup-linux-arm-musleabihf": "4.61.1", "@rollup/rollup-linux-arm64-gnu": "4.61.1", "@rollup/rollup-linux-arm64-musl": "4.61.1", "@rollup/rollup-linux-loong64-gnu": "4.61.1", "@rollup/rollup-linux-loong64-musl": "4.61.1", "@rollup/rollup-linux-ppc64-gnu": "4.61.1", "@rollup/rollup-linux-ppc64-musl": "4.61.1", "@rollup/rollup-linux-riscv64-gnu": "4.61.1", "@rollup/rollup-linux-riscv64-musl": "4.61.1", "@rollup/rollup-linux-s390x-gnu": "4.61.1", "@rollup/rollup-linux-x64-gnu": "4.61.1", "@rollup/rollup-linux-x64-musl": "4.61.1", "@rollup/rollup-openbsd-x64": "4.61.1", "@rollup/rollup-openharmony-arm64": "4.61.1", "@rollup/rollup-win32-arm64-msvc": "4.61.1", "@rollup/rollup-win32-ia32-msvc": "4.61.1", "@rollup/rollup-win32-x64-gnu": "4.61.1", "@rollup/rollup-win32-x64-msvc": "4.61.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA=="], + "rollup": ["rollup@4.62.4", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.4", "@rollup/rollup-android-arm64": "4.62.4", "@rollup/rollup-darwin-arm64": "4.62.4", "@rollup/rollup-darwin-x64": "4.62.4", "@rollup/rollup-freebsd-arm64": "4.62.4", "@rollup/rollup-freebsd-x64": "4.62.4", "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", "@rollup/rollup-linux-arm-musleabihf": "4.62.4", "@rollup/rollup-linux-arm64-gnu": "4.62.4", "@rollup/rollup-linux-arm64-musl": "4.62.4", "@rollup/rollup-linux-loong64-gnu": "4.62.4", "@rollup/rollup-linux-loong64-musl": "4.62.4", "@rollup/rollup-linux-ppc64-gnu": "4.62.4", "@rollup/rollup-linux-ppc64-musl": "4.62.4", "@rollup/rollup-linux-riscv64-gnu": "4.62.4", "@rollup/rollup-linux-riscv64-musl": "4.62.4", "@rollup/rollup-linux-s390x-gnu": "4.62.4", "@rollup/rollup-linux-x64-gnu": "4.62.4", "@rollup/rollup-linux-x64-musl": "4.62.4", "@rollup/rollup-openbsd-x64": "4.62.4", "@rollup/rollup-openharmony-arm64": "4.62.4", "@rollup/rollup-win32-arm64-msvc": "4.62.4", "@rollup/rollup-win32-ia32-msvc": "4.62.4", "@rollup/rollup-win32-x64-gnu": "4.62.4", "@rollup/rollup-win32-x64-msvc": "4.62.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg=="], "rotating-file-stream": ["rotating-file-stream@3.2.9", "", {}, "sha512-i9i0KkHh12ryl4xtELg+0gyoFre2PJ9RcQQLzquWsiqygyYsrZLckrqqYrthhnJZGZb4g+KUHtcoWYVq34gaug=="], @@ -3928,7 +4400,7 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], + "sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -3938,7 +4410,7 @@ "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], - "semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "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=="], @@ -3946,9 +4418,9 @@ "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], - "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], + "seroval": ["seroval@1.5.6", "", {}, "sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA=="], - "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], + "seroval-plugins": ["seroval-plugins@1.5.6", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ=="], "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=="], @@ -3956,23 +4428,19 @@ "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], - "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], - "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "sha.js": ["sha.js@2.4.12", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" } }, "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w=="], - - "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=="], + "sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="], "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=="], - "shell-quote": ["shell-quote@1.8.4", "", {}, "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ=="], + "shell-quote": ["shell-quote@1.10.0", "", {}, "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA=="], - "shiki": ["shiki@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/engine-javascript": "3.20.0", "@shikijs/engine-oniguruma": "3.20.0", "@shikijs/langs": "3.20.0", "@shikijs/themes": "3.20.0", "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kgCOlsnyWb+p0WU+01RjkCH+eBVsjL1jOwUYWv0YDWkM2/A46+LDKVs5yZCUXjJG6bj4ndFoAg5iLIIue6dulg=="], + "shiki": ["shiki@4.2.0", "", { "dependencies": { "@shikijs/core": "4.2.0", "@shikijs/engine-javascript": "4.2.0", "@shikijs/engine-oniguruma": "4.2.0", "@shikijs/langs": "4.2.0", "@shikijs/themes": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ=="], "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], @@ -4002,11 +4470,11 @@ "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], - "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], + "smol-toml": ["smol-toml@1.7.1", "", {}, "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ=="], "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=="], + "socket.io-parser": ["socket.io-parser@4.2.7", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg=="], "socks": ["socks@2.8.9", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw=="], @@ -4018,10 +4486,12 @@ "solid-presence": ["solid-presence@0.1.8", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA=="], - "solid-prevent-scroll": ["solid-prevent-scroll@0.1.10", "", { "dependencies": { "@corvu/utils": "~0.4.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-KplGPX2GHiWJLZ6AXYRql4M127PdYzfwvLJJXMkO+CMb8Np4VxqDAg5S8jLdwlEuBis/ia9DKw2M8dFx5u8Mhw=="], + "solid-prevent-scroll": ["solid-prevent-scroll@0.1.11", "", { "dependencies": { "@corvu/utils": "~0.4.2" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-2PComVCDHaQN/5t7ogEqUYeHasritZKXZ8Sb/VLkl0Web8o9YhEwOo8LSujJEahvMlYCNyKt0zRJeHldOAeWZg=="], "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=="], + "sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="], "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -4032,19 +4502,37 @@ "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], - "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], + "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], + + "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], + + "spdx-expression-parse": ["spdx-expression-parse@4.0.0", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ=="], + + "spdx-license-ids": ["spdx-license-ids@3.0.23", "", {}, "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="], + + "sst": ["sst@4.13.1", "", { "dependencies": { "@aws/durable-execution-sdk-js": "1.0.2", "aws4fetch": "1.0.18", "jose": "5.2.3", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "4.13.1", "sst-darwin-x64": "4.13.1", "sst-linux-arm64": "4.13.1", "sst-linux-x64": "4.13.1", "sst-linux-x86": "4.13.1", "sst-win32-arm64": "4.13.1", "sst-win32-x64": "4.13.1", "sst-win32-x86": "4.13.1" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-FKzhWuqV5NPhFWH7b+Ktf5+Mvox6DCMc8iOrjaS3mkjnnmyEcOZq7HNx2/ZlIcBwgOZE9sCPR3ot1zXtXkXzZg=="], + + "sst-darwin-arm64": ["sst-darwin-arm64@4.13.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gGwfgZGOsulU6iGeZJtxP5BpSZFsRUy1dG/yTqJa++n1P1APWXZC64u2qRHm6rdwLAjEeWaCQ9k7ouKgn0zh5Q=="], + + "sst-darwin-x64": ["sst-darwin-x64@4.13.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-RcKLI2oAY+Wk5fWUF0gJFKvwMSpid3qw9L3OcfgFL2ymZH652ybLpQPBxtoJ95FdyknWIMnYf6ZK9C2jnH5eaw=="], - "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], + "sst-linux-arm64": ["sst-linux-arm64@4.13.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Lp/8BWhH8KZnb50xPd8A6bkZ7QqCH3Dar9l16TPwuAmPxxRUlkmg42h+ACZ9n02vSJl8y/WLnV4JB6SgJpRFVg=="], - "spdx-expression-parse": ["spdx-expression-parse@4.0.0", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ=="], + "sst-linux-x64": ["sst-linux-x64@4.13.1", "", { "os": "linux", "cpu": "x64" }, "sha512-f4eHOsApoP+CpUzLUGzfoTBYsqqr6nl/M0gjYsVNi1T+6v92lc84AhQ4DoDZx033QggyxkqRezj8jOkjkKxa7w=="], - "spdx-license-ids": ["spdx-license-ids@3.0.23", "", {}, "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw=="], + "sst-linux-x86": ["sst-linux-x86@4.13.1", "", { "os": "linux", "cpu": "none" }, "sha512-5OHdtPa0GmozDpBPdsD9Tv+SGnpjJSoTXptgK59ivQg3UYNWBBotidvEyUJs1MU9B/3+I+Fmw32zsF2O+ZtPgg=="], - "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "sst-win32-arm64": ["sst-win32-arm64@4.13.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-qAUuCQj6wDdZ84a9W4FhnRTHYlEnYRHGYFqO3cmMImbnAQEoKSQR1XuWqt0Ev4RT3kU4Lz/fgvJjGkR7WUstLg=="], - "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "sst-win32-x64": ["sst-win32-x64@4.13.1", "", { "os": "win32", "cpu": "x64" }, "sha512-r+83NMwpe4MLAvkVoOxi1KNs6nIqilFucsTr+VN1PgknsiVBY1emITjSWS2jDJJMuH8xJXL5xJzF/l5rcKWErg=="], - "ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="], + "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=="], @@ -4054,7 +4542,7 @@ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], + "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], @@ -4062,6 +4550,8 @@ "storybook-solidjs-vite": ["storybook-solidjs-vite@10.0.9", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.6.1", "@storybook/builder-vite": "^10.0.0", "@storybook/global": "^5.0.0", "vite-plugin-solid": "^2.11.8" }, "peerDependencies": { "solid-js": "^1.9.0", "storybook": "^0.0.0-0 || ^10.0.0", "typescript": ">= 4.9.x", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["typescript"] }, "sha512-n6MwWCL9mK/qIaUutE9vhGB0X1I1hVnKin2NL+iVC5oXfAiuaABVZlr/1oEeEypsgCdyDOcbEbhJmDWmaqGpPw=="], + "streamx": ["streamx@2.28.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw=="], + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "string-width-cjs": ["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=="], @@ -4084,11 +4574,11 @@ "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], - "strnum": ["strnum@2.4.0", "", { "dependencies": { "anynum": "^1.0.0" } }, "sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg=="], + "strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="], "structured-source": ["structured-source@4.0.0", "", { "dependencies": { "boundary": "^2.0.0" } }, "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA=="], - "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "@babel/core": "*", "babel-plugin-macros": "*", "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" }, "optionalPeers": ["@babel/core", "babel-plugin-macros"] }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], "stylis": ["stylis@4.4.0", "", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="], @@ -4106,15 +4596,17 @@ "table-layout": ["table-layout@4.1.1", "", { "dependencies": { "array-back": "^6.2.2", "wordwrapjs": "^5.1.0" } }, "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA=="], - "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], + "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "tar": ["tar@7.5.16", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w=="], + "tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="], - "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], + "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@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=="], + "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=="], "temp-dir": ["temp-dir@3.0.0", "", {}, "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw=="], @@ -4124,6 +4616,8 @@ "test-exclude": ["test-exclude@7.0.2", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^10.2.2" } }, "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw=="], + "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="], + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], "textextensions": ["textextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ=="], @@ -4138,28 +4632,28 @@ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], + "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], - "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="], "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], "tmp": ["tmp@0.2.7", "", {}, "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw=="], - "to-buffer": ["to-buffer@1.2.2", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw=="], - "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=="], + "toad-cache": ["toad-cache@3.7.4", "", {}, "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg=="], "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=="], "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "traverse": ["traverse@0.3.9", "", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="], + "tree-sitter-bash": ["tree-sitter-bash@0.25.0", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-gZtlj9+qFS81qKxpLfD6H0UssQ3QBc/F0nKkPsiFDyfQF2YBqYvglFJUzchrPpVhZe9kLZTrJ9n2J6lmka69Vg=="], "tree-sitter-powershell": ["tree-sitter-powershell@0.25.10", "", { "dependencies": { "node-addon-api": "^7.1.0", "node-gyp-build": "^4.8.0" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-bEt8QoySpGFnU3aa8WedQyNMaN6aTwy/WUbvIVt0JSKF+BbJoSHNHu+wCbhj7xLMsfB0AuffmiJm+B8gzva8Lg=="], @@ -4186,23 +4680,23 @@ "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], - "turbo": ["turbo@2.9.14", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.14", "@turbo/darwin-arm64": "2.9.14", "@turbo/linux-64": "2.9.14", "@turbo/linux-arm64": "2.9.14", "@turbo/windows-64": "2.9.14", "@turbo/windows-arm64": "2.9.14" }, "bin": { "turbo": "bin/turbo" } }, "sha512-BQqXRr4UoWI3UPFrtznCLykYHxwxWh53iCB57x092jPMjIlW1wnm3N895g5irpiXmnxUhREBB0n6+y8BHhs4nw=="], + "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=="], + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "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=="], - "typed-rest-client": ["typed-rest-client@1.8.11", "", { "dependencies": { "qs": "^6.9.1", "tunnel": "0.0.6", "underscore": "^1.12.1" } }, "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA=="], "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], - "typescript-eslint": ["typescript-eslint@8.61.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.61.0", "@typescript-eslint/parser": "8.61.0", "@typescript-eslint/typescript-estree": "8.61.0", "@typescript-eslint/utils": "8.61.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw=="], + "typescript-eslint": ["typescript-eslint@8.66.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.66.0", "@typescript-eslint/parser": "8.66.0", "@typescript-eslint/typescript-estree": "8.66.0", "@typescript-eslint/utils": "8.66.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw=="], "typical": ["typical@4.0.0", "", {}, "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw=="], @@ -4236,6 +4730,8 @@ "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=="], + "unzip-stream": ["unzip-stream@0.3.4", "", { "dependencies": { "binary": "^0.3.0", "mkdirp": "^0.5.1" } }, "sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw=="], + "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=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -4258,7 +4754,7 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.0.1", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.37", "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.21" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-6SxA8a4MoA6Q/c+D3q7My0Hfog76enN3n0MXhwosM+tso66rXBEGeBRD/0lravRDVzL2Q1w5QJPc86rAVJtfXg=="], + "venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.1.1", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.51", "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.30" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-w3OHkuzzKZ3r2TOxER6myBYzZJNoDqol+DUHu3NnfBN/GETnUVxecZJab0CHQQ8GZc0jjzpFymepjcLDPS4SQg=="], "version-range": ["version-range@4.15.0", "", {}, "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg=="], @@ -4266,9 +4762,9 @@ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - "virtua": ["virtua@0.42.3", "", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-5FoAKcEvh05qsUF97Yz42SWJ7bwnPExjUYHGuoxz1EUtfWtaOgXaRwnylJbDpA0QcH1rKvJ2qsGRi9MK1fpQbg=="], + "virtua": ["virtua@0.49.1", "", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-6f79msqg3jzNFdqJiS0FSzhRN1EHlDhR7EvW7emp6z5qQ22VdsReiDHflkpMEMhoAyUuYr69nwT0aagiM7NrUg=="], - "vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="], + "vite": ["vite@7.3.5", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww=="], "vite-plugin-icons-spritesheet": ["vite-plugin-icons-spritesheet@3.0.1", "", { "dependencies": { "chalk": "^5.4.1", "glob": "^11.0.1", "node-html-parser": "^7.0.1", "tinyexec": "^0.3.2" }, "peerDependencies": { "vite": ">=5.2.0" } }, "sha512-Cr0+Z6wRMwSwKisWW9PHeTjqmQFv0jwRQQMc3YgAhAgZEe03j21el0P/CA31KN/L5eiL1LhR14VTXl96LetonA=="], @@ -4290,6 +4786,8 @@ "web-vitals": ["web-vitals@5.3.0", "", {}, "sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g=="], + "web-vitals-soft-navs": ["web-vitals@6.0.0", "", {}, "sha512-Guaibvy/+uNtL6Bsu4jmMJGzuSl91oeRH5iO9pPRbYftnFUr3yqT1TUNX/OE4o9HexuEMU3Kb/Wg7iKhlffZUA=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], @@ -4306,8 +4804,6 @@ "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], - "which-typed-array": ["which-typed-array@1.1.22", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw=="], - "why-is-node-running": ["why-is-node-running@3.2.2", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], @@ -4332,7 +4828,7 @@ "xlsx": ["xlsx@https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", { "bin": { "xlsx": "./bin/xlsx.njs" } }, "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA=="], - "xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="], + "xml-naming": ["xml-naming@0.3.0", "", {}, "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ=="], "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], @@ -4342,7 +4838,7 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], @@ -4358,7 +4854,7 @@ "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], - "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + "zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="], "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], @@ -4368,75 +4864,227 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@actions/artifact/@actions/core": ["@actions/core@2.0.3", "", { "dependencies": { "@actions/exec": "^2.0.0", "@actions/http-client": "^3.0.2" } }, "sha512-Od9Thc3T1mQJYddvVPM4QGiLUewdh+3txmDYHHxoNdkqysR1MbCT+rFOtNUxYAz+7+6RIsqipVahY2GJqGPyxA=="], + + "@actions/core/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], + + "@actions/github/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], + + "@actions/http-client/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], + "@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], - "@ai-sdk/azure/@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=="], + "@ai-sdk/alibaba/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], - "@ai-sdk/azure/@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/alibaba/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "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-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="], - "@ai-sdk/cerebras/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.81", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-B1JDd9Ugq9R5AgIaW3674lhGCMMYJcPUxnrZh8fzbGojgg4QvHFRv6eZahGQAUsmGHbcf74G9bdSBDLWQGY2GA=="], - "@ai-sdk/cerebras/@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/@ai-sdk/openai": ["@ai-sdk/openai@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-oAiGC9eWG7IgtdsdS74bOCnAAHarAfTJhWN9x5INwnWPekL802AvF+0I5DvLzIF1MIRmNw4N8mPSL/GUVbX9Mw=="], - "@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "@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/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], - "@ai-sdk/deepgram/@ai-sdk/provider": ["@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=="], - "@ai-sdk/deepgram/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.29", "", { "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-uhukHaCBvqkwBHkT8C2PrnqKTCoLn3pdHXqtcR9I8ErH+flbzgW4o7VHSNIup9LRu+WBvZIZDQLsx6rwl2tiOA=="], + "@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=="], + + "@ai-sdk/cerebras/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="], + + "@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nJ0bAfegMAIJtrzMJtbzer1cS3nb7c7DsyU1S4nrPm7ZU0Mn6SBBZv5IGZZGTbpWTJwqKTSPeZJTXalbAxt1BA=="], + + "@ai-sdk/cohere/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], + + "@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "@ai-sdk/deepgram/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I7hhjfw01yEI8NkuAsT8Mv6xbWFr/lqLXMdaJQ2zWfXEpxog1eT7skDcv1+RY29/+5btzH8wD+vVvy48bk9oNQ=="], "@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="], + "@ai-sdk/deepinfra/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], + "@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "@ai-sdk/deepseek/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@ai-sdk/elevenlabs/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I7hhjfw01yEI8NkuAsT8Mv6xbWFr/lqLXMdaJQ2zWfXEpxog1eT7skDcv1+RY29/+5btzH8wD+vVvy48bk9oNQ=="], - "@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.29", "", { "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-uhukHaCBvqkwBHkT8C2PrnqKTCoLn3pdHXqtcR9I8ErH+flbzgW4o7VHSNIup9LRu+WBvZIZDQLsx6rwl2tiOA=="], + "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.41" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EmrD7iRboidulu6yHfMiMhd6RQSw8KrIWhNLK8vl5brQZbIjXkyhUU+FULZM3P4m46Vatzx8u3vX1w/qmFUmqA=="], - "@ai-sdk/elevenlabs/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I7hhjfw01yEI8NkuAsT8Mv6xbWFr/lqLXMdaJQ2zWfXEpxog1eT7skDcv1+RY29/+5btzH8wD+vVvy48bk9oNQ=="], - "@ai-sdk/elevenlabs/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.29", "", { "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-uhukHaCBvqkwBHkT8C2PrnqKTCoLn3pdHXqtcR9I8ErH+flbzgW4o7VHSNIup9LRu+WBvZIZDQLsx6rwl2tiOA=="], + "@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], - "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.29" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HyuxddF2Yv5G8qxK/0uksAINjQ4h6TpwOqHuqzsCM0u78/JWAW2OXcIplQeB44PIAORgPjbMzrw9DhnPYHMskA=="], + "@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], - "@ai-sdk/fireworks/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.77", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ML8C2M1YvPA1ulEx4TiyF0k1xvC2ikEiPBIC1PPQ0a5xELUGrO2lAaEzsTEoJ+eCeDd8PSBuFJjs+r+9yIwQXA=="], - "@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.29", "", { "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-uhukHaCBvqkwBHkT8C2PrnqKTCoLn3pdHXqtcR9I8ErH+flbzgW4o7VHSNIup9LRu+WBvZIZDQLsx6rwl2tiOA=="], + "@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg=="], - "@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CbR82EgGPNrj/6q0HtclwuCqe0/pDShyv3nWDP/A9DroujzWXnLMlUJVrgPOsg4b40zQCwwVs2XSKCxvt/4QaA=="], + "@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], - "@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], + "@ai-sdk/google-vertex/@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/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/groq/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], - "@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/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/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], "@ai-sdk/openai-compatible/@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/perplexity/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], + "@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/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="], + "@ai-sdk/togetherai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], + "@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/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="], + "@ai-sdk/vercel/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], + "@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=="], - "@ai-sdk/xai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.35" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cQrN6OUn/jvsY3OdsU6Wn+ss7vp1iwIcakZKSlSRMnYqShBfyT7Qht+eqmgxs7w9ttrw6FAG6o11AiBs+iEsTA=="], + + "@ai-sdk/xai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.13", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-ZPtVYt5QIJzOta1kdUiDuCx4HhFkvNPv/rvmZ2b1iXwybYjJsCnNYR4PAw4kW7rgVfDARvHXcU64efWuqNp6bw=="], + + "@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=="], + + "@antfu/install-pkg/package-manager-detector": ["package-manager-detector@1.8.0", "", {}, "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A=="], - "@ai-sdk/xai/@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=="], + "@anthropic-ai/sandbox-runtime/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@antfu/install-pkg/package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + "@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-crypto/crc32c/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-crypto/sha1-browser/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@aws-crypto/sha256-browser/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@aws-crypto/sha256-js/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-crypto/util/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + "@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/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.53", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/credential-provider-env": "^3.972.46", "@aws-sdk/credential-provider-http": "^3.972.48", "@aws-sdk/credential-provider-login": "^3.972.52", "@aws-sdk/credential-provider-process": "^3.972.46", "@aws-sdk/credential-provider-sso": "^3.972.52", "@aws-sdk/credential-provider-web-identity": "^3.972.52", "@aws-sdk/nested-clients": "^3.997.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/credential-provider-imds": "^4.3.7", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-ZfdhIOR41q8TcWEnUac+gCOb+O2LBWdHLmjedXpXz4IEFW2ppNuFcm6p0sMTavpM+zD5TYfpH5Gp7guRyqSgsQ=="], + "@aws-sdk/client-bedrock-runtime/@aws-sdk/core": ["@aws-sdk/core@3.977.6", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw=="], + + "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.78", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.67", "@aws-sdk/credential-provider-http": "^3.972.69", "@aws-sdk/credential-provider-ini": "^3.973.12", "@aws-sdk/credential-provider-process": "^3.972.67", "@aws-sdk/credential-provider-sso": "^3.973.11", "@aws-sdk/credential-provider-web-identity": "^3.972.73", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-OgPAnfvbGAMWac6yvxJ1ihslrvDpPVwR68D2csospdNCCyPvHk9JLzYKwz48SNiS1T2znDwHauywRKRFfpyYng=="], + + "@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.42", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "tslib": "^2.6.2" } }, "sha512-IitZ5m7zfdVCgV2Mg6XM90dYQ1AzIcgfiuuZpP8W+t38W4p7fkyKvBavYMn91MaWOv2Woh5ObLAF2A7n7vT5MA=="], + + "@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.41", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "tslib": "^2.6.2" } }, "sha512-z7XlQKC0/RPd9Kor3MAaVVqYXK9JUgCMwUyLFwxXqRxpj3VfA3MbvUqgRRh5jJB54hwICOT6sd0alkLC0f9zpw=="], + + "@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.43", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "tslib": "^2.6.2" } }, "sha512-HjdnXlrWhL/YZLrKA6ZXJJZA/MhGIXAhPMuJYU5xCDfCToMoYZN8/tbIGUeTL6ZwJdYxsCEYRj4C8H2Xh2oB4w=="], + + "@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.71", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "tslib": "^2.6.2" } }, "sha512-wRRu4iEioYnddAtySab1+EctzaS25CH6xYrFcpuJvLKcOtSHkmaXSa0g+9I4cY3Z33dgzIlzsyd7K9IwmOD4+A=="], + + "@aws-sdk/client-bedrock-runtime/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.45", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "tslib": "^2.6.2" } }, "sha512-kynhIjb70q6omGfGj1uZEWImv6fFNwy7IVu2qCMtfbqFTStSduWRiYUI8wLJtSARxG0VThvgChxOfPsc/Ry3KQ=="], + + "@aws-sdk/client-bedrock-runtime/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-sdk/client-bedrock-runtime/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.40", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@smithy/core": "^3.31.1", "tslib": "^2.6.2" } }, "sha512-/2XLWqQVgzbaZNZ8mqJmsmF7wiUB6qd3CCQmNWQWRG7RX3H9FwrQ6JGe3tvq+IJVlHVrfmfMeBKqG40rVSsIrg=="], + + "@aws-sdk/client-bedrock-runtime/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.42", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "tslib": "^2.6.2" } }, "sha512-bT04jVHgpayfLumPfBYzz/2iRzxd7Lm1Xmw5aIpbed42EDDdnAiDLpageHUWAAGP9/xNdBpCRHzBPI6uaPqATQ=="], + + "@aws-sdk/client-bedrock-runtime/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.57", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "tslib": "^2.6.2" } }, "sha512-nOzS+CIIbWQN8+OyN2sufQ1/KGwPq3Rx6x+U7m2KAy98bWh7gW18RvBRnq+fnpnG7CX94lkyZzxf1qmbbS/pjA=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.977.6", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.78", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.67", "@aws-sdk/credential-provider-http": "^3.972.69", "@aws-sdk/credential-provider-ini": "^3.973.12", "@aws-sdk/credential-provider-process": "^3.972.67", "@aws-sdk/credential-provider-sso": "^3.973.11", "@aws-sdk/credential-provider-web-identity": "^3.972.73", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-OgPAnfvbGAMWac6yvxJ1ihslrvDpPVwR68D2csospdNCCyPvHk9JLzYKwz48SNiS1T2znDwHauywRKRFfpyYng=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-sdk/client-lambda/@aws-sdk/core": ["@aws-sdk/core@3.977.6", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw=="], + + "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.78", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.67", "@aws-sdk/credential-provider-http": "^3.972.69", "@aws-sdk/credential-provider-ini": "^3.973.12", "@aws-sdk/credential-provider-process": "^3.972.67", "@aws-sdk/credential-provider-sso": "^3.973.11", "@aws-sdk/credential-provider-web-identity": "^3.972.73", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-OgPAnfvbGAMWac6yvxJ1ihslrvDpPVwR68D2csospdNCCyPvHk9JLzYKwz48SNiS1T2znDwHauywRKRFfpyYng=="], + + "@aws-sdk/client-lambda/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/core": ["@aws-sdk/core@3.977.6", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1066.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/nested-clients": "^3.997.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-UqEUJq7dqa44hneLDUcX7UJy95cg8YqEWyakRpvIPnrNS3Mq+UlQHgCDGu5pvwAPtlIW4qcYbvW6reG6++FyvA=="], + "@aws-sdk/credential-provider-env/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/core": ["@aws-sdk/core@3.977.6", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/core": ["@aws-sdk/core@3.977.6", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-sdk/credential-provider-login/@aws-sdk/core": ["@aws-sdk/core@3.977.6", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw=="], + + "@aws-sdk/credential-provider-login/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-ozge/c7NdHUDyHqro6+P5oHt8wfKSUBN+olttiVfBe9Mw3wBMpPa3gQ0pZnG+gwBkKskBuip2bMR16tqYvUSEA=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/node-http-handler": "^4.4.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" } }, "sha512-b6N9Nnlg8JInQwzBkUq5spNaXssM3h3zLxGzpPrnw0nHSIWPJPTbZzA5Ca285fcDUFuKP+qf3qkuqlAjGOdWhg=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-env": "3.932.0", "@aws-sdk/credential-provider-http": "3.932.0", "@aws-sdk/credential-provider-process": "3.932.0", "@aws-sdk/credential-provider-sso": "3.933.0", "@aws-sdk/credential-provider-web-identity": "3.933.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-HygGyKuMG5AaGXsmM0d81miWDon55xwalRHB3UmDg3QBhtunbNIoIaWUbNTKuBZXcIN6emeeEZw/YgSMqLc0YA=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-BodZYKvT4p/Dkm28Ql/FhDdS1+p51bcZeMMu2TRtU8PoMDHnVDhHz27zASEKSZwmhvquxHrZHB0IGuVqjZUtSQ=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.933.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.933.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/token-providers": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-/R1DBR7xNcuZIhS2RirU+P2o8E8/fOk+iLAhbqeSTq+g09fP/F6W7ouFpS5eVE2NIfWG7YBFoVddOhvuqpn51g=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-c7Eccw2lhFx2/+qJn3g+uIDWRuWi2A6Sz3PVvckFUEzPsP0dPUo19hlvtarwP5GzrsXn0yEPRVhpewsIaSCGaQ=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/core": ["@aws-sdk/core@3.977.6", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/core": ["@aws-sdk/core@3.977.6", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1103.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-N4wy26MNn31ItGVHYHPrEuCIFY4MBBjC+C5v1lJKqIUSA7OZBdhleCY53zCCrXn27hsk7YNOaTuhQu807S4AfQ=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core": ["@aws-sdk/core@3.977.6", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-sdk/credential-providers/@aws-sdk/core": ["@aws-sdk/core@3.977.6", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw=="], + + "@aws-sdk/credential-providers/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.12", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/credential-provider-env": "^3.972.67", "@aws-sdk/credential-provider-http": "^3.972.69", "@aws-sdk/credential-provider-login": "^3.972.74", "@aws-sdk/credential-provider-process": "^3.972.67", "@aws-sdk/credential-provider-sso": "^3.973.11", "@aws-sdk/credential-provider-web-identity": "^3.972.73", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-pNEf/OeyN5X3VmLKlgSO6TqaWmW10CvI3TfwL1XhsuhYjSLT2VDaxFnCPHnOeQXSaFisMX4jNhpETriqN8DOmg=="], + + "@aws-sdk/credential-providers/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.78", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.67", "@aws-sdk/credential-provider-http": "^3.972.69", "@aws-sdk/credential-provider-ini": "^3.973.12", "@aws-sdk/credential-provider-process": "^3.972.67", "@aws-sdk/credential-provider-sso": "^3.973.11", "@aws-sdk/credential-provider-web-identity": "^3.972.73", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-OgPAnfvbGAMWac6yvxJ1ihslrvDpPVwR68D2csospdNCCyPvHk9JLzYKwz48SNiS1T2znDwHauywRKRFfpyYng=="], + + "@aws-sdk/credential-providers/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-sdk/eventstream-handler-node/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-sdk/middleware-eventstream/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-sdk/middleware-websocket/@aws-sdk/core": ["@aws-sdk/core@3.977.6", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw=="], + + "@aws-sdk/middleware-websocket/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.977.6", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw=="], + + "@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.43", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA=="], + + "@aws-sdk/nested-clients/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-sdk/token-providers/@aws-sdk/core": ["@aws-sdk/core@3.977.6", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw=="], + + "@aws-sdk/token-providers/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], "@axe-core/playwright/axe-core": ["axe-core@4.11.4", "", {}, "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA=="], @@ -4450,11 +5098,13 @@ "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@bufbuild/protoplugin/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="], + "@changesets/apply-release-plan/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], "@changesets/write/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], - "@effect/platform-node/undici": ["undici@8.4.1", "", {}, "sha512-RNHlB4fxZK0IrkhBsxhlbx7s8kFWwr7rzzOqj5nvZugw3ig3RsB7KW3zVlV0eu8POl+rx5d1hmL7rRg0z1owow=="], + "@effect/platform-node/undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -4472,16 +5122,24 @@ "@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@hey-api/openapi-ts/commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], + "@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=="], + "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + "@joshwooding/vite-plugin-react-docgen-typescript/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], "@kilocode/kilo-docs/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "@kilocode/kilo-indexing/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "@kilocode/kilo-indexing/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "@kilocode/plugin/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], + "@manypkg/find-root/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], @@ -4490,10 +5148,18 @@ "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], - "@morphllm/morphsdk/openai": ["openai@4.104.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="], + "@modelcontextprotocol/sdk/jose": ["jose@6.2.8", "", {}, "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ=="], + + "@npmcli/arborist/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "@npmcli/config/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + + "@npmcli/git/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], "@npmcli/map-workspaces/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "@npmcli/map-workspaces/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "@npmcli/package-json/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], "@octokit/core/@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="], @@ -4506,83 +5172,109 @@ "@octokit/endpoint/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="], - "@octokit/graphql/@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="], + "@octokit/graphql/@octokit/request": ["@octokit/request@10.0.13", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.1.1", "@octokit/types": "^17.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-v2269YxL9Yf+x3d+gRI63FP0vFQEiWgLyBzxe/Y+0yFDg2B/Tzf5dhh9VNfccVAQnfcfwQWyk/y6Bn7rUXXs7A=="], "@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], - "@octokit/plugin-request-log/@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="], - "@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], + "@octokit/plugin-retry/@octokit/types": ["@octokit/types@6.41.0", "", { "dependencies": { "@octokit/openapi-types": "^12.11.0" } }, "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg=="], + "@octokit/request/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], "@octokit/request/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="], "@octokit/request-error/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], - "@octokit/rest/@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="], + "@octokit/rest/@octokit/core": ["@octokit/core@7.0.7", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.4", "@octokit/request": "^10.0.13", "@octokit/request-error": "^7.1.1", "@octokit/types": "^17.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DcB0M3KFgr9ECI328lhBMVsyFT2DnmNucSBTqEN3exyNKUzkkpUSCHmTRcunF41Eou2TIQKW4seewri8ON9bSA=="], "@octokit/rest/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.2.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw=="], + "@octokit/rest/@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="], + "@octokit/rest/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.1.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg=="], - "@opencode-ai/core/@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=="], + "@openauthjs/openauth/@standard-schema/spec": ["@standard-schema/spec@1.0.0-beta.3", "", {}, "sha512-0ifF3BjA1E8SY9C+nUew8RefNOIq0cDlYALPty4rhUm8Rrl6tCM8hBT4bhGhx7I7iXD0uAgt50lgo8dD73ACMw=="], + + "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], - "@opencode-ai/core/@ai-sdk/google": ["@ai-sdk/google@3.0.73", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-o2MuIeyvZrFIeIbnbA8Thrr63irdyUBh0uWBZ2lY6yFeXuE/tcwyXF74bDKS4KvTu84uFpQfpbS/LXHGKKXz+g=="], + "@opencode-ai/core/@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.54", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.48", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-m+oLYlZUA0j5aeE8BrjDd2DSCx6HujqPB2C0YjQF7iGQ4sTCCmhHfH2o0uY7vlSPIJpA2ak6YyF7R14a5dq8fw=="], - "@opencode-ai/core/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.128", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.77", "@ai-sdk/google": "3.0.73", "@ai-sdk/openai-compatible": "2.0.47", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jK8fixb4km2yfgvb9DUFQRpV/jiDB0v9gyxHoHfPydaQvz+CpAz8DTt1quyaM+Wg9G2R8Zo68CYmHbIkUqW2AA=="], + "@opencode-ai/core/@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], - "@opencode-ai/core/@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.993.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.993.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-cognito-identity": "^3.972.3", "@aws-sdk/credential-provider-env": "^3.972.9", "@aws-sdk/credential-provider-http": "^3.972.11", "@aws-sdk/credential-provider-ini": "^3.972.9", "@aws-sdk/credential-provider-login": "^3.972.9", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/credential-provider-process": "^3.972.9", "@aws-sdk/credential-provider-sso": "^3.972.9", "@aws-sdk/credential-provider-web-identity": "^3.972.9", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-1M/nukgPSLqe9krzOKHnE8OylUaKAiokAV3xRLdeExVHcRE7WG5uzCTKWTj1imKvPjDqXq/FWhlbbdWIn7xIwA=="], + "@opencode-ai/core/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "@opencode-ai/core/venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.0.2", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.47", "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.27" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-aoa05nI3BTK5aGbjBflq+Gfln2AHAkwNbWuGGvCzUIsOfp5Y3iPD4O4PUGDAEiWVJWbjpPn0KfDa0H/HebwsaA=="], + "@opencode-ai/plugin/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], - "@opencode-ai/storybook/@storybook/addon-a11y": ["@storybook/addon-a11y@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.4.4" } }, "sha512-/eUCx/6Ozq5grauwm/NqKtlW0oJ26b6GNesXrMuFID8WLg/qLEKf79Awfz9XrmyWxe7loD40K952r7AA5Oc23A=="], + "@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/@storybook/addon-docs": ["@storybook/addon-docs@10.4.4", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.4.4", "@storybook/icons": "^2.0.2", "@storybook/react-dom-shim": "10.4.4", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.4.4" }, "optionalPeers": ["@types/react"] }, "sha512-yPshCvtmQTq52T2sXuXgjy7B/QbhA/WIZxLYggptNjBL8BJMvbOfp9bAfCKh7+KpRWGqDZ6Y6tWL1Q48Wj3vtw=="], + "@opencode-ai/storybook/@storybook/addon-a11y": ["@storybook/addon-a11y@10.5.7", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.5.7" } }, "sha512-I30rsNz6aA3xg3811MEry40uJDHP3l5SOkfqtmNkp7y4NdqTDdKhmbhhDAZQX1WWEk6GeMBGr3AJ3TCz7r7JmQ=="], + + "@opencode-ai/storybook/@storybook/addon-docs": ["@storybook/addon-docs@10.5.7", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.5.7", "@storybook/icons": "^2.0.2", "@storybook/react-dom-shim": "10.5.7", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.5.7" }, "optionalPeers": ["@types/react"] }, "sha512-KNARJfjICaizinsR3INMEiipZm1ObYo+xw+E26gteu50Bcy2dIZUtk5uHY5XdtardU3AXX6yRXoBZ2HCY3lbHA=="], "@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/storybook/react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="], - "@opencode-ai/storybook/storybook": ["storybook@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA=="], + "@opencode-ai/storybook/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=="], + + "@opencode-ai/storybook/storybook": ["storybook@10.5.7", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", "jsonc-parser": "^3.3.1", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.21.1" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15 || ^0.2.0" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-oiKvWIwIoOhFP1i6dASYyMXwPHKEtVZMshqSB7EvIVYjWRh0l9H7gHEt1z4Gh2rLGFMekWdsm4s94rvwpR7gkg=="], "@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/ui/tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="], - "@oxc-parser/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + "@opentui/core/marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], - "@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=="], + "@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], - "@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@opentui/solid/@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="], - "@qdrant/js-client-rest/undici": ["undici@6.26.0", "", {}, "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A=="], + "@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="], - "@shikijs/transformers/@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=="], + "@oxc-resolver/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], - "@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + "@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], - "@solid-primitives/resize-observer/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="], + "@protobuf-ts/plugin/typescript": ["typescript@3.9.10", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q=="], - "@storybook/addon-links/storybook": ["storybook@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA=="], + "@qdrant/js-client-rest/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "@storybook/addon-onboarding/storybook": ["storybook@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA=="], + "@shikijs/core/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], - "@storybook/addon-vitest/storybook": ["storybook@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA=="], + "@shikijs/engine-javascript/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], - "@storybook/builder-vite/@storybook/csf-plugin": ["@storybook/csf-plugin@10.4.4", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.4.4", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-1mzZyAwVUmAcw4WEUsJDVdSupkJf+Kf/f5uNAs4RzlBXA75P8YRkDKAb2EoMwsB5URiXFi9XoeAN/vWke0G6+w=="], + "@shikijs/engine-oniguruma/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], - "@storybook/builder-vite/storybook": ["storybook@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA=="], + "@shikijs/langs/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q=="], + "@shikijs/primitive/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg=="], + "@shikijs/themes/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + "@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.9.2", "", { "dependencies": { "@shikijs/types": "3.9.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3q/mzmw09B2B6PgFNeiaN8pkNOixWS726IHmJEpjDAcneDPMQmUg2cweT9cWXY4XcyQS3i6mOOUgQz9RRUP6HA=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], + "@solid-primitives/resize-observer/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.4", "", { "dependencies": { "@solid-primitives/utils": "^6.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TOIZa1VUfVJ+9nkCcRajw3U4t9vBOP1HxX1WHNTbXq32mXwlqTvUnC4CRIilohcryBkT9u2ZkhUDSHRTaGp55g=="], - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + "@storybook/addon-links/storybook": ["storybook@10.5.7", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", "jsonc-parser": "^3.3.1", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.21.1" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15 || ^0.2.0" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-oiKvWIwIoOhFP1i6dASYyMXwPHKEtVZMshqSB7EvIVYjWRh0l9H7gHEt1z4Gh2rLGFMekWdsm4s94rvwpR7gkg=="], + + "@storybook/addon-onboarding/storybook": ["storybook@10.5.7", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", "jsonc-parser": "^3.3.1", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.21.1" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15 || ^0.2.0" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-oiKvWIwIoOhFP1i6dASYyMXwPHKEtVZMshqSB7EvIVYjWRh0l9H7gHEt1z4Gh2rLGFMekWdsm4s94rvwpR7gkg=="], + + "@storybook/addon-vitest/storybook": ["storybook@10.5.7", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", "jsonc-parser": "^3.3.1", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.21.1" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15 || ^0.2.0" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-oiKvWIwIoOhFP1i6dASYyMXwPHKEtVZMshqSB7EvIVYjWRh0l9H7gHEt1z4Gh2rLGFMekWdsm4s94rvwpR7gkg=="], + + "@storybook/builder-vite/@storybook/csf-plugin": ["@storybook/csf-plugin@10.5.7", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.5.7", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-IaX8FlM0H36HNFhJ2+4L9bCldqfvHGqcLg841SJNyK/DhfMlM7JsvY/GDH2ZFuWrUf8FSOx96GRRnHq6XfRKag=="], + + "@storybook/builder-vite/storybook": ["storybook@10.5.7", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", "jsonc-parser": "^3.3.1", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.21.1" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15 || ^0.2.0" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-oiKvWIwIoOhFP1i6dASYyMXwPHKEtVZMshqSB7EvIVYjWRh0l9H7gHEt1z4Gh2rLGFMekWdsm4s94rvwpR7gkg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" }, "bundled": true }, "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -4596,35 +5288,43 @@ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], - "@textlint/linter-formatter/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "@textlint/linter-formatter/pluralize": ["pluralize@2.0.0", "", {}, "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw=="], "@textlint/linter-formatter/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=="], "@textlint/linter-formatter/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@ts-morph/common/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "@tufjs/models/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@vscode/test-cli/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=="], "@vscode/test-cli/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=="], "@vscode/test-cli/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - "@vscode/test-cli/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=="], + "@vscode/test-cli/yargs": ["yargs@17.7.3", "", { "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-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], "@vscode/vsce/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "@vscode/vsce/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - "@vscode/vsce/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], "@vscode/vsce/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], "@vscode/vsce/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "ai-gateway-provider/@ai-sdk/google": ["@ai-sdk/google@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CbR82EgGPNrj/6q0HtclwuCqe0/pDShyv3nWDP/A9DroujzWXnLMlUJVrgPOsg4b40zQCwwVs2XSKCxvt/4QaA=="], + "@vscode/vsce/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "archiver-utils/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + + "archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], "ava/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], @@ -4638,7 +5338,7 @@ "ava/write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], - "ava/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=="], + "ava/yargs": ["yargs@17.7.3", "", { "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-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], "axios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], @@ -4650,11 +5350,13 @@ "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=="], + "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "c12/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], - "c8/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=="], + "c8/yargs": ["yargs@17.7.3", "", { "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-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], "c8/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], @@ -4664,7 +5366,7 @@ "cheerio/htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], - "cheerio/undici": ["undici@7.27.2", "", {}, "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA=="], + "cheerio/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], "cheerio/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], @@ -4676,6 +5378,8 @@ "command-line-usage/typical": ["typical@7.3.0", "", {}, "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw=="], + "compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], @@ -4694,11 +5398,13 @@ "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "effect/uuid": ["uuid@13.0.2", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw=="], + "effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "effect/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "engine.io-client/ws": ["ws@8.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=="], + "engine.io-client/ws": ["ws@8.21.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw=="], "enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -4712,6 +5418,8 @@ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "fast-json-stringify/fast-uri": ["fast-uri@4.1.2", "", {}, "sha512-TyGmBcbDTZXcb2cj5MV89DrF42DKvb3y5DDUNh95iO+IMeAzMkVSxK1PZRrRIpc9yg8U2GhGdbofNa0LS/a4Bw=="], + "fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "figures/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -4720,7 +5428,7 @@ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "framer-motion/motion-dom": ["motion-dom@12.40.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg=="], + "framer-motion/motion-dom": ["motion-dom@12.43.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag=="], "framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], @@ -4736,15 +5444,17 @@ "gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - "gray-matter/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + "gray-matter/js-yaml": ["js-yaml@3.15.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag=="], "htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "ignore-walk/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "isomorphic-git/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -4766,6 +5476,8 @@ "knip/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "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/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], @@ -4780,6 +5492,8 @@ "markdown-it/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "marked-katex-extension/marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], + "matcher/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], @@ -4796,17 +5510,17 @@ "mocha/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "mocha/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=="], + "mocha/yargs": ["yargs@17.7.3", "", { "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-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], "mocha/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + "next/postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="], - "node-gyp/undici": ["undici@6.26.0", "", {}, "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A=="], + "node-gyp/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], "node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "node-sarif-builder/fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="], + "node-sarif-builder/fs-extra": ["fs-extra@11.4.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA=="], "normalize-package-data/hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], @@ -4814,7 +5528,9 @@ "nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], - "ora/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], + + "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], "ora/log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], @@ -4850,7 +5566,9 @@ "read-pkg/unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], - "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + "read-yaml-file/js-yaml": ["js-yaml@3.15.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag=="], + + "readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], "redent/indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], @@ -4864,12 +5582,16 @@ "sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "shiki/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], + "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], "spdx-correct/spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], + "sst/aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="], + "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], "storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], @@ -4884,7 +5606,7 @@ "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "supertap/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + "supertap/js-yaml": ["js-yaml@3.15.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag=="], "supports-hyperlinks/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -4896,25 +5618,25 @@ "table-layout/array-back": ["array-back@6.2.3", "", {}, "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw=="], + "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - "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=="], + "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=="], "test-exclude/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=="], - "thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="], + "test-exclude/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "to-buffer/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + "thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="], - "tree-sitter-bash/node-addon-api": ["node-addon-api@8.8.0", "", {}, "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA=="], + "tree-sitter-bash/node-addon-api": ["node-addon-api@8.9.1", "", {}, "sha512-4eUQWVPCUUUiBjLnHS3cXWeC6ryoPUc0U3rP7IuzapoGbzMqd/r6KKO0clr0b+snQhsrueFEhCZDdK+LK7hxKg=="], "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], "validate-npm-package-license/spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], - "venice-ai-sdk-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], - - "venice-ai-sdk-provider/@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=="], + "venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.35" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cQrN6OUn/jvsY3OdsU6Wn+ss7vp1iwIcakZKSlSRMnYqShBfyT7Qht+eqmgxs7w9ttrw6FAG6o11AiBs+iEsTA=="], "vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], @@ -4934,101 +5656,185 @@ "yargs-unparser/decamelize": ["decamelize@4.0.0", "", {}, "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ=="], + "yazl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + + "@actions/artifact/@actions/core/@actions/exec": ["@actions/exec@2.0.0", "", { "dependencies": { "@actions/io": "^2.0.0" } }, "sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw=="], + + "@ai-sdk/alibaba/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@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/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/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/openai-compatible/@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=="], + + "@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=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], - "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], - "@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], + "@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.12", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/credential-provider-env": "^3.972.67", "@aws-sdk/credential-provider-http": "^3.972.69", "@aws-sdk/credential-provider-login": "^3.972.74", "@aws-sdk/credential-provider-process": "^3.972.67", "@aws-sdk/credential-provider-sso": "^3.973.11", "@aws-sdk/credential-provider-web-identity": "^3.972.73", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-pNEf/OeyN5X3VmLKlgSO6TqaWmW10CvI3TfwL1XhsuhYjSLT2VDaxFnCPHnOeQXSaFisMX4jNhpETriqN8DOmg=="], - "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], - "@hey-api/openapi-ts/open/wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.12", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/credential-provider-env": "^3.972.67", "@aws-sdk/credential-provider-http": "^3.972.69", "@aws-sdk/credential-provider-login": "^3.972.74", "@aws-sdk/credential-provider-process": "^3.972.67", "@aws-sdk/credential-provider-sso": "^3.973.11", "@aws-sdk/credential-provider-web-identity": "^3.972.73", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-pNEf/OeyN5X3VmLKlgSO6TqaWmW10CvI3TfwL1XhsuhYjSLT2VDaxFnCPHnOeQXSaFisMX4jNhpETriqN8DOmg=="], - "@morphllm/morphsdk/openai/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@aws-sdk/client-lambda/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], - "@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], + "@aws-sdk/client-lambda/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], + "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.12", "", { "dependencies": { "@aws-sdk/core": "^3.977.6", "@aws-sdk/credential-provider-env": "^3.972.67", "@aws-sdk/credential-provider-http": "^3.972.69", "@aws-sdk/credential-provider-login": "^3.972.74", "@aws-sdk/credential-provider-process": "^3.972.67", "@aws-sdk/credential-provider-sso": "^3.973.11", "@aws-sdk/credential-provider-web-identity": "^3.972.73", "@aws-sdk/nested-clients": "^3.997.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-pNEf/OeyN5X3VmLKlgSO6TqaWmW10CvI3TfwL1XhsuhYjSLT2VDaxFnCPHnOeQXSaFisMX4jNhpETriqN8DOmg=="], - "@octokit/graphql/@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=="], + "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], - "@octokit/graphql/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@octokit/graphql/@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], - "@octokit/graphql/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], - "@octokit/plugin-request-log/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@octokit/plugin-request-log/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], + "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], - "@octokit/plugin-request-log/@octokit/core/@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="], + "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@octokit/plugin-request-log/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], - "@octokit/plugin-request-log/@octokit/core/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Qzq7zj9yXUgAAJEbbmqRhm0jmUndl8nHG0AbxFEfCfQRVZWL96Qzx0mf8lYwT9hIMrXncLwy31HOthmbXwFRwQ=="], - "@octokit/plugin-request-log/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], - "@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], + "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], - "@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], + "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], - "@octokit/rest/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@octokit/rest/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], - "@octokit/rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="], + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@octokit/rest/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], - "@octokit/rest/@octokit/core/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "@aws-sdk/credential-providers/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + "@aws-sdk/middleware-websocket/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], + + "@aws-sdk/middleware-websocket/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + + "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], + + "@aws-sdk/nested-clients/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + + "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], + + "@aws-sdk/token-providers/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="], + + "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="], + + "@hey-api/openapi-ts/open/wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], - "@opencode-ai/core/@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="], + "@joshwooding/vite-plugin-react-docgen-typescript/glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "@opencode-ai/core/@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=="], + "@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "@npmcli/package-json/glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], + + "@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], + + "@octokit/graphql/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="], + + "@octokit/graphql/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.1", "", { "dependencies": { "@octokit/types": "^17.0.0" } }, "sha512-+eaY7G2VVpSf2pc5Gn1+mph837V/d/TYTJAgWL9Tb0ogGYcpN3IlAVFgjL+Vv93F/sevrxkvsYCedtpLdcFLzA=="], + + "@octokit/graphql/@octokit/request/@octokit/types": ["@octokit/types@17.0.0", "", { "dependencies": { "@octokit/openapi-types": "^28.0.0" } }, "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q=="], + + "@octokit/graphql/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + + "@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], + + "@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], + + "@octokit/plugin-retry/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@12.11.0", "", {}, "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ=="], - "@opencode-ai/core/@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=="], + "@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], + + "@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], + + "@octokit/rest/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], - "@opencode-ai/core/@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@octokit/rest/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.4", "", { "dependencies": { "@octokit/request": "^10.0.13", "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-5s15CCiY8XXQ+FG+b1YQcl6Z2FA++nwAz/tg2VUrTmnMncP+2nnGUEYANImdnxsA2Fnq+Mbl7hDjUTw7cFAwcg=="], - "@opencode-ai/core/@ai-sdk/google/@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=="], + "@octokit/rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.13", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.1.1", "@octokit/types": "^17.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-v2269YxL9Yf+x3d+gRI63FP0vFQEiWgLyBzxe/Y+0yFDg2B/Tzf5dhh9VNfccVAQnfcfwQWyk/y6Bn7rUXXs7A=="], - "@opencode-ai/core/@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.77", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ML8C2M1YvPA1ulEx4TiyF0k1xvC2ikEiPBIC1PPQ0a5xELUGrO2lAaEzsTEoJ+eCeDd8PSBuFJjs+r+9yIwQXA=="], + "@octokit/rest/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.1.1", "", { "dependencies": { "@octokit/types": "^17.0.0" } }, "sha512-+eaY7G2VVpSf2pc5Gn1+mph837V/d/TYTJAgWL9Tb0ogGYcpN3IlAVFgjL+Vv93F/sevrxkvsYCedtpLdcFLzA=="], - "@opencode-ai/core/@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg=="], + "@octokit/rest/@octokit/core/@octokit/types": ["@octokit/types@17.0.0", "", { "dependencies": { "@octokit/openapi-types": "^28.0.0" } }, "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q=="], - "@opencode-ai/core/@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], - "@opencode-ai/core/@ai-sdk/google-vertex/@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=="], + "@opencode-ai/core/@ai-sdk/cerebras/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], - "@opencode-ai/core/@aws-sdk/credential-providers/@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.993.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@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.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7Ne3Yk/bgQPVebAkv7W+RfhiwTRSbfER9BtbhOa2w/+dIr902LrJf6vrZlxiqaJbGj2ALx8M+ZK1YIHVxSwu9A=="], + "@opencode-ai/core/@ai-sdk/cerebras/@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=="], - "@opencode-ai/core/@aws-sdk/credential-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.993.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@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.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-iOq86f2H67924kQUIPOAvlmMaOAvOLoDOIb66I2YqSUpMYB6ufiuJW3RlREgskxv86S5qKzMnfy/X6CqMjK6XQ=="], + "@opencode-ai/session-ui/@solid-primitives/resize-observer/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.4", "", { "dependencies": { "@solid-primitives/utils": "^6.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TOIZa1VUfVJ+9nkCcRajw3U4t9vBOP1HxX1WHNTbXq32mXwlqTvUnC4CRIilohcryBkT9u2ZkhUDSHRTaGp55g=="], - "@opencode-ai/core/venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.29" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HyuxddF2Yv5G8qxK/0uksAINjQ4h6TpwOqHuqzsCM0u78/JWAW2OXcIplQeB44PIAORgPjbMzrw9DhnPYHMskA=="], + "@opencode-ai/storybook/@storybook/addon-docs/@storybook/csf-plugin": ["@storybook/csf-plugin@10.5.7", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.5.7", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-IaX8FlM0H36HNFhJ2+4L9bCldqfvHGqcLg841SJNyK/DhfMlM7JsvY/GDH2ZFuWrUf8FSOx96GRRnHq6XfRKag=="], - "@opencode-ai/core/venice-ai-sdk-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@opencode-ai/storybook/@storybook/addon-docs/@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.5.7", "", { "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.5.7" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lxOkyh+wu/MiBXvYQHjZfD+DRKOa4bHBzbuGuiHXnHXmdOcTRdcrQTsoeN2FPtfugmmOG66cZUEgDwNX+k5eRA=="], - "@opencode-ai/core/venice-ai-sdk-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.29", "", { "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-uhukHaCBvqkwBHkT8C2PrnqKTCoLn3pdHXqtcR9I8ErH+flbzgW4o7VHSNIup9LRu+WBvZIZDQLsx6rwl2tiOA=="], + "@opencode-ai/storybook/@storybook/addon-docs/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], - "@opencode-ai/storybook/@storybook/addon-docs/@storybook/csf-plugin": ["@storybook/csf-plugin@10.4.4", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.4.4", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-1mzZyAwVUmAcw4WEUsJDVdSupkJf+Kf/f5uNAs4RzlBXA75P8YRkDKAb2EoMwsB5URiXFi9XoeAN/vWke0G6+w=="], + "@opencode-ai/storybook/@storybook/addon-docs/react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], - "@opencode-ai/storybook/@storybook/addon-docs/@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.4.4", "", { "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.4.4" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-y6SObmoW78AydE6VfKQSUmCkuqiaMPy9LgMpMdMEyWfJ/pSxBDMIKycr9dlRMJP1cvNgByaJgrusWtA46ndSQw=="], + "@opencode-ai/storybook/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], - "@opencode-ai/storybook/@storybook/addon-docs/react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + "@opencode-ai/storybook/storybook/@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=="], "@opencode-ai/storybook/storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], @@ -5036,7 +5842,15 @@ "@opencode-ai/storybook/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=="], - "@opencode-ai/ui/@solid-primitives/resize-observer/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="], + "@opencode-ai/storybook/storybook/ws": ["ws@8.21.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw=="], + + "@opencode-ai/ui/@solid-primitives/resize-observer/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.4", "", { "dependencies": { "@solid-primitives/utils": "^6.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TOIZa1VUfVJ+9nkCcRajw3U4t9vBOP1HxX1WHNTbXq32mXwlqTvUnC4CRIilohcryBkT9u2ZkhUDSHRTaGp55g=="], + + "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + + "@storybook/addon-links/storybook/@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=="], "@storybook/addon-links/storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], @@ -5044,24 +5858,38 @@ "@storybook/addon-links/storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "@storybook/addon-links/storybook/ws": ["ws@8.21.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw=="], + + "@storybook/addon-onboarding/storybook/@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=="], + "@storybook/addon-onboarding/storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], "@storybook/addon-onboarding/storybook/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], "@storybook/addon-onboarding/storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "@storybook/addon-onboarding/storybook/ws": ["ws@8.21.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw=="], + + "@storybook/addon-vitest/storybook/@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=="], + "@storybook/addon-vitest/storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], "@storybook/addon-vitest/storybook/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], "@storybook/addon-vitest/storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "@storybook/addon-vitest/storybook/ws": ["ws@8.21.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw=="], + + "@storybook/builder-vite/storybook/@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=="], + "@storybook/builder-vite/storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], "@storybook/builder-vite/storybook/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], "@storybook/builder-vite/storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "@storybook/builder-vite/storybook/ws": ["ws@8.21.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw=="], + "@tailwindcss/vite/@tailwindcss/node/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=="], "@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.11", "", { "os": "android", "cpu": "arm64" }, "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg=="], @@ -5090,8 +5918,6 @@ "@tailwindcss/vite/@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "@textlint/linter-formatter/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@textlint/linter-formatter/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "@textlint/linter-formatter/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -5104,7 +5930,7 @@ "@vscode/test-cli/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=="], - "@vscode/test-cli/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], + "@vscode/test-cli/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], "@vscode/test-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=="], @@ -5116,6 +5942,12 @@ "@vscode/vsce/hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + "archiver-utils/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "archiver-utils/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "archiver-utils/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=="], + "ava/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "ava/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -5138,12 +5970,14 @@ "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=="], - "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + "c12/chokidar/readdirp": ["readdirp@5.1.1", "", {}, "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="], "c8/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=="], "c8/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=="], + "cacache/glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "chalk-template/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "cli-truncate/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], @@ -5214,13 +6048,13 @@ "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "eslint/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], + "eslint/minimatch/brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="], "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "friendly-words/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=="], - "friendly-words/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=="], + "friendly-words/express/body-parser": ["body-parser@1.20.6", "", { "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-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="], "friendly-words/express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], @@ -5234,6 +6068,8 @@ "friendly-words/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + "friendly-words/express/range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + "friendly-words/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=="], "friendly-words/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=="], @@ -5246,17 +6082,17 @@ "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "mocha/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "mocha/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=="], - "mocha/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], + "mocha/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], "mocha/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=="], @@ -5288,6 +6124,8 @@ "qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], + "readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], + "rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "rimraf/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -5308,39 +6146,49 @@ "table/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=="], + "test-exclude/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "test-exclude/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], "test-exclude/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=="], + "venice-ai-sdk-provider/@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@3.0.13", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-ZPtVYt5QIJzOta1kdUiDuCx4HhFkvNPv/rvmZ2b1iXwybYjJsCnNYR4PAw4kW7rgVfDARvHXcU64efWuqNp6bw=="], + + "venice-ai-sdk-provider/@ai-sdk/openai-compatible/@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=="], + + "vite-plugin-icons-spritesheet/glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@eslint/config-array/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "@actions/artifact/@actions/core/@actions/exec/@actions/io": ["@actions/io@2.0.0", "", {}, "sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg=="], - "@eslint/eslintrc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "@manypkg/find-root/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "@octokit/graphql/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "@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=="], + + "@eslint/config-array/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "@octokit/plugin-request-log/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], + "@eslint/eslintrc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "@octokit/plugin-request-log/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@manypkg/find-root/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - "@octokit/plugin-request-log/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "@octokit/graphql/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="], - "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], + "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="], "@octokit/rest/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="], - "@opencode-ai/core/@aws-sdk/credential-providers/@aws-sdk/client-cognito-identity/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.993.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-j6vioBeRZ4eHX4SWGvGPpwGg/xSOcK7f1GL0VM+rdf3ZFTIsUEhCFmD78B+5r2PgztcECSzEfvHQX01k8dPQPw=="], - - "@opencode-ai/core/@aws-sdk/credential-providers/@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.993.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-j6vioBeRZ4eHX4SWGvGPpwGg/xSOcK7f1GL0VM+rdf3ZFTIsUEhCFmD78B+5r2PgztcECSzEfvHQX01k8dPQPw=="], + "@opencode-ai/core/@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@opencode-ai/storybook/storybook/@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], @@ -5394,11 +6242,11 @@ "@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], - "@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q=="], + "@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], - "@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg=="], + "@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], - "@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=="], + "@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="], "@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], @@ -5420,7 +6268,11 @@ "@vscode/test-cli/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@vscode/vsce/hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "archiver-utils/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], + + "archiver-utils/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "ava/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -5430,7 +6282,7 @@ "ava/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "babel-plugin-module-resolver/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], + "babel-plugin-module-resolver/glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], "babel-plugin-module-resolver/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -5494,9 +6346,11 @@ "qrcode/yargs/yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + "readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "rimraf/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=="], - "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], + "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], "rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -5504,10 +6358,12 @@ "test-exclude/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=="], - "test-exclude/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], + "test-exclude/glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], "test-exclude/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "venice-ai-sdk-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@manypkg/find-root/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "@opencode-ai/storybook/storybook/@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], @@ -5520,7 +6376,7 @@ "@storybook/builder-vite/storybook/@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], - "@tailwindcss/vite/@tailwindcss/oxide/@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=="], + "@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], "@vscode/test-cli/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -5530,6 +6386,12 @@ "@vscode/test-cli/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "archiver-utils/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "archiver-utils/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "archiver-utils/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "ava/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ava/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -5580,6 +6442,10 @@ "@vscode/test-cli/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "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=="], + "mocha/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "mocha/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], diff --git a/bunfig.toml b/bunfig.toml index 6b1e40c73f5..61bb6e0fdb3 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -2,7 +2,7 @@ exact = true # Keep Kilo's longer supply-chain quarantine while allowing packages that must track coordinated releases. minimumReleaseAge = 410520 # seconds (~4.75 days / ~114 hours) -minimumReleaseAgeExcludes = ["mermaid", "@mermaid-js/parser", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-x64", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid"] +minimumReleaseAgeExcludes = ["mermaid", "@mermaid-js/parser", "@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish"] [test] root = "./do-not-run-tests-from-root" diff --git a/docs/jetbrains-vscode-settings-parity.md b/docs/jetbrains-vscode-settings-parity.md new file mode 100644 index 00000000000..5388c36c9bf --- /dev/null +++ b/docs/jetbrains-vscode-settings-parity.md @@ -0,0 +1,85 @@ +# JetBrains ↔ VS Code Settings Parity: Easy Wins + +## How parity works here + +Both clients edit the **same shared `kilo.json`** through the CLI. So any setting whose +behavior lives entirely in the CLI is an "easy win" for JetBrains: the CLI already does the +work, JetBrains just needs a UI row that writes the config key. No CLI changes, no new feature. + +Structural gap: today JetBrains only has **Models / Providers / Agent Behavior / Profile** +settings pages. There is **no General / Display / Experimental / Context / Checkpoints** page. +The lift for most easy wins is: + +1. Add a new `Configurable` page (using existing `settings/base/` primitives — + `BaseSettingsUi`, `SettingsRow`, `SettingsToggle`, `SettingsListPanel`), register it in + `kilo.jetbrains.frontend.xml`. +2. Extend the `buildConfigPatch` allowlist in `KiloCliDataParser.kt` (currently only + `model`, `small_model`, `subagent_model`, `subagent_variant`, `default_agent`) and add + boolean/number JSON serialization — it currently only emits strings. +3. Add localized labels to `KiloBundle.properties`. + +No CLI/SDK change and no new runtime feature. + +## Excluded from "easy" + +| Excluded | Reason | +|---|---| +| Agent Behavior, Auto-Approve | Skipped by request | +| Indexing, Sandboxing | Imply enabling new features | +| Browser Automation | Playwright feature not present in JetBrains | +| Autocomplete (provider/model/toggles) | No autocomplete feature (flags exist only as migration stubs) | +| Agent Manager (auto-branch, prefix) | VS Code-only feature | +| Notification/attention sounds | Client must implement sound playback | +| `maxCost` alert | Client must render the alert UI | +| Commit message (`commit_message.prompt`, `languageCommitMessage`) | No commit-message generation feature in JetBrains | +| `language`, `fontSize`, `diff.renderMarkdown`, `agentWorkStyle` | VS Code-webview/onboarding-specific | + +## Tier 1 — Genuinely easy (CLI does all the work; just add UI + config key) + +| Setting | Config key | Type | Suggested page | +|---|---|---|---| +| Hide prompt-training models | `hide_prompt_training_models` | bool | Models | +| Enable checkpoints | `snapshot` | bool | new "Checkpoints" | +| Auto-compaction | `compaction.auto` | bool | new "Context" | +| Compaction threshold % | `compaction.threshold_percent` | number | Context | +| Prune on compaction | `compaction.prune` | bool | Context | +| Watcher ignore patterns | `watcher.ignore` | string[] | Context (list editor) | +| Display username | `username` | string | new "Display/General" | +| Share mode | `share` | enum (manual/auto/disabled) | new "Experimental" | +| Remote control on startup | `remote_control` | bool | Experimental | +| Formatter integration | `formatter` | bool | Experimental | +| LSP integration | `lsp` | bool | Experimental | +| Batch tool | `experimental.batch_tool` | bool | Experimental | +| Native notebook tools | `experimental.native_notebook_tools` | bool | Experimental | +| Continue loop on deny | `experimental.continue_loop_on_deny` | bool | Experimental | +| MCP timeout | `experimental.mcp_timeout` | number | Experimental | +| Per-tool toggles | `tools.` | bool | Experimental | + +**Claude Code compatibility**: lives under "Agent Behavior" in VS Code, but in JetBrains the +entire backend (`KiloClaudeCompatSettings` + RPC getter/setter + spawn-env wiring) already +exists with no UI. Exposing it is the single lowest-effort item — just a checkbox bound to the +existing RPC, no config plumbing. + +⚠️ Hold back `experimental.image_generation` (adds a tool) — arguably "enabling a feature." + +## Tier 2 — Config is easy, but honoring it needs JetBrains rendering work + +| Setting | Config key | Extra work | +|---|---|---| +| Auto-collapse reasoning | `auto_collapse_reasoning` | Reasoning-card default collapse | +| Terminal command display | `terminal_command_display` (expanded/collapsed) | Tool-card default state | +| Code edit display | `code_edit_display` (expanded/collapsed) | Edit-card default state | + +## Recommendation + +Add a new **"General/Display" page + "Experimental" page** driven entirely by CLI config, +seeded with Tier 1 behavioral settings, plus wire the already-built **Claude Code compat** +toggle. This closes most of the non-feature gap with: + +- zero CLI/SDK changes, +- one allowlist extension in `KiloCliDataParser.buildConfigPatch` (add keys + boolean/number serialization), +- reuse of existing `settings/base/` UI primitives and test patterns (`FakeAppRpcApi` + frontend test + `MockCliServer` backend body assertion). + +Do Tier 2 (reasoning/terminal/edit display defaults) after Tier 1, since it touches the +session-rendering layer rather than being pure config. diff --git a/flake.nix b/flake.nix index 7383da725db..280d60035c5 100644 --- a/flake.nix +++ b/flake.nix @@ -21,59 +21,7 @@ devShells = forEachSystem (pkgs: { default = let - # Pin bun to the version declared in package.json (packageManager: "bun@1.3.14"). - # The locked nixpkgs revision ships 1.3.11, so we fetch the official release directly. - bun = - let - sources = { - "aarch64-linux" = { - name = "bun-linux-aarch64"; - hash = "sha256-on/7Y6gxA3WDbg1vZorhf6jY0YuIw3yCHGUzGXOhmjs="; - }; - "x86_64-linux" = { - name = "bun-linux-x64"; - hash = "sha256-lR7iruhV8IWVruxiJSJqKY0/6oOj3NZGXAnLzN9+hI8="; - }; - "aarch64-darwin" = { - name = "bun-darwin-aarch64"; - hash = "sha256-2LliIYKK1vl6x6wKt+lYcjQa92MAHogD6CZ2UsJlJiA="; - }; - "x86_64-darwin" = { - name = "bun-darwin-x64"; - hash = "sha256-QYPfM3RiPlurMVxUfPoJdFM81FfYa3O2OfeoeXTNZjM="; - }; - }; - source = - sources.${pkgs.stdenv.hostPlatform.system} - or (throw "Unsupported system for bun: ${pkgs.stdenv.hostPlatform.system}"); - in - pkgs.stdenv.mkDerivation rec { - pname = "bun"; - version = "1.3.14"; - src = pkgs.fetchurl { - url = "https://github.com/oven-sh/bun/releases/download/bun-v${version}/${source.name}.zip"; - inherit (source) hash; - }; - nativeBuildInputs = [ - pkgs.unzip - ] ++ pkgs.lib.optional pkgs.stdenv.isLinux pkgs.autoPatchelfHook; - buildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.stdenv.cc.cc.lib ]; - dontConfigure = true; - dontBuild = true; - installPhase = '' - runHook preInstall - install -Dm755 bun $out/bin/bun - ln -s $out/bin/bun $out/bin/bunx - runHook postInstall - ''; - meta = { - description = "Fast all-in-one JavaScript runtime"; - homepage = "https://bun.sh"; - license = pkgs.lib.licenses.mit; - mainProgram = "bun"; - platforms = builtins.attrNames sources; - }; - }; + bun = pkgs.callPackage ./nix/bun.nix { }; kilo-dev = pkgs.writeShellScriptBin "kilo-dev" '' set -euo pipefail @@ -279,11 +227,12 @@ packages = forEachSystem ( pkgs: let + bun = pkgs.callPackage ./nix/bun.nix { }; node_modules = pkgs.callPackage ./nix/node_modules.nix { - inherit rev; + inherit bun rev; }; kilo = pkgs.callPackage ./nix/kilo.nix { - inherit node_modules; + inherit bun node_modules; }; in { diff --git a/install b/install index f78cbc8d1b6..a12dfea5497 100755 --- a/install +++ b/install @@ -183,13 +183,17 @@ else fi if [ -z "$requested_version" ]; then - url="https://github.com/Kilo-Org/kilocode/releases/latest/download/$filename" - specific_version=$(curl -s https://api.github.com/repos/Kilo-Org/kilocode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p') - - if [[ $? -ne 0 || -z "$specific_version" ]]; then + # The npm latest tag tracks stable CLI releases independently of other GitHub release types. + if ! specific_version=$(curl -fsSL "https://registry.npmjs.org/-/package/%40kilocode%2Fcli/dist-tags" | + sed -n 's/.*"latest"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'); then + echo -e "${RED}Failed to fetch version information${NC}" + exit 1 + fi + if [[ ! "$specific_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo -e "${RED}Failed to fetch version information${NC}" exit 1 fi + url="https://github.com/Kilo-Org/kilocode/releases/download/v${specific_version}/$filename" else # Strip leading 'v' if present requested_version="${requested_version#v}" diff --git a/nix/bun.nix b/nix/bun.nix new file mode 100644 index 00000000000..a8808953494 --- /dev/null +++ b/nix/bun.nix @@ -0,0 +1,60 @@ +{ + lib, + stdenv, + fetchurl, + unzip, + autoPatchelfHook, +}: +let + package = lib.pipe ../package.json [ + builtins.readFile + builtins.fromJSON + ]; + version = lib.removePrefix "bun@" package.packageManager; + sources = { + "aarch64-linux" = { + name = "bun-linux-aarch64"; + hash = "sha256-on/7Y6gxA3WDbg1vZorhf6jY0YuIw3yCHGUzGXOhmjs="; + }; + "x86_64-linux" = { + name = "bun-linux-x64"; + hash = "sha256-lR7iruhV8IWVruxiJSJqKY0/6oOj3NZGXAnLzN9+hI8="; + }; + "aarch64-darwin" = { + name = "bun-darwin-aarch64"; + hash = "sha256-2LliIYKK1vl6x6wKt+lYcjQa92MAHogD6CZ2UsJlJiA="; + }; + "x86_64-darwin" = { + name = "bun-darwin-x64"; + hash = "sha256-QYPfM3RiPlurMVxUfPoJdFM81FfYa3O2OfeoeXTNZjM="; + }; + }; + source = + sources.${stdenv.hostPlatform.system} + or (throw "Unsupported system for bun: ${stdenv.hostPlatform.system}"); +in +stdenv.mkDerivation { + pname = "bun"; + inherit version; + src = fetchurl { + url = "https://github.com/oven-sh/bun/releases/download/bun-v${version}/${source.name}.zip"; + inherit (source) hash; + }; + nativeBuildInputs = [ unzip ] ++ lib.optional stdenv.isLinux autoPatchelfHook; + buildInputs = lib.optionals stdenv.isLinux [ stdenv.cc.cc.lib ]; + dontConfigure = true; + dontBuild = true; + installPhase = '' + runHook preInstall + install -Dm755 bun $out/bin/bun + ln -s $out/bin/bun $out/bin/bunx + runHook postInstall + ''; + meta = { + description = "Fast all-in-one JavaScript runtime"; + homepage = "https://bun.sh"; + license = lib.licenses.mit; + mainProgram = "bun"; + platforms = builtins.attrNames sources; + }; +} diff --git a/nix/hashes.json b/nix/hashes.json index 9bf86577db8..254761e6717 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-WYLCq59M4c6MsNjIeBmgV66v6LBvsTVGA1xV2oY935s=", - "aarch64-linux": "sha256-kpJ1SFCmBrhDwuJNJNoDU5NK6mDvEG4Yd41w9YJnsOs=", - "aarch64-darwin": "sha256-junSolEGOe+xUmPY4SAJGj+XKuytxvD0IWQWtLiaIEs=", - "x86_64-darwin": "sha256-p+3Jtb5CZexwx0UlhVeqeMr25R+jkjahFp9YTZYK0rE=" + "x86_64-linux": "sha256-9riExKYPZOz7RqMyrtEMiJyxbtBHnCRkwoBrx825t6k=", + "aarch64-linux": "sha256-mCu+gs0rZGv2PRQlojZR+g3TnRXjMdiN0ORrnUwFbRU=", + "aarch64-darwin": "sha256-boQX3rpYs2vgRzIgVwEeyAKJ8b3MzbBbrq8GL5YkKE8=", + "x86_64-darwin": "sha256-flbEqrgSGWDHfuTeVTv3iV8wYCeDHpPeqkj5Mq5WXy0=" } } diff --git a/nix/kilo.nix b/nix/kilo.nix index 9d01a0ee26a..8ad056e09d7 100644 --- a/nix/kilo.nix +++ b/nix/kilo.nix @@ -3,6 +3,7 @@ stdenvNoCC, callPackage, bun, + bubblewrap, nodejs, sysctl, makeBinaryWrapper, @@ -39,6 +40,7 @@ stdenvNoCC.mkDerivation (finalAttrs: { env.MODELS_DEV_API_JSON = "${models-dev}/dist/_api.json"; env.KILO_DISABLE_MODELS_FETCH = true; + env.KILO_SKIP_BUNDLED_BWRAP = "1"; env.KILO_VERSION = finalAttrs.version; env.KILO_CHANNEL = "local"; @@ -59,6 +61,7 @@ stdenvNoCC.mkDerivation (finalAttrs: { install -Dm644 schema.json $out/share/kilo/schema.json wrapProgram $out/bin/kilo \ + ${lib.optionalString stdenvNoCC.hostPlatform.isLinux "--set KILO_BWRAP_PATH ${bubblewrap}/bin/bwrap"} \ --prefix PATH : ${ lib.makeBinPath ( [ @@ -97,7 +100,7 @@ stdenvNoCC.mkDerivation (finalAttrs: { meta = { description = "AI-powered development tool"; homepage = "https://kilo.ai/"; - license = lib.licenses.mit; + license = [ lib.licenses.mit ] ++ lib.optional stdenvNoCC.hostPlatform.isLinux lib.licenses.lgpl2Plus; mainProgram = "kilo"; inherit (node_modules.meta) platforms; }; diff --git a/package.json b/package.json index f725c678223..efbc37aeb8c 100644 --- a/package.json +++ b/package.json @@ -6,18 +6,24 @@ "type": "module", "packageManager": "bun@1.3.14", "scripts": { - "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", + "dev": "KILO_CLIENT=cli bun run --cwd packages/opencode --conditions=node src/index.ts", + "dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev", "dev:storybook": "bun --cwd packages/storybook storybook", "lint": "oxlint", + "check:architecture": "bun run script/check-architecture.ts", "typecheck": "bun turbo typecheck", "upgrade-opentui": "bun run script/upgrade-opentui.ts", - "postinstall": "bun run --cwd packages/opencode fix-node-pty && bun run script/setup-git.ts", + "postinstall": "bun run --cwd packages/core fix-node-pty && bun run script/setup-git.ts", "prepare": "husky", "random": "echo 'Random script'", - "hello": "echo 'Hello World!'", + "sso": "aws sso login --sso-session=opencode --no-browser", "test": "echo 'do not run tests from root' && exit 1", + "test:script:ci": "mkdir -p .artifacts/unit && bun test ./script --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml", "extension": "bun --cwd packages/kilo-vscode script/launch.ts", - "dev-setup": "bun run --cwd packages/opencode --conditions=browser src/index.ts dev-setup" + "extension:isolated": "bun --cwd packages/kilo-vscode script/launch.ts --isolated", + "extension:isolated:clean": "bun --cwd packages/kilo-vscode script/launch.ts --isolated --clean", + "dev-setup": "bun run --cwd packages/opencode --conditions=browser src/index.ts dev-setup", + "dev:local": "bun run packages/opencode/script/dev-local.ts" }, "workspaces": { "packages": [ @@ -25,14 +31,15 @@ "packages/sdk/js" ], "catalog": { - "@effect/opentelemetry": "4.0.0-beta.66", - "@effect/platform-node": "4.0.0-beta.66", + "@effect/opentelemetry": "4.0.0-beta.83", + "@effect/platform-node": "4.0.0-beta.83", + "@anthropic-ai/sandbox-runtime": "0.0.63", "@npmcli/arborist": "9.4.0", "@types/bun": "1.3.14", "@types/cross-spawn": "6.0.6", "@octokit/rest": "22.0.0", - "@opentui/core": "0.2.15", - "@opentui/solid": "0.2.15", + "@opentui/core": "0.4.5", + "@opentui/solid": "0.4.5", "ulid": "3.0.1", "@kobalte/core": "0.13.11", "@types/luxon": "3.7.1", @@ -42,22 +49,22 @@ "@tsconfig/bun": "1.0.9", "@cloudflare/workers-types": "4.20251008.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/diffs": "1.1.22", - "opentui-spinner": "0.0.6", + "@pierre/diffs": "1.2.10", "@solid-primitives/storage": "4.3.3", "@tailwindcss/vite": "4.1.11", "diff": "8.0.4", "dompurify": "3.4.2", "drizzle-kit": "1.0.0-rc.2", "drizzle-orm": "1.0.0-rc.2", - "effect": "4.0.0-beta.66", - "ai": "6.0.168", + "effect": "4.0.0-beta.83", + "ai": "6.0.235", "cross-spawn": "7.0.6", "hono": "4.12.12", "hono-openapi": "1.1.2", + "ipaddr.js": "2.4.0", "fuzzysort": "3.1.0", "luxon": "3.6.1", - "marked": "17.0.1", + "marked": "18.0.7", "marked-shiki": "1.2.1", "remend": "1.3.0", "@playwright/test": "1.59.1", @@ -66,19 +73,26 @@ "@typescript/native-preview": "7.0.0-dev.20260316.1", "zod": "4.1.8", "remeda": "2.26.0", - "shiki": "3.20.0", + "shiki": "4.2.0", "solid-list": "0.3.0", "tailwindcss": "4.1.11", - "virtua": "0.42.3", - "vite": "7.3.2", + "virtua": "0.49.1", + "vite": "7.3.5", "@solidjs/meta": "0.29.4", "@solidjs/router": "0.15.4", "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020", "solid-js": "1.9.12", "vite-plugin-solid": "2.11.10", "@lydell/node-pty": "1.2.0-beta.12", - "@opentui/keymap": "0.2.15", - "@effect/sql-sqlite-bun": "4.0.0-beta.66" + "@opentui/keymap": "0.4.5", + "@effect/sql-sqlite-bun": "4.0.0-beta.83", + "@hono/standard-validator": "0.2.0", + "@hono/zod-validator": "0.4.2", + "sst": "4.13.1", + "@tanstack/solid-virtual": "3.13.32", + "@shikijs/stream": "4.2.0", + "@corvu/drawer": "0.2.4", + "solid-sonner": "0.3.1" } }, "devDependencies": { @@ -91,16 +105,20 @@ "oxlint-tsgolint": "0.21.0", "prettier": "3.6.2", "semver": "^7.6.0", - "turbo": "2.9.14", + "turbo": "2.10.2", "@types/bun": "catalog:", "@changesets/changelog-github": "^0.5.1", - "@changesets/cli": "^2.27.10" + "@changesets/cli": "^2.27.10", + "@actions/artifact": "5.0.1", + "sst": "catalog:" }, "dependencies": { "@kilocode/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", "@kilocode/sdk": "workspace:*", - "typescript": "catalog:" + "typescript": "catalog:", + "@aws-sdk/client-s3": "3.933.0", + "heap-snapshot-toolkit": "1.1.3" }, "repository": { "type": "git", @@ -117,7 +135,6 @@ "protobufjs", "tree-sitter", "tree-sitter-bash", - "tree-sitter-powershell", "web-tree-sitter", "electron" ], @@ -125,7 +142,7 @@ "@types/bun": "catalog:", "@types/node": "catalog:", "effect": "catalog:", - "@effect/platform-node-shared": "4.0.0-beta.46", + "@effect/platform-node-shared": "4.0.0-beta.74", "path-to-regexp": ">=8.4.0", "picomatch": ">=2.3.2", "defu": "6.1.6", @@ -133,6 +150,7 @@ "@xmldom/xmldom": ">=0.8.12", "smol-toml": ">=1.6.1", "fastify": ">=5.8.3", + "vite": "7.3.5", "diff": "8.0.4", "dompurify": "3.4.2", "happy-dom": ">=20.8.9", @@ -142,12 +160,23 @@ "@opentui/keymap": "catalog:" }, "patchedDependencies": { - "@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.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", - "mammoth@1.12.0": "patches/mammoth@1.12.0.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", + "@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", + "virtua@0.49.1": "patches/virtua@0.49.1.patch", + "@ff-labs/fff-bun@0.9.4": "patches/@ff-labs%2Ffff-bun@0.9.4.patch", + "pacote@21.5.1": "patches/pacote@21.5.1.patch", + "mammoth@1.12.0": "patches/mammoth@1.12.0.patch", + "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", + "solid-js@1.9.12": "patches/solid-js@1.9.12.patch" }, - "version": "7.3.54", + "version": "7.4.23", "peerDependencies": {} } diff --git a/packages/client/README.md b/packages/client/README.md new file mode 100644 index 00000000000..8c53e47c7e6 --- /dev/null +++ b/packages/client/README.md @@ -0,0 +1,27 @@ +# @opencode-ai/client + +Private generation target for clients derived directly from OpenCode's authoritative Effect `HttpApi`. + +## Entrypoints + +- `@opencode-ai/client`: zero-Effect Promise client using `fetch`. +- `@opencode-ai/client/effect`: rich Effect network client using an environment-provided `HttpClient`. + +The generated surface includes every standard HTTP group from Server's concrete API. The build compiler reads `@opencode-ai/server/api`; the generated Effect runtime imports a client-local projection built from Protocol, with a generation-equivalence test preventing transport drift. Custom transports such as the PTY WebSocket connection remain outside the generic HTTP client. Run `bun run generate` after changing the contract and `bun run check:generated` to detect committed-output drift. + +The Effect entrypoint uses canonical decoded values such as `Session.ID`, `Location.Ref`, and `Prompt`. These datatypes come from the lightweight `@opencode-ai/schema` package and are re-exported so callers depend only on the client surface. Protocol owns endpoint construction and middleware placement; Server supplies the concrete middleware keys used by the build-time API. + +The Promise root remains structural and has no Core or Effect runtime dependency. `/effect` depends only on Effect, Schema, and Protocol and is browser-bundle safe. Bundle-boundary tests enforce both import graphs. + +Effect consumers construct canonical decoded inputs: + +```ts +import { AbsolutePath, Location, OpenCode, Prompt } from "@opencode-ai/client/effect" + +const client = yield * OpenCode.make({ baseUrl: "https://opencode.example" }) +yield * + client.sessions.create({ + location: Location.Ref.make({ directory: AbsolutePath.make("/workspace") }), + }) +yield * client.sessions.prompt({ sessionID, prompt: Prompt.make({ text: "Hello" }) }) +``` diff --git a/packages/client/package.json b/packages/client/package.json new file mode 100644 index 00000000000..59cc5fd1db2 --- /dev/null +++ b/packages/client/package.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@opencode-ai/client", + "private": true, + "type": "module", + "license": "MIT", + "exports": { + ".": "./src/index.ts", + "./promise": "./src/index.ts", + "./effect": "./src/effect.ts" + }, + "scripts": { + "generate": "bun run script/build.ts", + "check:generated": "bun run generate && git diff --exit-code -- src/generated src/generated-effect", + "test": "bun test --timeout 5000", + "typecheck": "tsgo --noEmit", + "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 10000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml" + }, + "dependencies": { + "@opencode-ai/schema": "workspace:*", + "@opencode-ai/protocol": "workspace:*" + }, + "peerDependencies": { + "effect": "4.0.0-beta.83" + }, + "peerDependenciesMeta": { + "effect": { + "optional": true + } + }, + "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:" + }, + "version": "7.4.23" +} diff --git a/packages/client/script/build.ts b/packages/client/script/build.ts new file mode 100644 index 00000000000..aeec4b3e345 --- /dev/null +++ b/packages/client/script/build.ts @@ -0,0 +1,30 @@ +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 { Effect } from "effect" +import { fileURLToPath } from "url" + +const contract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints }) + +await Effect.runPromise( + Effect.all( + [ + write( + emitPromise(contract, { + outputTypes: { + "events.subscribe": { + name: "OpenCodeEventEncoded", + import: 'import type { OpenCodeEventEncoded } from "@opencode-ai/protocol/groups/event"', + }, + }, + }), + fileURLToPath(new URL("../src/generated", import.meta.url)), + ), + write( + emitEffectImported(contract, { module: "../contract", api: "ClientApi" }), + fileURLToPath(new URL("../src/generated-effect", import.meta.url)), + ), + ], + { concurrency: 2, discard: true }, + ).pipe(Effect.provide(NodeFileSystem.layer)), +) diff --git a/packages/client/src/contract.ts b/packages/client/src/contract.ts new file mode 100644 index 00000000000..413fea9dc33 --- /dev/null +++ b/packages/client/src/contract.ts @@ -0,0 +1,53 @@ +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"]) diff --git a/packages/client/src/effect.ts b/packages/client/src/effect.ts new file mode 100644 index 00000000000..b580c7f48ac --- /dev/null +++ b/packages/client/src/effect.ts @@ -0,0 +1,25 @@ +// 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" +export { Agent } from "@opencode-ai/schema/agent" +export { Command } from "@opencode-ai/schema/command" +export { Credential } from "@opencode-ai/schema/credential" +export { FileSystem } from "@opencode-ai/schema/filesystem" +export { Integration } from "@opencode-ai/schema/integration" +export { Location } from "@opencode-ai/schema/location" +export { Model } from "@opencode-ai/schema/model" +export { Permission } from "@opencode-ai/schema/permission" +export { PermissionSaved } from "@opencode-ai/schema/permission-saved" +export { Project } from "@opencode-ai/schema/project" +export { ProjectCopy } from "@opencode-ai/schema/project-copy" +export { Provider } from "@opencode-ai/schema/provider" +export { Pty } from "@opencode-ai/schema/pty" +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 { SessionMessage } from "@opencode-ai/schema/session-message" +export { Skill } from "@opencode-ai/schema/skill" +export { Prompt } from "@opencode-ai/schema/prompt" +export type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event" diff --git a/packages/client/src/generated-effect/.httpapi-codegen.json b/packages/client/src/generated-effect/.httpapi-codegen.json new file mode 100644 index 00000000000..958eb566dbd --- /dev/null +++ b/packages/client/src/generated-effect/.httpapi-codegen.json @@ -0,0 +1,5 @@ +[ + "client-error.ts", + "client.ts", + "index.ts" +] diff --git a/packages/client/src/generated-effect/client-error.ts b/packages/client/src/generated-effect/client-error.ts new file mode 100644 index 00000000000..bcc65d9bdd2 --- /dev/null +++ b/packages/client/src/generated-effect/client-error.ts @@ -0,0 +1,5 @@ +import { Schema } from "effect" + +export class ClientError extends Schema.TaggedErrorClass()("ClientError", { + cause: Schema.Defect(), +}) {} diff --git a/packages/client/src/generated-effect/client.ts b/packages/client/src/generated-effect/client.ts new file mode 100644 index 00000000000..5eac8611d0e --- /dev/null +++ b/packages/client/src/generated-effect/client.ts @@ -0,0 +1,708 @@ +// 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"] + readonly size?: Endpoint14_1Request["payload"]["size"] +} +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"], + size: input?.["size"], + }, + }).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-effect/index.ts b/packages/client/src/generated-effect/index.ts new file mode 100644 index 00000000000..bc0dbc9fa4d --- /dev/null +++ b/packages/client/src/generated-effect/index.ts @@ -0,0 +1,2 @@ +export { ClientError } from "./client-error" +export * as OpenCode from "./client" diff --git a/packages/client/src/generated/.httpapi-codegen.json b/packages/client/src/generated/.httpapi-codegen.json new file mode 100644 index 00000000000..25700fc72da --- /dev/null +++ b/packages/client/src/generated/.httpapi-codegen.json @@ -0,0 +1,6 @@ +[ + "client-error.ts", + "client.ts", + "index.ts", + "types.ts" +] diff --git a/packages/client/src/generated/client-error.ts b/packages/client/src/generated/client-error.ts new file mode 100644 index 00000000000..c278f0ddc80 --- /dev/null +++ b/packages/client/src/generated/client-error.ts @@ -0,0 +1,11 @@ +export type ClientErrorReason = "Transport" | "UnexpectedStatus" | "UnsupportedContentType" | "MalformedResponse" + +export class ClientError extends Error { + override readonly name = "ClientError" + constructor( + readonly reason: ClientErrorReason, + options?: ErrorOptions, + ) { + super(reason, options) + } +} diff --git a/packages/client/src/generated/client.ts b/packages/client/src/generated/client.ts new file mode 100644 index 00000000000..b17a7ebd9fe --- /dev/null +++ b/packages/client/src/generated/client.ts @@ -0,0 +1,1030 @@ +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: [400, 401], empty: false }, + requestOptions, + ), + }, + location: { + get: (input?: LocationGetInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/location`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [400, 401], + empty: false, + }, + requestOptions, + ), + }, + agents: { + list: (input?: AgentsListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/agent`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [400, 401], + 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: [400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + active: (requestOptions?: RequestOptions) => + request<{ readonly data: SessionsActiveOutput }>( + { + method: "GET", + path: `/api/session/active`, + successStatus: 200, + declaredStatuses: [400, 401], + 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: [400, 401, 404], + 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: [400, 401, 404], + 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: [400, 401, 404], + 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: [400, 401, 404, 409], + 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: [400, 401, 404, 503], + empty: true, + }, + requestOptions, + ), + wait: (input: SessionsWaitInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/wait`, + successStatus: 204, + declaredStatuses: [400, 401, 404, 503], + 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: [400, 401, 404, 500], + 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: [400, 401, 404, 500], + empty: true, + }, + requestOptions, + ), + commit: (input: SessionsCommitInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`, + successStatus: 204, + declaredStatuses: [400, 401, 404], + empty: true, + }, + requestOptions, + ), + context: (input: SessionsContextInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionsContextOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/context`, + successStatus: 200, + declaredStatuses: [400, 401, 404, 500], + 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: [400, 401, 404], + 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: [400, 401, 404], + empty: false, + }, + requestOptions, + ), + interrupt: (input: SessionsInterruptInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/interrupt`, + successStatus: 204, + declaredStatuses: [400, 401, 404], + 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: [400, 401, 404], + 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, 401, 404, 500], + empty: false, + }, + requestOptions, + ), + }, + models: { + list: (input?: ModelsListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/model`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [400, 401, 503], + empty: false, + }, + requestOptions, + ), + }, + providers: { + list: (input?: ProvidersListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/provider`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [400, 401, 503], + empty: false, + }, + requestOptions, + ), + get: (input: ProvidersGetInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/provider/${encodeURIComponent(input.providerID)}`, + query: { location: input["location"] }, + successStatus: 200, + declaredStatuses: [400, 401, 404, 503], + empty: false, + }, + requestOptions, + ), + }, + integrations: { + list: (input?: IntegrationsListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/integration`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [400, 401], + empty: false, + }, + requestOptions, + ), + get: (input: IntegrationsGetInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/integration/${encodeURIComponent(input.integrationID)}`, + query: { location: input["location"] }, + successStatus: 200, + declaredStatuses: [400, 401], + 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: [400, 401], + 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: [400, 401], + 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: [400, 401], + empty: true, + }, + requestOptions, + ), + remove: (input: CredentialsRemoveInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/credential/${encodeURIComponent(input.credentialID)}`, + query: { location: input["location"] }, + successStatus: 204, + declaredStatuses: [400, 401], + empty: true, + }, + requestOptions, + ), + }, + permissions: { + listRequests: (input?: PermissionsListRequestsInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/permission/request`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [400, 401], + 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: [400, 401], + 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: [400, 401], + 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: [400, 401, 404], + 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: [400, 401, 404], + 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: [400, 401, 404], + 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: [400, 401, 404], + 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: [400, 401], + 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: [400, 401], + empty: false, + }, + requestOptions, + ), + }, + commands: { + list: (input?: CommandsListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/command`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [400, 401], + empty: false, + }, + requestOptions, + ), + }, + skills: { + list: (input?: SkillsListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/skill`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [400, 401], + empty: false, + }, + requestOptions, + ), + }, + events: { + subscribe: (requestOptions?: RequestOptions): AsyncIterable => + sse( + { method: "GET", path: `/api/event`, successStatus: 200, declaredStatuses: [400, 401], empty: false }, + requestOptions, + ), + }, + ptys: { + list: (input?: PtysListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/pty`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [400, 401], + 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"], + size: input?.["size"], + }, + successStatus: 200, + declaredStatuses: [400, 401], + empty: false, + }, + requestOptions, + ), + get: (input: PtysGetInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/pty/${encodeURIComponent(input.ptyID)}`, + query: { location: input["location"] }, + successStatus: 200, + declaredStatuses: [400, 401, 404], + 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: [400, 401, 404], + empty: false, + }, + requestOptions, + ), + remove: (input: PtysRemoveInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/pty/${encodeURIComponent(input.ptyID)}`, + query: { location: input["location"] }, + successStatus: 204, + declaredStatuses: [400, 401, 404], + empty: true, + }, + requestOptions, + ), + }, + questions: { + listRequests: (input?: QuestionsListRequestsInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/question/request`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [400, 401], + empty: false, + }, + requestOptions, + ), + list: (input: QuestionsListInput, requestOptions?: RequestOptions) => + request<{ readonly data: QuestionsListOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/question`, + successStatus: 200, + declaredStatuses: [400, 401, 404], + 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: [400, 401, 404], + 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: [400, 401, 404], + empty: true, + }, + requestOptions, + ), + }, + references: { + list: (input?: ReferencesListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/reference`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [400, 401], + 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/index.ts b/packages/client/src/generated/index.ts new file mode 100644 index 00000000000..2570372cf81 --- /dev/null +++ b/packages/client/src/generated/index.ts @@ -0,0 +1,3 @@ +export { ClientError, type ClientErrorReason } from "./client-error" +export * as OpenCode from "./client" +export * from "./types" diff --git a/packages/client/src/generated/types.ts b/packages/client/src/generated/types.ts new file mode 100644 index 00000000000..5e59d854bfb --- /dev/null +++ b/packages/client/src/generated/types.ts @@ -0,0 +1,2887 @@ +import type { OpenCodeEventEncoded } from "@opencode-ai/protocol/groups/event" + +export type JsonValue = + | null + | boolean + | number + | string + | ReadonlyArray + | { readonly [key: string]: JsonValue } + +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 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 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 workspace?: "restored" | "snapshots-disabled" | "unavailable" + } + }> + 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 + }> + readonly workspace?: "restored" | "snapshots-disabled" | "unavailable" + } + } +}["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 + }> + readonly workspace?: "restored" | "snapshots-disabled" | "unavailable" + } + } +}["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 + }> + readonly workspace?: "restored" | "snapshots-disabled" | "unavailable" + } +}["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 include?: string | null + } + } + | { + 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 workspace?: "restored" | "snapshots-disabled" | "unavailable" + } + } + } + | { + 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 id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.prompt.promoted" + 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 timeCreated: number + } + } + > + 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 include?: string | undefined + } + } + | { + 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 workspace?: "restored" | "snapshots-disabled" | "unavailable" + } + } + } + | { + 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 } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.prompt.promoted" + 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 timeCreated: number + } + } + +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 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 + readonly sessionID?: string | null + }> +} + +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 } + readonly size?: { readonly rows: number; readonly cols: number } + }["command"] + readonly args?: { + readonly command?: string + readonly args?: ReadonlyArray + readonly cwd?: string + readonly title?: string + readonly env?: { readonly [x: string]: string } + readonly size?: { readonly rows: number; readonly cols: number } + }["args"] + readonly cwd?: { + readonly command?: string + readonly args?: ReadonlyArray + readonly cwd?: string + readonly title?: string + readonly env?: { readonly [x: string]: string } + readonly size?: { readonly rows: number; readonly cols: number } + }["cwd"] + readonly title?: { + readonly command?: string + readonly args?: ReadonlyArray + readonly cwd?: string + readonly title?: string + readonly env?: { readonly [x: string]: string } + readonly size?: { readonly rows: number; readonly cols: number } + }["title"] + readonly env?: { + readonly command?: string + readonly args?: ReadonlyArray + readonly cwd?: string + readonly title?: string + readonly env?: { readonly [x: string]: string } + readonly size?: { readonly rows: number; readonly cols: number } + }["env"] + readonly size?: { + readonly command?: string + readonly args?: ReadonlyArray + readonly cwd?: string + readonly title?: string + readonly env?: { readonly [x: string]: string } + readonly size?: { readonly rows: number; readonly cols: number } + }["size"] +} + +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 + readonly sessionID?: string | null + } +} + +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 + readonly sessionID?: string | null + } +} + +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 + readonly sessionID?: string | null + } +} + +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 new file mode 100644 index 00000000000..2d4c87f7164 --- /dev/null +++ b/packages/client/src/index.ts @@ -0,0 +1,12 @@ +export * from "./generated/index" +export type { EventsSubscribeOutput as OpenCodeEvent } from "./generated/types" + +// kilocode_change start - compatibility with upstream session-ui's legacy Promise client type +export type FileDiffInfo = { + file: string + patch: string + additions: number + deletions: number + status: "added" | "deleted" | "modified" +} +// kilocode_change end diff --git a/packages/client/sst-env.d.ts b/packages/client/sst-env.d.ts new file mode 100644 index 00000000000..64441936d7a --- /dev/null +++ b/packages/client/sst-env.d.ts @@ -0,0 +1,10 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +/* deno-fmt-ignore-file */ +/* biome-ignore-all lint: auto-generated */ + +/// + +import "sst" +export {} \ No newline at end of file diff --git a/packages/client/test/contract-identity.test.ts b/packages/client/test/contract-identity.test.ts new file mode 100644 index 00000000000..64a2e958ce2 --- /dev/null +++ b/packages/client/test/contract-identity.test.ts @@ -0,0 +1,58 @@ +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_") +}) + +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("shared DTO schemas construct and decode plain objects", () => { + const made = Prompt.make({ text: "hello" }) + const decoded = Schema.decodeUnknownSync(Prompt)({ text: "hello" }) + const content = Schema.decodeUnknownSync(SessionMessage.AssistantText)({ type: "text", id: "part_1", text: "hi" }) + + expect(Object.getPrototypeOf(made)).toBe(Object.prototype) + expect(Object.getPrototypeOf(decoded)).toBe(Object.prototype) + 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 new file mode 100644 index 00000000000..7bf4d26f8fe --- /dev/null +++ b/packages/client/test/effect.test.ts @@ -0,0 +1,246 @@ +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" + +test("sessions.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") }) + }).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 () => { + 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(modelSwitchedEvent)}\n\n`, + { headers: { "content-type": "text/event-stream" } }, + ), + ), + ), + ) + const events = await Effect.gen(function* () { + const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" }) + return yield* client.events.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"]) + 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) + expect(durable.durable).toEqual({ aggregateID: "ses_test", seq: 1, version: 1 }) +}) + +test("events.subscribe terminates on Effect protocol decode failures", async () => { + const httpClient = HttpClient.make((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response(`data: {"type":"server.connected"}\n\n`, { + headers: { "content-type": "text/event-stream" }, + }), + ), + ), + ) + 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) + }).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 httpClient = HttpClient.make((request) => { + const url = request.url + if (url.includes("/event")) { + return Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\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.includes("/context")) { + return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ data: [] }))) + } + if (url.includes("/message/")) { + return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ data: modelSwitchedMessage }))) + } + if (url.endsWith("/api/session/active")) { + return Effect.succeed( + HttpClientResponse.fromWeb(request, Response.json({ data: { ses_test: { type: "running" } } })), + ) + } + if (request.method === "POST" && url.endsWith("/api/session")) { + return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(session))) + } + if (request.method === "POST") { + return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null, { status: 204 }))) + } + return Effect.succeed( + HttpClientResponse.fromWeb(request, Response.json({ data: [session.data], cursor: { next: "next" } })), + ) + }) + 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({ + 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({ + sessionID: Session.ID.make("ses_test"), + model: Model.Ref.make({ id: "claude", providerID: "anthropic" }), + }) + const admitted = yield* client.sessions.prompt({ + sessionID: Session.ID.make("ses_test"), + prompt: Prompt.make({ 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 }) + .pipe(Stream.runCollect) + yield* client.sessions.interrupt({ sessionID: Session.ID.make("ses_test") }) + const message = yield* client.sessions.message({ + sessionID: Session.ID.make("ses_test"), + messageID: SessionMessage.ID.make("msg_model"), + }) + return { page, active, created, admitted, context, history, historyNext, events, message } + }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise) + + expect(DateTime.toEpochMillis(result.page.data[0].time.created)).toBe(1_717_171_717_000) + expect(result.active).toEqual({ ses_test: { type: "running" } }) + expect(Object.getPrototypeOf(result.page.data[0])).toBe(Object.prototype) + 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(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(result.message).toEqual(expect.objectContaining({ id: "msg_model", type: "model-switched" })) +}) + +test("sessions.history retains the typed SessionNotFoundError", async () => { + const httpClient = HttpClient.make((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + Response.json( + { _tag: "SessionNotFoundError", sessionID: "ses_missing", message: "Session not found" }, + { status: 404 }, + ), + ), + ), + ) + 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) + }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise) + + expect(error._tag).toBe("SessionNotFoundError") +}) + +const session = { + data: { + id: "ses_test", + projectID: "project", + cost: 0, + tokens: { + input: 1, + output: 2, + reasoning: 3, + cache: { read: 4, write: 5 }, + }, + time: { + created: 1_717_171_717_000, + updated: 1_717_171_717_000, + }, + title: "Test", + location: { directory: "/tmp/project" }, + }, +} + +const admission = { + data: { + admittedSeq: 0, + id: "msg_test", + sessionID: "ses_test", + prompt: { text: "Hello" }, + delivery: "steer", + timeCreated: 1_717_171_717_000, + }, +} + +const modelSwitchedMessage = { + id: "msg_model", + type: "model-switched", + time: { created: 1_717_171_717_000 }, + model: { id: "claude", providerID: "anthropic" }, +} + +const modelSwitchedEvent = { + id: "evt_model", + type: "session.next.model.switched", + 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/import-boundaries.test.ts b/packages/client/test/import-boundaries.test.ts new file mode 100644 index 00000000000..b25e8cc18d0 --- /dev/null +++ b/packages/client/test/import-boundaries.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, test } from "bun:test" +import { mkdtemp, rm } from "node:fs/promises" +import { join, resolve, sep } from "node:path" + +const directory = resolve(import.meta.dir, "..") +const schema = resolve(import.meta.dir, "../../schema") +const protocol = resolve(import.meta.dir, "../../protocol") +const core = resolve(import.meta.dir, "../../core") +const server = resolve(import.meta.dir, "../../server") + +describe("public import boundaries", () => { + test("isolates each public entrypoint", async () => { + const root = await bundleInputs("@opencode-ai/client", "browser") + + expect(dependency(root, "effect")).toEqual([]) // kilocode_change + expect(within(root, schema)).toEqual([]) + expect(within(root, protocol)).toEqual([]) + expect(within(root, core)).toEqual([]) + expect(within(root, server)).toEqual([]) + + const network = await bundleInputs("@opencode-ai/client/effect", "browser") + + expect(dependency(network, "effect").length).toBeGreaterThan(0) // kilocode_change + expect(within(network, schema).length).toBeGreaterThan(0) + expect(within(network, protocol).length).toBeGreaterThan(0) + expect(within(network, core)).toEqual([]) + expect(within(network, server)).toEqual([]) + }) +}) + +async function bundleInputs(specifier: string, target: "browser" | "bun") { + const temporary = await mkdtemp(join(import.meta.dir, ".import-boundary-")) + const entrypoint = join(temporary, "index.ts") + const metafile = join(temporary, "meta.json") + try { + await Bun.write(entrypoint, `export * from ${JSON.stringify(specifier)}`) + const child = Bun.spawn( + [ + process.execPath, + "build", + entrypoint, + `--target=${target}`, + "--format=esm", + "--packages=bundle", + `--metafile=${metafile}`, + `--outdir=${join(temporary, "out")}`, + ], + { cwd: directory, stdout: "pipe", stderr: "pipe" }, + ) + const [exitCode, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]) + if (exitCode !== 0) throw new Error(stdout + stderr) + const metadata = await Bun.file(metafile).json() + return Object.keys(metadata.inputs).map((input) => resolve(directory, input)) + } finally { + await rm(temporary, { recursive: true, force: true }) + } +} + +function within(inputs: ReadonlyArray, directory: string) { + const prefix = directory.endsWith(sep) ? directory : directory + sep + return inputs.filter((input) => input === directory || input.startsWith(prefix)) +} + +// kilocode_change start - support hoisted dependencies in Windows workspace installs +function dependency(inputs: ReadonlyArray, name: string) { + return inputs.filter((input) => input.replaceAll("\\", "/").includes(`/node_modules/${name}/`)) +} +// kilocode_change end diff --git a/packages/client/test/promise.test.ts b/packages/client/test/promise.test.ts new file mode 100644 index 00000000000..322a39cd6b2 --- /dev/null +++ b/packages/client/test/promise.test.ts @@ -0,0 +1,255 @@ +import { expect, test } from "bun:test" +import { isSessionNotFoundError, isUnauthorizedError, OpenCode } from "../src" + +test("exposes every standard HTTP API group", () => { + const client = OpenCode.make({ baseUrl: "http://localhost:3000" }) + + expect(Object.keys(client)).toEqual([ + "health", + "location", + "agents", + "sessions", + "messages", + "models", + "providers", + "integrations", + "credentials", + "permissions", + "files", + "commands", + "skills", + "events", + "ptys", + "questions", + "references", + "projectCopies", + ]) + 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"]) +}) + +test("sessions.get returns the wire projection", async () => { + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async (input) => { + expect(typeof input === "string" ? input : input instanceof URL ? input.href : input.url).toBe( + "http://localhost:3000/api/session/ses_test", + ) + return Response.json(session) + }, + }) + + const result = await client.sessions.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 () => { + 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` + + `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) + + 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) +}) + +test("events.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({ + name: "ClientError", + reason: "MalformedResponse", + }) +}) + +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) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url + requests.push({ url, init }) + if (url.includes("/event")) { + return new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`, { + 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("/prompt")) return Response.json(admission) + 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" } } }) + if (init?.method === "POST" && url.endsWith("/api/session")) return Response.json(session) + if (init?.method === "POST") return new Response(null, { status: 204 }) + return Response.json({ data: [session.data], cursor: { next: "next" } }) + }, + }) + + 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({ + sessionID: "ses_test", + model: { id: "claude", providerID: "anthropic" }, + }) + const admitted = await client.sessions.prompt({ + sessionID: "ses_test", + prompt: { 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" }) + + 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(context).toEqual([]) + expect(history).toEqual({ data: [modelSwitchedEvent], hasMore: true }) + expect(historyNext).toEqual({ data: [], hasMore: false }) + expect(events).toEqual([modelSwitchedEvent]) + 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/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/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"], + ["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" }, + resume: false, + }) +}) + +test("middleware errors remain declared client errors", async () => { + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async () => + Response.json({ _tag: "UnauthorizedError", message: "Authentication required" }, { status: 401 }), + }) + + try { + await client.sessions.create({}) + throw new Error("Expected request to fail") + } catch (error) { + expect(isUnauthorizedError(error)).toBe(true) + } +}) + +test("sessions.history decodes SessionNotFoundError", async () => { + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async () => + Response.json( + { _tag: "SessionNotFoundError", sessionID: "ses_missing", message: "Session not found" }, + { status: 404 }, + ), + }) + + try { + await client.sessions.history({ sessionID: "ses_missing" }) + throw new Error("Expected request to fail") + } catch (error) { + expect(isSessionNotFoundError(error)).toBe(true) + } +}) + +const session = { + data: { + id: "ses_test", + projectID: "project", + cost: 0, + tokens: { + input: 1, + output: 2, + reasoning: 3, + cache: { read: 4, write: 5 }, + }, + time: { + created: 1_717_171_717_000, + updated: 1_717_171_717_000, + }, + title: "Test", + location: { directory: "/tmp/project" }, + }, +} + +const admission = { + data: { + admittedSeq: 0, + id: "msg_test", + sessionID: "ses_test", + prompt: { text: "Hello" }, + delivery: "steer", + timeCreated: 1_717_171_717_000, + }, +} + +const modelSwitchedMessage = { + id: "msg_model", + type: "model-switched", + time: { created: 1_717_171_717_000 }, + model: { id: "claude", providerID: "anthropic" }, +} + +const modelSwitchedEvent = { + id: "evt_model", + type: "session.next.model.switched", + 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/tsconfig.json b/packages/client/tsconfig.json new file mode 100644 index 00000000000..47fc90bc55b --- /dev/null +++ b/packages/client/tsconfig.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/bun/tsconfig.json", + "compilerOptions": { + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "noUncheckedIndexedAccess": false + }, + "include": ["src"] +} diff --git a/packages/codemode/AGENTS.md b/packages/codemode/AGENTS.md new file mode 100644 index 00000000000..df812c6d86f --- /dev/null +++ b/packages/codemode/AGENTS.md @@ -0,0 +1,23 @@ +# @opencode-ai/codemode + +- This local package owns confined execution over explicit schema-described tools. Applications own authorization, persistence, external authority, and tool-specific delivery semantics. +- 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. + +## OpenAPI + +- Generate an operation only when its transport semantics are supported; otherwise return a precise `skipped` reason. +- Never guess parameter serialization or malformed security semantics. Unsupported serialization is skipped and malformed security fails closed. +- Render unresolved schema constructs as `unknown`, never as invented TypeScript names. +- Keep network reads bounded and map expected encoding, transport, and decoding failures to model-safe `ToolError` values. +- Test supported behavior directly; do not reproduce adapter algorithms in tests. + +## 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. +- 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. +- If `fetch` is added, model it as a host-provided outbound capability with policy controls: allowed origins, methods, headers, response size, timeout, and whether response bodies may be returned, emitted, or only summarized through a tool. diff --git a/packages/codemode/README.md b/packages/codemode/README.md new file mode 100644 index 00000000000..37bde2d8696 --- /dev/null +++ b/packages/codemode/README.md @@ -0,0 +1,369 @@ +# @opencode-ai/codemode + +Effect-native confined code execution over explicit, schema-described tools. + +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. + +The package is currently private to this workspace. Its API is designed around one-shot and reusable execution: + +```ts +// One execution +yield * CodeMode.execute({ tools, code }) + +// A reusable runtime +const runtime = CodeMode.make({ tools, limits }) +yield * runtime.execute(code) +``` + +## 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. + +## Quick Start + +Define tools with Effect Schema, then place them in the object tree exposed to programs as `tools`: + +```ts +import { CodeMode, Tool } from "@opencode-ai/codemode" +import { Effect, Schema } from "effect" + +const lookupOrder = Tool.make({ + description: "Look up an order by ID", + input: Schema.Struct({ id: Schema.String }), + output: Schema.Struct({ id: Schema.String, status: Schema.String }), + run: ({ id }) => Effect.succeed({ id, status: "open" }), +}) + +const runtime = CodeMode.make({ + tools: { + orders: { + lookup: lookupOrder, + }, + }, +}) + +const result = + yield * + runtime.execute(` + const order = await tools.orders.lookup({ id: "order_42" }) + return { id: order.id, needsAttention: order.status !== "complete" } +`) +``` + +`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. + +## 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 (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). + +`output` is optional. Without it the tool's signature advertises `Promise` and the host result is exposed as-is. + +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: + +```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 }, +}) + +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. + +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. + +### Results + +```ts +type Result = Success | Failure + +interface Success { + readonly ok: true + readonly value: CodeMode.DataValue + readonly logs?: ReadonlyArray + readonly truncated?: boolean + readonly toolCalls: ReadonlyArray +} + +interface Failure { + readonly ok: false + readonly error: CodeMode.Diagnostic + readonly logs?: ReadonlyArray + readonly truncated?: boolean + readonly toolCalls: ReadonlyArray +} +``` + +`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). + +### Tool-call hooks + +`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. + +`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. + +## 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. + +## 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 | Model-facing output: the serialized result value plus captured logs. | + +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. + +Pass only the overrides you need: + +```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. + +## Testing + +From the package directory: + +```sh +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 new file mode 100644 index 00000000000..f56c405746f --- /dev/null +++ b/packages/codemode/codemode.md @@ -0,0 +1,176 @@ +# 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/package.json b/packages/codemode/package.json new file mode 100644 index 00000000000..fab4911e331 --- /dev/null +++ b/packages/codemode/package.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@opencode-ai/codemode", + "version": "7.4.23", + "description": "Effect-native confined code execution over schema-described tools", + "private": true, + "type": "module", + "license": "MIT", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "typecheck": "tsgo --noEmit", + "test": "bun test", + "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml" + }, + "dependencies": { + "acorn": "8.15.0", + "effect": "catalog:", + "typescript": "catalog:" + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:" + }, + "peerDependencies": {} +} diff --git a/packages/codemode/src/codemode.ts b/packages/codemode/src/codemode.ts new file mode 100644 index 00000000000..14782d7c8aa --- /dev/null +++ b/packages/codemode/src/codemode.ts @@ -0,0 +1,159 @@ +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" + +/** 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. */ + 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. */ + readonly maxOutputBytes?: number +} + +/** Controls how much of the tool catalog is inlined in agent instructions. */ +export type DiscoveryOptions = { + /** Approximate token budget (chars/4, default 2000) for full catalog entries. */ + readonly catalogBudget?: number +} + +type ToolTree = { + readonly [name: string]: Definition | ToolTree +} + +export type ResolvedExecutionLimits = { + readonly timeoutMs: number | undefined + readonly maxToolCalls: number | undefined + readonly maxOutputBytes: number | undefined +} + +/** Options for one CodeMode execution. */ +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> + /** Per-execution overrides for the default resource limits. */ + limits?: ExecutionLimits + /** Observes decoded tool input immediately before tool execution. */ + onToolCallStart?: (call: ToolRuntime.ToolCallStarted) => Effect.Effect> + /** Observes each admitted tool call as it settles, with outcome and duration. */ + 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"> & { + /** Progressive-disclosure configuration for the agent-facing tool catalog. */ + readonly discovery?: DiscoveryOptions +} + +/** Schema for a host tool input containing CodeMode source. */ +export const Input = Schema.Struct({ code: Schema.String }) +export type Input = typeof Input.Type + +export const DiagnosticKind = Schema.Literals([ + "ParseError", + "UnsupportedSyntax", + "UnknownTool", + "InvalidToolInput", + "InvalidToolOutput", + "InvalidDataValue", + "ToolCallLimitExceeded", + "TimeoutExceeded", + "ToolFailure", + "ExecutionFailure", +]) +/** Stable categories produced by program, schema, tool, and limit failures. */ +export type DiagnosticKind = typeof DiagnosticKind.Type + +export const Diagnostic = Schema.Struct({ + kind: DiagnosticKind, + message: Schema.String, + location: Schema.optionalKey(Schema.Struct({ line: Schema.Number, column: Schema.Number })), + suggestions: Schema.optionalKey(Schema.Array(Schema.String)), +}) +/** A normalized program diagnostic safe to return across an agent tool boundary. */ +export type Diagnostic = typeof Diagnostic.Type + +const ToolCallSchema = Schema.Struct({ name: Schema.String }) +export const Success = Schema.Struct({ + ok: Schema.Literal(true), + value: Schema.Json, + logs: Schema.optionalKey(Schema.Array(Schema.String)), + truncated: Schema.optionalKey(Schema.Boolean), + toolCalls: Schema.Array(ToolCallSchema), +}) +/** Successful execution after the result has crossed the plain-data boundary. */ +export type Success = typeof Success.Type + +export const Failure = Schema.Struct({ + ok: Schema.Literal(false), + error: Diagnostic, + logs: Schema.optionalKey(Schema.Array(Schema.String)), + truncated: Schema.optionalKey(Schema.Boolean), + toolCalls: Schema.Array(ToolCallSchema), +}) +/** Failed execution with calls admitted before the diagnostic was produced. */ +export type Failure = typeof Failure.Type + +/** Schema for the structured success or diagnostic returned by CodeMode execution. */ +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. */ +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 => { + if (value !== undefined && (!Number.isSafeInteger(value) || value < minimum)) { + throw new RangeError(`${name} must be a safe integer greater than or equal to ${minimum}.`) + } + return value +} + +const resolveExecutionLimits = (limits?: ExecutionLimits): ResolvedExecutionLimits => ({ + timeoutMs: validateLimit("timeoutMs", limits?.timeoutMs, 1), + maxToolCalls: validateLimit("maxToolCalls", limits?.maxToolCalls, 0), + maxOutputBytes: validateLimit("maxOutputBytes", limits?.maxOutputBytes, 0), +}) + +/** 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) + 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) + 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), + } +} diff --git a/packages/codemode/src/index.ts b/packages/codemode/src/index.ts new file mode 100644 index 00000000000..92019825004 --- /dev/null +++ b/packages/codemode/src/index.ts @@ -0,0 +1,4 @@ +export * as CodeMode from "./codemode.js" +export * as Tool from "./tool.js" +export * as OpenAPI from "./openapi/index.js" +export { ToolError, toolError } from "./tool-error.js" diff --git a/packages/codemode/src/interpreter/model.ts b/packages/codemode/src/interpreter/model.ts new file mode 100644 index 00000000000..d9535145103 --- /dev/null +++ b/packages/codemode/src/interpreter/model.ts @@ -0,0 +1,201 @@ +import type { SafeObject } from "../tool-runtime.js" +import type { SandboxURL } from "../values.js" + +export type SourcePosition = { + line: number + column: number +} + +export type SourceLocation = { + start: SourcePosition + end: SourcePosition +} + +export type AstNode = { + type: string + loc?: SourceLocation + [key: string]: unknown +} + +export type ProgramNode = AstNode & { + type: "Program" + body: Array +} + +export type Binding = { + mutable: boolean + value: unknown + initialized?: boolean +} + +export type StatementResult = + | { kind: "none" } + | { kind: "value"; value: unknown } + | { kind: "return"; value: unknown } + | { kind: "break" } + | { kind: "continue" } + +export type MemberReference = { + target: SafeObject | Array | SandboxURL + key: string | number +} + +export class CodeModeFunction { + constructor( + readonly parameters: ReadonlyArray, + readonly body: AstNode, + readonly capturedScopes: ReadonlyArray>, + ) {} +} + +export class IntrinsicReference { + constructor( + readonly receiver: unknown, + readonly name: string, + ) {} +} + +export class ComputedValue { + constructor(readonly value: unknown) {} +} + +export class PromiseNamespace {} + +export type PromiseMethodName = "all" | "allSettled" | "race" | "resolve" | "reject" + +export class PromiseMethodReference { + constructor(readonly name: PromiseMethodName) {} +} + +export type GlobalNamespaceName = + | "Object" + | "Math" + | "JSON" + | "Array" + | "console" + | "Date" + | "RegExp" + | "Map" + | "Set" + | "URL" + | "URLSearchParams" + +export class GlobalNamespace { + constructor(readonly name: GlobalNamespaceName) {} +} + +export class GlobalMethodReference { + constructor( + readonly namespace: GlobalNamespaceName | "Number" | "String", + readonly name: string, + ) {} +} + +export class CoercionFunction { + constructor(readonly name: "Number" | "String" | "Boolean" | "parseInt" | "parseFloat") {} +} + +export class UriFunction { + constructor(readonly name: "encodeURI" | "encodeURIComponent" | "decodeURI" | "decodeURIComponent") {} +} + +export class ProgramThrow { + constructor(readonly value: unknown) {} +} + +export class ErrorConstructorReference { + constructor(readonly name: string) {} +} + +export type DiagnosticKind = + | "ParseError" + | "UnsupportedSyntax" + | "UnknownTool" + | "InvalidToolInput" + | "InvalidToolOutput" + | "InvalidDataValue" + | "ToolCallLimitExceeded" + | "TimeoutExceeded" + | "ToolFailure" + | "ExecutionFailure" + +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)." + +export class InterpreterRuntimeError extends Error { + readonly node?: AstNode + errorName: string = "Error" + + constructor( + message: string, + node?: AstNode, + readonly kind: DiagnosticKind = "ExecutionFailure", + readonly suggestions?: ReadonlyArray, + ) { + super(message) + this.name = "InterpreterRuntimeError" + if (node) this.node = node + } + + as(errorName: string): this { + this.errorName = errorName + return this + } +} + +export const unsupportedSyntax = (kind: string, node: AstNode): InterpreterRuntimeError => + new InterpreterRuntimeError( + `Syntax '${kind}' is not supported in CodeMode. ${supportedSyntaxMessage}`, + node, + "UnsupportedSyntax", + [supportedSyntaxMessage], + ) + +export const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null + +export const asNode = (value: unknown, context: string): AstNode => { + if (!isRecord(value) || typeof value.type !== "string") { + throw new InterpreterRuntimeError(`Invalid AST node while reading ${context}.`) + } + return value as AstNode +} + +export const getArray = (node: AstNode, key: string): Array => { + const value = node[key] + if (!Array.isArray(value)) throw new InterpreterRuntimeError(`Expected '${key}' to be an array.`, node) + return value +} + +export const getString = (node: AstNode, key: string): string => { + const value = node[key] + if (typeof value !== "string") throw new InterpreterRuntimeError(`Expected '${key}' to be a string.`, node) + return value +} + +export const getBoolean = (node: AstNode, key: string): boolean => { + const value = node[key] + if (typeof value !== "boolean") throw new InterpreterRuntimeError(`Expected '${key}' to be a boolean.`, node) + return value +} + +export const getOptionalNode = (node: AstNode, key: string): AstNode | undefined => { + const value = node[key] + if (value === undefined || value === null) return undefined + return asNode(value, key) +} + +export const getNode = (node: AstNode, key: string): AstNode => asNode(node[key], key) + +export const sourceLocation = (node: AstNode): { readonly line: number; readonly column: number } => ({ + line: Math.max(1, (node.loc?.start.line ?? 2) - 1), + column: Math.max(1, (node.loc?.start.column ?? 4) - 3), +}) + +export const formatLocation = (node?: AstNode): string => { + if (!node?.loc) return "" + const location = sourceLocation(node) + return ` (line ${location.line}, col ${location.column})` +} diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts new file mode 100644 index 00000000000..093f5777651 --- /dev/null +++ b/packages/codemode/src/interpreter/runtime.ts @@ -0,0 +1,3465 @@ +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 { + type AstNode, + asNode, + type Binding, + CodeModeFunction, + CoercionFunction, + ComputedValue, + ErrorConstructorReference, + GlobalMethodReference, + GlobalNamespace, + type GlobalNamespaceName, + formatLocation, + getArray, + getBoolean, + getNode, + getOptionalNode, + getString, + IntrinsicReference, + InterpreterRuntimeError, + isRecord, + type MemberReference, + OptionalShortCircuit, + PromiseMethodReference, + type PromiseMethodName, + PromiseNamespace, + ProgramThrow, + type ProgramNode, + type StatementResult, + sourceLocation, + supportedSyntaxMessage, + unsupportedSyntax, + UriFunction, +} from "./model.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 { + urlMethods, + urlProperties, + urlSearchParamsMethods, + urlStatics, + urlWritableProperties, + invokeUriFunction, + invokeURLMethod, + invokeURLStatic, + uriArgument, + urlArgument, +} from "../stdlib/url.js" +import { + boundedData, + coerceToNumber, + coerceToString, + compoundOperators, + createErrorValue, + errorBrandName, + errorConstructors, + invokeCoercion, + valueConstructors, +} from "../stdlib/value.js" +import { + isSandboxValue, + SandboxDate, + SandboxMap, + SandboxPromise, + SandboxRegExp, + SandboxSet, + SandboxURL, + SandboxURLSearchParams, +} 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 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)), + } +} + +// 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) + return brand !== undefined && (rhs.name === "Error" || brand === rhs.name) + } + if (rhs instanceof GlobalNamespace) { + switch (rhs.name) { + case "Date": + return lhs instanceof SandboxDate + case "RegExp": + return lhs instanceof SandboxRegExp + case "Map": + return lhs instanceof SandboxMap + case "Set": + return lhs instanceof SandboxSet + case "URL": + return lhs instanceof SandboxURL + case "URLSearchParams": + return lhs instanceof SandboxURLSearchParams + 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 CoercionFunction && (rhs.name === "Number" || rhs.name === "String" || rhs.name === "Boolean")) { + return false + } + throw new InterpreterRuntimeError( + "The right-hand side of 'instanceof' must be a constructor CodeMode knows: Error (or a specific error type like TypeError), Date, RegExp, Map, Set, URL, URLSearchParams, Array, Object, or Promise.", + node, + ) +} + +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": + out.push(getString(pattern, "name")) + break + case "AssignmentPattern": + collectPatternNames(getNode(pattern, "left"), out) + break + case "RestElement": + collectPatternNames(getNode(pattern, "argument"), out) + break + case "ArrayPattern": + for (const element of getArray(pattern, "elements")) { + if (element !== null) collectPatternNames(asNode(element, "elements"), out) + } + break + case "ObjectPattern": + for (const property of getArray(pattern, "properties")) { + const prop = asNode(property, "properties") + collectPatternNames(prop.type === "RestElement" ? getNode(prop, "argument") : getNode(prop, "value"), out) + } + break + } + return out +} + +class Interpreter { + private scopes: Array> + 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 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() + + constructor( + invokeTool: (path: ReadonlyArray, args: Array) => Effect.Effect, + toolKeys: (path: ReadonlyArray) => ReadonlyArray, + logs: Array = [], + ) { + const globalScope = new Map() + this.scopes = [globalScope] + this.invokeTool = invokeTool + this.toolKeys = toolKeys + this.logs = logs + this.lastValue = undefined + this.callPermits = Semaphore.makeUnsafe(TOOL_CALL_CONCURRENCY) + globalScope.set("tools", { mutable: false, value: new ToolReference([]) }) + globalScope.set("Promise", { mutable: false, value: new PromiseNamespace() }) + globalScope.set("undefined", { mutable: false, value: undefined }) + globalScope.set("Object", { mutable: false, value: new GlobalNamespace("Object") }) + globalScope.set("Math", { mutable: false, value: new GlobalNamespace("Math") }) + globalScope.set("JSON", { mutable: false, value: new GlobalNamespace("JSON") }) + globalScope.set("Number", { mutable: false, value: new CoercionFunction("Number") }) + globalScope.set("String", { mutable: false, value: new CoercionFunction("String") }) + globalScope.set("Boolean", { mutable: false, value: new CoercionFunction("Boolean") }) + globalScope.set("Array", { mutable: false, value: new GlobalNamespace("Array") }) + 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("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") }) + globalScope.set("Set", { mutable: false, value: new GlobalNamespace("Set") }) + globalScope.set("URL", { mutable: false, value: new GlobalNamespace("URL") }) + globalScope.set("URLSearchParams", { mutable: false, value: new GlobalNamespace("URLSearchParams") }) + globalScope.set("encodeURI", { mutable: false, value: new UriFunction("encodeURI") }) + 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() + return Effect.gen(function* () { + self.hoistFunctions(program.body) + let value: unknown = undefined + let returned = false + for (const statement of program.body) { + 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() + return value + }).pipe(Effect.ensuring(Effect.sync(() => self.popScope()))) + } + + // 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. + 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 + }, + ) + } + + // 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) + } + + // `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) + } + + private evaluateStatement(node: AstNode): Effect.Effect { + switch (node.type) { + case "ExpressionStatement": + return Effect.map(this.evaluateExpression(getNode(node, "expression")), (value) => ({ kind: "value", value })) + case "VariableDeclaration": + return Effect.map(this.evaluateVariableDeclaration(node), () => ({ kind: "none" })) + case "ReturnStatement": { + const argumentNode = getOptionalNode(node, "argument") + return argumentNode + ? Effect.map(this.evaluateExpression(argumentNode), (value) => ({ kind: "return", value })) + : Effect.succeed({ kind: "return", value: undefined }) + } + case "BlockStatement": + return this.evaluateBlock(node) + case "IfStatement": + return this.evaluateIfStatement(node) + case "SwitchStatement": + return this.evaluateSwitchStatement(node) + case "WhileStatement": + return this.evaluateWhileStatement(node) + case "DoWhileStatement": + return this.evaluateDoWhileStatement(node) + case "ForStatement": + return this.evaluateForStatement(node) + case "ForOfStatement": + return this.evaluateForOfStatement(node) + case "ForInStatement": + return this.evaluateForInStatement(node) + case "BreakStatement": + return Effect.succeed(this.evaluateBreakStatement(node)) + case "ContinueStatement": + return Effect.succeed(this.evaluateContinueStatement(node)) + case "ThrowStatement": + return this.evaluateThrowStatement(node) + case "TryStatement": + return this.evaluateTryStatement(node) + case "EmptyStatement": + return Effect.succeed({ kind: "none" }) + case "FunctionDeclaration": + return Effect.succeed({ kind: "none" }) // bound ahead of time by hoistFunctions + default: + throw unsupportedSyntax(node.type, node) + } + } + + private evaluateBlock(node: AstNode): Effect.Effect { + this.pushScope() + const self = this + return Effect.gen(function* () { + const body = getArray(node, "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()))) + } + + private createFunction(node: AstNode): CodeModeFunction { + if (node.generator === true) { + throw new InterpreterRuntimeError( + "Generator functions are not supported in CodeMode.", + node, + "UnsupportedSyntax", + [supportedSyntaxMessage], + ) + } + return new CodeModeFunction( + getArray(node, "params").map((parameter, index) => asNode(parameter, `params[${index}]`)), + getNode(node, "body"), + this.scopes.slice(), + ) + } + + // 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) + } + } + + private evaluateIfStatement(node: AstNode): Effect.Effect { + const testNode = getNode(node, "test") + const consequentNode = getNode(node, "consequent") + const alternateNode = getOptionalNode(node, "alternate") + + return Effect.flatMap(this.evaluateExpression(testNode), (test) => + test + ? this.evaluateStatement(consequentNode) + : alternateNode + ? this.evaluateStatement(alternateNode) + : Effect.succeed({ kind: "none" }), + ) + } + + 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)) { + throw new InterpreterRuntimeError( + "Switch discriminants must be data values in CodeMode.", + node, + "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 + } + 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 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()))) + } + + private evaluateWhileStatement(node: AstNode): Effect.Effect { + const testNode = getNode(node, "test") + const bodyNode = getNode(node, "body") + + const self = this + return Effect.gen(function* () { + while (yield* self.evaluateExpression(testNode)) { + const result = yield* self.evaluateStatement(bodyNode) + + if (result.kind === "continue") { + continue + } + + if (result.kind === "break") { + return { kind: "none" } satisfies StatementResult + } + + if (result.kind === "return") { + return result + } + + if (result.kind === "value") { + self.lastValue = result.value + } + } + + return { kind: "none" } satisfies StatementResult + }) + } + + private evaluateDoWhileStatement(node: AstNode): Effect.Effect { + const bodyNode = getNode(node, "body") + const testNode = getNode(node, "test") + + const self = this + return Effect.gen(function* () { + do { + const result = yield* self.evaluateStatement(bodyNode) + + if (result.kind === "continue") { + continue + } + + if (result.kind === "break") { + return { kind: "none" } satisfies StatementResult + } + + if (result.kind === "return") { + return result + } + + if (result.kind === "value") { + self.lastValue = result.value + } + } while (yield* self.evaluateExpression(testNode)) + + return { kind: "none" } satisfies StatementResult + }) + } + + private evaluateForStatement(node: AstNode): Effect.Effect { + this.pushScope() + const self = this + return Effect.gen(function* () { + const initNode = getOptionalNode(node, "init") + const testNode = getOptionalNode(node, "test") + const updateNode = getOptionalNode(node, "update") + const bodyNode = getNode(node, "body") + + if (initNode) { + if (initNode.type === "VariableDeclaration") { + yield* self.evaluateVariableDeclaration(initNode) + } else { + yield* self.evaluateExpression(initNode) + } + } + + const perIterationBindings = + initNode?.type === "VariableDeclaration" && getString(initNode, "kind") !== "var" + ? Array.from(self.currentScope().keys()) + : [] + + 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).pipe( + Effect.ensuring( + Effect.sync(() => { + if (iterationScope) self.popScope() + }), + ), + ) + + if (result.kind === "return") { + return result + } + + if (result.kind === "break") { + 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()))) + } + + 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)] + } + if (Array.isArray(value)) { + return Object.keys(value) + } + if (value !== null && typeof value === "object" && !isRuntimeReference(value)) { + return Object.keys(value) + } + return undefined + } + + private evaluateForInStatement(node: AstNode): Effect.Effect { + 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") + + // 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( + "for...in requires a plain object, array, or tools reference in CodeMode. Use for...of for arrays/strings/Maps/Sets, or Object.keys(value) for a key list.", + 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...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") { + assignmentName = getString(left, "name") + } else { + 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( + 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" } + }) + } + + private evaluateBreakStatement(node: AstNode): StatementResult { + const labelNode = getOptionalNode(node, "label") + + if (labelNode) { + throw new InterpreterRuntimeError("Labeled break is not supported in v1.", node) + } + + return { kind: "break" } + } + + private evaluateContinueStatement(node: AstNode): StatementResult { + const labelNode = getOptionalNode(node, "label") + + if (labelNode) { + throw new InterpreterRuntimeError("Labeled continue is not supported in v1.", node) + } + + return { kind: "continue" } + } + + private evaluateThrowStatement(node: AstNode): Effect.Effect { + const argument = getNode(node, "argument") + return Effect.flatMap(this.evaluateExpression(argument), (value) => Effect.fail(new ProgramThrow(value))) + } + + private evaluateTryStatement(node: AstNode): Effect.Effect { + const body = getNode(node, "block") + const handler = getOptionalNode(node, "handler") + const finalizer = getOptionalNode(node, "finalizer") + const self = this + + const attempted = Effect.matchCauseEffect(this.evaluateStatement(body), { + onFailure: (cause) => { + if (cause.reasons.some(Cause.isInterruptReason) || !handler) { + 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() + 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()))) + }, + onSuccess: Effect.succeed, + }) + + if (!finalizer) return attempted + + const isAbrupt = (result: StatementResult): boolean => + result.kind === "return" || result.kind === "break" || result.kind === "continue" + + return Effect.matchCauseEffect(attempted, { + onFailure: (cause) => + cause.reasons.some(Cause.isInterruptReason) + ? Effect.failCause(cause) + : Effect.flatMap(this.evaluateStatement(finalizer), (final) => + isAbrupt(final) ? Effect.succeed(final) : Effect.failCause(cause), + ), + onSuccess: (result) => + Effect.flatMap(this.evaluateStatement(finalizer), (final) => + isAbrupt(final) ? Effect.succeed(final) : Effect.succeed(result), + ), + }) + } + + private evaluateVariableDeclaration(node: AstNode): Effect.Effect { + const kind = getString(node, "kind") + const declarations = getArray(node, "declarations") + const self = this + return Effect.gen(function* () { + for (const declarationValue of declarations) { + const declaration = asNode(declarationValue, "declarations") + + if (declaration.type !== "VariableDeclarator") { + throw new InterpreterRuntimeError("Unsupported variable declaration shape.", declaration) + } + + const init = getOptionalNode(declaration, "init") + const value = init ? yield* self.evaluateExpression(init) : undefined + yield* self.declarePattern(getNode(declaration, "id"), value, kind !== "const", declaration) + } + }) + } + + private declarePattern( + pattern: AstNode, + value: unknown, + mutable: boolean, + node: AstNode, + ): Effect.Effect { + const self = this + return Effect.gen(function* () { + if (pattern.type === "Identifier") { + self.declare(getString(pattern, "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) + return + } + + if (pattern.type === "ObjectPattern") { + if (value === null || typeof value !== "object" || Array.isArray(value) || isRuntimeReference(value)) { + throw new InterpreterRuntimeError( + "Object destructuring requires a data object value.", + pattern, + "InvalidDataValue", + ) + } + + const consumed = new Set() + 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) + continue + } + + if ( + property.type !== "Property" || + getBoolean(property, "computed") || + getString(property, "kind") !== "init" + ) { + throw new InterpreterRuntimeError("Only named object destructuring properties are supported.", 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) + } + return + } + + if (pattern.type === "ArrayPattern") { + if (!Array.isArray(value)) { + throw new InterpreterRuntimeError("Array destructuring requires an array 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) + break + } + yield* self.declarePattern(element, value[index], mutable, pattern) + } + return + } + + throw new InterpreterRuntimeError(`Unsupported binding pattern '${pattern.type}'.`, pattern) + }) + } + + 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(() => + this.constructRegExp([regex.pattern, typeof regex.flags === "string" ? regex.flags : ""], node), + ) + } + return Effect.sync(() => boundedData(node.value, "Literal")) + } + case "Identifier": + return Effect.sync(() => this.getIdentifierValue(getString(node, "name"), node)) + case "BinaryExpression": + return this.evaluateBinaryExpression(node) + case "LogicalExpression": + return this.evaluateLogicalExpression(node) + case "UnaryExpression": + return this.evaluateUnaryExpression(node) + case "AssignmentExpression": + return this.evaluateAssignmentExpression(node) + case "CallExpression": + return this.evaluateCallExpression(node) + case "ArrowFunctionExpression": + case "FunctionExpression": + return Effect.sync(() => this.createFunction(node)) + case "MemberExpression": + return this.readMember(node) + case "ChainExpression": + return Effect.map(this.evaluateExpression(getNode(node, "expression")), (value) => + value === OptionalShortCircuit ? undefined : value, + ) + case "ObjectExpression": + return this.evaluateObjectExpression(node) + case "ArrayExpression": + return this.evaluateArrayExpression(node) + case "TemplateLiteral": + return this.evaluateTemplateLiteral(node) + case "ConditionalExpression": + return this.evaluateConditionalExpression(node) + 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. + const self = this + return Effect.flatMap(this.evaluateExpression(getNode(node, "argument")), (value) => + value instanceof SandboxPromise ? self.settlePromise(value, node) : Effect.succeed(value), + ) + } + case "NewExpression": + return this.evaluateNewExpression(node) + default: + throw unsupportedSyntax(node.type, node) + } + } + + private evaluateNewExpression(node: AstNode): Effect.Effect { + const callee = getNode(node, "callee") + if (callee.type !== "Identifier") { + throw unsupportedSyntax("NewExpression", node) + } + const name = getString(callee, "name") + 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], + ) + } + 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)) + }) + } + if (valueConstructors.has(name)) { + return Effect.gen(function* () { + const args = yield* self.evaluateCallArguments(argNodes) + switch (name) { + case "Date": + return self.constructDate(args) + case "RegExp": + return self.constructRegExp(args, node) + case "Map": + return self.constructMap(args[0], node) + case "Set": + return self.constructSet(args[0], node) + case "URL": + return self.constructURL(args, node) + default: + return self.constructURLSearchParams(args[0], node) + } + }) + } + 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) + } + // 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()) + } + + private constructRegExp(args: Array, node: AstNode): SandboxRegExp { + const first = args[0] + const pattern = + first instanceof SandboxRegExp ? 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, + ) + } + const flags = flagsArg ?? (first instanceof SandboxRegExp ? first.regex.flags : "") + try { + return new SandboxRegExp(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) + ? `new RegExp(...) received invalid flags ${JSON.stringify(flags)} (${reason}). Valid flags are d, g, i, m, s, u, v, and y.` + : `new RegExp(...) received ${JSON.stringify(pattern)}, which is not a valid regular expression pattern (${reason}). ${escapeRegexHint}`, + node, + ).as("SyntaxError") + } + } + + private constructMap(init: unknown, node: AstNode): SandboxMap { + const target = new SandboxMap() + if (init === undefined || init === null) return target + const entries = Array.isArray(init) + ? init + : init instanceof SandboxMap + ? Array.from(init.map.entries(), ([key, item]): Array => [key, item]) + : undefined + if (entries === undefined) { + throw new InterpreterRuntimeError( + "new Map(...) expects an array of [key, value] pairs, a Map, or no argument.", + node, + ) + } + for (const pair of entries) { + if (!Array.isArray(pair)) { + throw new InterpreterRuntimeError("new Map(...) expects [key, value] pairs.", node) + } + target.map.set(pair[0], pair[1]) + } + return target + } + + private constructSet(init: unknown, node: AstNode): SandboxSet { + const target = new SandboxSet() + if (init === undefined || init === null) return target + const items = Array.isArray(init) + ? init + : init instanceof SandboxSet + ? Array.from(init.set.values()) + : typeof init === "string" + ? Array.from(init) + : undefined + if (items === undefined) { + throw new InterpreterRuntimeError("new Set(...) expects an array, Set, string, or no argument.", node) + } + for (const item of items) target.set.add(item) + return target + } + + private constructURL(args: Array, node: AstNode): SandboxURL { + if (args.length === 0) { + throw new InterpreterRuntimeError("new URL(...) requires a URL string and an optional base URL.", node).as( + "TypeError", + ) + } + 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)) + } catch { + throw new InterpreterRuntimeError( + `new URL(...) received an invalid URL${base === undefined ? "" : " or base URL"}.`, + node, + ).as("TypeError") + } + } + + 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)) + } + if (typeof init === "string") return new SandboxURLSearchParams(new URLSearchParams(init)) + if (init === null || typeof init === "number" || typeof init === "boolean") { + return new SandboxURLSearchParams(new URLSearchParams(coerceToString(init))) + } + if (init instanceof SandboxMap) { + return this.constructURLSearchParams( + Array.from(init.map.entries(), ([key, value]) => [key, value]), + node, + ) + } + if (Array.isArray(init)) { + const entries = init.map((pair) => { + if (!Array.isArray(pair) || pair.length !== 2) { + throw new InterpreterRuntimeError( + "new URLSearchParams(...) expects an array of [name, value] pairs.", + node, + ).as("TypeError") + } + return [uriArgument(pair[0], "URLSearchParams name"), uriArgument(pair[1], "URLSearchParams value")] as [ + string, + string, + ] + }) + return new SandboxURLSearchParams(new URLSearchParams(entries)) + } + if (isSandboxValue(init)) return new SandboxURLSearchParams(new URLSearchParams()) + const data = boundedData(init, "new URLSearchParams input") + if (data === null || typeof data !== "object") { + throw new InterpreterRuntimeError( + "new URLSearchParams(...) expects a query string, data object, array of pairs, or URLSearchParams.", + node, + ).as("TypeError") + } + return new SandboxURLSearchParams( + new URLSearchParams(Object.fromEntries(Object.entries(data).map(([key, value]) => [key, coerceToString(value)]))), + ) + } + + private evaluateBinaryExpression(node: AstNode): Effect.Effect { + const operator = getString(node, "operator") + const self = this + 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. + const coerceOperand = (operand: unknown): unknown => { + if (operand instanceof SandboxDate) 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" + const l = coerceOperand(lhs) + const r = coerceOperand(rhs) + switch (operator) { + case "+": + return (l as string) + (r as string) + case "-": + return (l as number) - (r as number) + case "*": + return (l as number) * (r as number) + case "/": + return (l as number) / (r as number) + case "%": + 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 "===": + return lhs === rhs + case "!=": + return bothObjects ? lhs !== rhs : l != r + case "!==": + return lhs !== rhs + case "<": + return (l as string) < (r as string) + case "<=": + return (l as string) <= (r as string) + case ">": + return (l as string) > (r as string) + case ">=": + return (l as string) >= (r as string) + case "&": + return (l as number) & (r as number) + case "|": + return (l as number) | (r as number) + case "^": + return (l as number) ^ (r as number) + case "<<": + return (l as number) << (r as number) + case ">>": + return (l as number) >> (r as number) + case ">>>": + return (l as number) >>> (r as number) + case "in": + 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/...). + return Object.hasOwn(rhs as object, coerceOperand(lhs) as PropertyKey) + default: + throw new InterpreterRuntimeError(`Unsupported binary operator '${operator}'.`, node) + } + } + + private evaluateLogicalExpression(node: AstNode): Effect.Effect { + const operator = getString(node, "operator") + return Effect.flatMap(this.evaluateExpression(getNode(node, "left")), (left) => { + if (operator === "&&") return left ? this.evaluateExpression(getNode(node, "right")) : Effect.succeed(left) + if (operator === "||") return left ? Effect.succeed(left) : this.evaluateExpression(getNode(node, "right")) + if (operator === "??") + return left !== null && left !== undefined + ? Effect.succeed(left) + : this.evaluateExpression(getNode(node, "right")) + throw new InterpreterRuntimeError(`Unsupported logical operator '${operator}'.`, node) + }) + } + + 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"))) { + 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 (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.time + : value !== null && typeof value === "object" + ? coerceToString(value) + : value + let result: unknown + switch (operator) { + case "+": + result = +(operand as number) + break + case "-": + result = -(operand as number) + break + case "~": + result = ~(operand as number) + break + default: + throw new InterpreterRuntimeError(`Unsupported unary operator '${operator}'.`, node) + } + return boundedData(result, "Unary expression result") + }) + } + + private evaluateAssignmentExpression(node: AstNode): Effect.Effect { + const left = getNode(node, "left") + const operator = getString(node, "operator") + const self = this + return Effect.gen(function* () { + if (operator === "??=" || operator === "||=" || operator === "&&=") { + return yield* self.evaluateLogicalAssignment(node, left, operator) + } + const rightValue = yield* self.evaluateExpression(getNode(node, "right")) + 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) => { + const next = boundedData( + self.applyCompoundAssignment(operator, current, rightValue, node), + "Assignment result", + ) + return Effect.succeed({ write: true, next, result: next }) + }) + } + throw new InterpreterRuntimeError("Assignment target must be an Identifier or MemberExpression.", left) + }) + } + + private evaluateLogicalAssignment( + node: AstNode, + left: AstNode, + operator: string, + ): Effect.Effect { + const self = this + const shouldAssign = (current: unknown): boolean => + operator === "??=" ? current === null || current === undefined : operator === "||=" ? !current : Boolean(current) + if (left.type === "Identifier") { + const name = getString(left, "name") + return Effect.gen(function* () { + const current = self.getIdentifierValue(name, left) + if (!shouldAssign(current)) return current + const rightValue = yield* self.evaluateExpression(getNode(node, "right")) + return self.setIdentifierValue(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) => ({ + write: true, + next: rightValue, + result: rightValue, + })) + : Effect.succeed({ write: false, next: current, result: current }), + ) + } + throw new InterpreterRuntimeError("Assignment target must be an Identifier or MemberExpression.", left) + } + + private evaluateUpdateExpression(node: AstNode): Effect.Effect { + const operator = getString(node, "operator") + const argument = getNode(node, "argument") + const prefix = getBoolean(node, "prefix") + + const increment = operator === "++" ? 1 : operator === "--" ? -1 : undefined + + if (increment === undefined) { + throw new InterpreterRuntimeError(`Unsupported update operator '${operator}'.`, node) + } + + if (argument.type === "Identifier") { + return Effect.sync(() => { + const name = getString(argument, "name") + const current = Number(this.getIdentifierValue(name, argument)) + const next = current + increment + this.setIdentifierValue(name, next, argument) + return prefix ? next : current + }) + } + + if (argument.type === "MemberExpression") { + return this.modifyMember(argument, (current) => { + const value = Number(current) + const next = value + increment + return Effect.succeed({ write: true, next, result: prefix ? next : value }) + }) + } + + throw new InterpreterRuntimeError("Update target must be an Identifier or MemberExpression.", argument) + } + + private evaluateCallExpression(node: AstNode): Effect.Effect { + const callee = getNode(node, "callee") + const argNodes = getArray(node, "arguments") + + const self = this + return Effect.gen(function* () { + const callable = yield* self.evaluateExpression(callee) + if (callable === OptionalShortCircuit) return OptionalShortCircuit + if ((callable === null || callable === undefined) && node.optional === true) return OptionalShortCircuit + + const args = yield* self.evaluateCallArguments(argNodes) + + 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) + } + if (callable instanceof CodeModeFunction) { + return yield* self.invokeFunction(callable, args) + } + if (callable instanceof IntrinsicReference) { + return yield* self.invokeIntrinsic(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 boundedData(invokeGlobalMethod(callable, args, node), `${callable.namespace}.${callable.name} result`) + } + if (callable instanceof CoercionFunction) { + return boundedData(invokeCoercion(callable, args, node), `${callable.name} result`) + } + 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 ErrorConstructorReference) { + return createErrorValue(callable.name, args[0] === undefined ? "" : coerceToString(args[0])) + } + 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.`, + node, + "InvalidDataValue", + ) + } + + 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))) + 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* () { + const args: Array = [] + for (const [index, arg] of argNodes.entries()) { + const argNode = asNode(arg, `arguments[${index}]`) + if (argNode.type === "SpreadElement") { + const spread = yield* self.evaluateExpression(getNode(argNode, "argument")) + const items = spreadItems(spread) + if (items === undefined) + throw new InterpreterRuntimeError( + "Spread arguments require an array, string, Map, or Set in CodeMode.", + argNode, + ) + args.push(...items) + } else { + args.push(yield* self.evaluateExpression(argNode)) + } + } + return args + }) + } + + // 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 }) + } + } + 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) + } + + if (fn.body.type === "BlockStatement") { + const result = yield* self.evaluateStatement(fn.body) + return result.kind === "return" || result.kind === "value" ? result.value : undefined + } + + return yield* self.evaluateExpression(fn.body) + }) + return run.pipe( + Effect.ensuring( + Effect.sync(() => { + self.scopes = savedScopes + }), + ), + ) + }) + } + + 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 + }), + ) + } + 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)]) + } + + private evaluateObjectExpression(node: AstNode): Effect.Effect, unknown, R> { + const objectValue: Record = Object.create(null) as Record + const properties = getArray(node, "properties") + const self = this + return Effect.gen(function* () { + for (const propertyValue of properties) { + const property = asNode(propertyValue, "properties") + + 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 (typeof spread !== "object" || Array.isArray(spread) || isRuntimeReference(spread)) { + throw new InterpreterRuntimeError( + "Object spread requires a data object in CodeMode.", + property, + "InvalidDataValue", + ) + } + for (const [key, value] of Object.entries(spread)) { + if (isBlockedMember(key)) + throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, property) + objectValue[key] = value + } + continue + } + + if (property.type !== "Property") { + throw new InterpreterRuntimeError("Only standard object properties are supported.", property) + } + + if (getString(property, "kind") !== "init") { + throw new InterpreterRuntimeError("Only init object properties are supported.", property) + } + + const keyNode = getNode(property, "key") + const valueNode = getNode(property, "value") + const computed = getBoolean(property, "computed") + + let key: PropertyKey + + if (computed) { + key = self.toPropertyKey(yield* self.evaluateExpression(keyNode), keyNode) + } else if (keyNode.type === "Identifier") { + key = getString(keyNode, "name") + } else if (keyNode.type === "Literal") { + key = self.toPropertyKey(keyNode.value, keyNode) + } else { + throw new InterpreterRuntimeError("Unsupported object property key shape.", keyNode) + } + + if (isBlockedMember(String(key))) { + throw new InterpreterRuntimeError(`Property '${String(key)}' is not available in CodeMode.`, keyNode) + } + objectValue[String(key)] = yield* self.evaluateExpression(valueNode) + } + + return objectValue + }) + } + + private evaluateArrayExpression(node: AstNode): Effect.Effect, unknown, R> { + const elements = getArray(node, "elements") + const values: Array = [] + + const self = this + return Effect.gen(function* () { + for (const elementValue of elements) { + if (elementValue === null) { + values.push(undefined) + continue + } + const element = asNode(elementValue, "elements") + if (element.type === "SpreadElement") { + const spread = yield* self.evaluateExpression(getNode(element, "argument")) + const items = spreadItems(spread) + if (items === undefined) + throw new InterpreterRuntimeError( + "Array spread requires an array, string, Map, or Set in CodeMode.", + element, + ) + values.push(...items) + } else { + values.push(yield* self.evaluateExpression(element)) + } + } + return values + }) + } + + private evaluateTemplateLiteral(node: AstNode): Effect.Effect { + const quasis = getArray(node, "quasis") + const expressions = getArray(node, "expressions") + + let output = "" + + const self = this + return Effect.gen(function* () { + for (let index = 0; index < quasis.length; index += 1) { + const quasi = asNode(quasis[index], "quasis") + const rawValue = quasi.value + + if (!isRecord(rawValue) || typeof rawValue.cooked !== "string") { + throw new InterpreterRuntimeError("Invalid template literal quasi.", quasi) + } + + output += rawValue.cooked + + 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")) + } + } + + return output + }) + } + + private evaluateConditionalExpression(node: AstNode): Effect.Effect { + return Effect.flatMap(this.evaluateExpression(getNode(node, "test")), (test) => + this.evaluateExpression(getNode(node, test ? "consequent" : "alternate")), + ) + } + + 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) + } + return this.applyBinaryOperator(operator.slice(0, -1), current, incoming, node) + } + + private getMemberReference( + node: AstNode, + ): Effect.Effect< + | MemberReference + | ToolReference + | PromiseMethodReference + | IntrinsicReference + | GlobalMethodReference + | ComputedValue + | typeof OptionalShortCircuit + | undefined, + unknown, + R + > { + const objectNode = getNode(node, "object") + const propertyNode = getNode(node, "property") + const computed = getBoolean(node, "computed") + const optional = node.optional === true + const self = this + return Effect.gen(function* () { + const objectValue = yield* self.evaluateExpression(objectNode) + if (objectValue === OptionalShortCircuit) return OptionalShortCircuit + if ((objectValue === null || objectValue === undefined) && optional) return OptionalShortCircuit + + const key = computed + ? self.toPropertyKey(yield* self.evaluateExpression(propertyNode), propertyNode) + : propertyNode.type === "Identifier" + ? getString(propertyNode, "name") + : 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) + } + return new ToolReference([...objectValue.path, key]) + } + + if (objectValue instanceof PromiseNamespace) { + if (typeof key === "string" && promiseStatics.has(key as PromiseMethodName)) { + 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.`, + 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 (objectValue.name === "Math" && mathConstants.has(key)) { + return new ComputedValue((Math as unknown as Record)[key]) + } + return new GlobalMethodReference(objectValue.name, key) + } + + if (typeof objectValue === "string") { + if (key === "length") return new ComputedValue(objectValue.length) + 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.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) + } + + // 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 (typeof key === "string" && dateMethods.has(key)) return new IntrinsicReference(objectValue, key) + return new ComputedValue(undefined) + } + if (objectValue instanceof SandboxRegExp) { + 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 (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 (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 (key === "searchParams") { + return new ComputedValue(objectValue.searchParams) + } + if (typeof key === "string" && urlMethods.has(key)) return new IntrinsicReference(objectValue, key) + if (typeof key === "string" && urlProperties.has(key)) return { target: objectValue, key } + return new ComputedValue(undefined) + } + if (objectValue instanceof SandboxURLSearchParams) { + if (key === "size") return new ComputedValue(objectValue.params.size) + if (typeof key === "string" && urlSearchParamsMethods.has(key)) { + return new IntrinsicReference(objectValue, key) + } + 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) { + 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], + ) + } + 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(...)`.", + objectNode, + "InvalidDataValue", + ) + } + + if (isRuntimeReference(objectValue)) { + throw new InterpreterRuntimeError( + "CodeMode runtime references are opaque and do not expose properties.", + objectNode, + "InvalidDataValue", + ) + } + + if (typeof objectValue !== "object" || objectValue === null) { + throw new InterpreterRuntimeError("Cannot access a property on a non-object value.", objectNode) + } + + if (typeof key === "string" && isBlockedMember(key)) { + throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, propertyNode) + } + + if (Array.isArray(objectValue)) { + 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 } + } + + return { target: objectValue as SafeObject, key } + }) + } + + private readMember(node: AstNode): Effect.Effect { + return Effect.map(this.getMemberReference(node), (reference) => { + if (reference === OptionalShortCircuit) return OptionalShortCircuit + if (reference instanceof ComputedValue) return reference.value + if ( + reference === undefined || + reference instanceof ToolReference || + reference instanceof PromiseMethodReference || + reference instanceof IntrinsicReference || + reference instanceof GlobalMethodReference + ) + return reference + if (Array.isArray(reference.target)) { + if (typeof reference.key === "string" && arrayMethods.has(reference.key)) { + return new IntrinsicReference(reference.target, reference.key) + } + return reference.key === "length" ? reference.target.length : reference.target[Number(reference.key)] + } + if (reference.target instanceof SandboxURL) { + return (reference.target.url as unknown as Record)[String(reference.key)] + } + return reference.target[String(reference.key)] + }) + } + + private writeMember(node: AstNode, value: unknown): Effect.Effect { + 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 modifyMember( + node: AstNode, + compute: (current: unknown) => Effect.Effect<{ write: boolean; next: unknown; result: unknown }, unknown, R>, + ): Effect.Effect { + const self = this + return Effect.gen(function* () { + const reference = yield* self.getMemberReference(node) + if ( + reference === OptionalShortCircuit || + reference instanceof ComputedValue || + reference === undefined || + reference instanceof ToolReference || + reference instanceof PromiseMethodReference || + reference instanceof IntrinsicReference || + reference instanceof GlobalMethodReference + ) { + throw new InterpreterRuntimeError("Only data fields may be assigned in CodeMode.", node) + } + if (Array.isArray(reference.target)) { + if (reference.key === "length") + throw new InterpreterRuntimeError("Array length cannot be assigned in CodeMode.", node) + if (typeof reference.key === "string" && arrayMethods.has(reference.key)) { + throw new InterpreterRuntimeError("Array methods cannot be assigned in CodeMode.", node) + } + } + const key = Array.isArray(reference.target) ? Number(reference.key) : String(reference.key) + const current = + reference.target instanceof SandboxURL + ? (reference.target.url as unknown as Record)[key] + : (reference.target as Record)[key] + const { write, next, result } = yield* compute(current) + if (write) self.assignToReference(reference, key, next, node) + return result + }) + } + + // 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 + const index = key as number + if (!Number.isInteger(index) || index < 0) { + throw new InterpreterRuntimeError( + "Array assignment index must be a non-negative integer.", + node, + "InvalidDataValue", + ) + } + this.rejectCircularInsertion(target, next, "Array assignment result", node) + target[index] = next + return + } + if (reference.target instanceof SandboxURL) { + const property = key as string + if (!urlWritableProperties.has(property)) { + throw new InterpreterRuntimeError(`URL.${property} is read-only.`, node).as("TypeError") + } + try { + const url = reference.target.url as unknown as Record + url[property] = uriArgument(next, `URL.${property} value`) + return + } catch (error) { + if (error instanceof InterpreterRuntimeError || error instanceof ToolRuntimeError) throw error + throw new InterpreterRuntimeError(`URL.${property} received an invalid value.`, node).as("TypeError") + } + } + const target = reference.target as SafeObject + const objectKey = key as string + this.rejectCircularInsertion(target, next, "Object assignment result", node) + target[objectKey] = next + } + + private toPropertyKey(value: unknown, node: AstNode): string | number { + if (typeof value === "string" || typeof value === "number") { + return value + } + + 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/openapi/TODO.md b/packages/codemode/src/openapi/TODO.md new file mode 100644 index 00000000000..cbcfe81a68c --- /dev/null +++ b/packages/codemode/src/openapi/TODO.md @@ -0,0 +1,19 @@ +# OpenAPI Follow-ups + +The initial adapter intentionally skips operations it cannot execute correctly. Future work may add: + +- 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. +- 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. +- Adapter-enforced redirect policy independent of the supplied `HttpClient`. +- Strict UTF-8 and empty-body validation for JSON responses. +- Compile-time rejection of parameter schemas with nested values unsupported by their serialization style; runtime rejects them before auth resolution. +- Complete malformed-security-scheme validation and broader auth-combination coverage. diff --git a/packages/codemode/src/openapi/index.ts b/packages/codemode/src/openapi/index.ts new file mode 100644 index 00000000000..7f1770ef362 --- /dev/null +++ b/packages/codemode/src/openapi/index.ts @@ -0,0 +1,130 @@ +import { HttpClient } from "effect/unstable/http" +import { make, type Definition } from "../tool.js" +import { invoke } from "./runtime.js" +import { + componentDefinitions, + inputSchema, + isRecord, + methods, + nonEmptyString, + operationInput, + operationOutput, + operationPath, + operationSecurityRequirements, + securityRequirements, + securitySchemes, + specServerUrl, + validateBaseUrl, +} from "./spec.js" +import type { Operation, Options, Result, Skipped, Tools } from "./types.js" + +export type { + AuthResolver, + Credential, + Document, + Operation, + Options, + Result, + SecurityScheme, + Skipped, + Tools, +} 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`. + */ +export const fromSpec = (options: Options): Result => { + const document = options.spec + const schemes = securitySchemes(document) + const defaultSecurity = securityRequirements(document.security) + const definitions = componentDefinitions(document) + const paths = isRecord(document.paths) ? document.paths : {} + const used = new Set() + const namespaces = new Set() + const skipped: Array = [] + const tools = Object.create(null) as Tools + + for (const [path, pathValue] of Object.entries(paths)) { + if (!isRecord(pathValue)) continue + for (const [method, operationValue] of Object.entries(pathValue)) { + if (!methods.has(method) || !isRecord(operationValue)) continue + const segments = operationPath(method, path, operationValue, used, namespaces) + const operation: Operation = { + operationId: nonEmptyString(operationValue.operationId), + method: method.toUpperCase(), + path, + summary: nonEmptyString(operationValue.summary), + description: nonEmptyString(operationValue.description), + } + const output = operationOutput(document, operationValue, definitions) + if (!output.ok) { + skipped.push({ method: operation.method, path, reason: output.reason }) + continue + } + + const resolvedBaseUrl = (() => { + if (options.baseUrl !== undefined) return validateBaseUrl(options.baseUrl) + if (operationValue.servers !== undefined) return specServerUrl(operationValue) + if (pathValue.servers !== undefined) return specServerUrl(pathValue) + return specServerUrl(document) + })() + if (!resolvedBaseUrl.ok) { + skipped.push({ method: operation.method, path, reason: resolvedBaseUrl.reason }) + continue + } + const parsedInput = operationInput(document, pathValue, operationValue) + if (!parsedInput.ok) { + skipped.push({ method: operation.method, path, reason: parsedInput.reason }) + continue + } + const input = parsedInput.value + + const security = operationSecurityRequirements(operationValue.security, defaultSecurity, schemes) + if (!security.ok) { + skipped.push({ method: operation.method, path, reason: security.reason }) + continue + } + const plan = { + operation, + url: `${resolvedBaseUrl.value.replace(/\/+$/, "")}${path}`, + fields: input.fields, + body: input.body, + security: security.value, + schemes, + auth: options.auth, + headers: options.headers ?? {}, + } + used.add(segments.join(".")) + for (const index of segments.slice(0, -1).keys()) namespaces.add(segments.slice(0, index + 1).join(".")) + setTool( + tools, + segments, + make({ + description: operation.description ?? operation.summary ?? `${operation.method} ${path}`, + input: inputSchema(input.fields, definitions), + output: output.value, + run: (input) => invoke(plan, input), + }), + ) + } + } + + return { tools, skipped } +} + +const setTool = (tools: Tools, path: ReadonlyArray, definition: Definition): void => { + const [head, ...rest] = path + if (head === undefined) return + if (rest.length === 0) { + tools[head] = definition + return + } + const child = tools[head] + if (child === undefined || !isRecord(child) || child._tag === "CodeModeTool") { + tools[head] = Object.create(null) as Tools + } + setTool(tools[head] as Tools, rest, definition) +} diff --git a/packages/codemode/src/openapi/runtime.ts b/packages/codemode/src/openapi/runtime.ts new file mode 100644 index 00000000000..2515621792b --- /dev/null +++ b/packages/codemode/src/openapi/runtime.ts @@ -0,0 +1,326 @@ +import { Effect, Option, Schema, Stream } from "effect" +import { HttpClient, HttpClientRequest, HttpClientResponse, type HttpMethod } from "effect/unstable/http" +import { ToolError, toolError } from "../tool-error.js" +import { isRecord, own } from "./spec.js" +import type { AppliedAuth, Credential, Plan, SecurityScheme } from "./types.js" + +const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString) +const maxErrorBodyChars = 1_024 +const maxResponseBodyBytes = 50 * 1024 * 1024 + +export const invoke = (plan: Plan, input: unknown): Effect.Effect => + Effect.gen(function* () { + const value = isRecord(input) ? input : {} + + let request = yield* buildRequest(plan, value) + + const auth = yield* resolveAuth(plan) + for (const [name, item] of Object.entries(auth.query)) { + request = HttpClientRequest.setUrlParam(request, name, item) + } + request = HttpClientRequest.setHeaders(request, auth.headers) + + const client = yield* HttpClient.HttpClient + const response = yield* client + .execute(request) + .pipe( + Effect.catch((cause) => + Effect.fail(toolError(`${plan.operation.method} ${plan.operation.path} failed: transport error`, cause)), + ), + ) + const text = yield* readResponseBody(response, plan) + const mediaType = response.headers["content-type"]?.split(";")[0]?.trim().toLowerCase() + const json = mediaType === "application/json" || mediaType?.endsWith("+json") === true + const decoded = text === "" ? Option.some(null) : json ? decodeJson(text) : Option.none() + const parsed = json ? Option.getOrElse(decoded, () => text) : text === "" ? null : text + if (response.status < 200 || response.status >= 300) { + const rendered = typeof parsed === "string" ? parsed : (JSON.stringify(parsed) ?? "") + const summary = + rendered === "" || rendered === "null" + ? "no response body" + : rendered.length > maxErrorBodyChars + ? `${rendered.slice(0, maxErrorBodyChars)}...` + : rendered + return yield* Effect.fail( + toolError(`${plan.operation.method} ${plan.operation.path} failed with HTTP ${response.status}: ${summary}`), + ) + } + if (json && Option.isNone(decoded)) { + return yield* Effect.fail(toolError(`${plan.operation.method} ${plan.operation.path} returned malformed JSON.`)) + } + return parsed + }) + +const buildRequest = ( + plan: Plan, + input: Readonly>, +): Effect.Effect => + Effect.gen(function* () { + // Validate every model-controlled value before auth resolution, which may refresh tokens. + const url = buildUrl(plan, input) + if (url instanceof ToolError) return yield* Effect.fail(url) + const missing = plan.fields.find( + (field) => field.required && field.location !== "path" && own(input, field.inputName) === undefined, + ) + if (missing !== undefined) { + const label = missing.location === "body" ? "body field" : `${missing.location} parameter` + return yield* Effect.fail(toolError(`Missing required ${label} '${missing.inputName}'.`)) + } + + let request = HttpClientRequest.make(plan.operation.method as HttpMethod.HttpMethod)(url) + 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) + if (serialized instanceof ToolError) return yield* Effect.fail(serialized) + request = serialized + } + + // Host headers first, then declared header parameters. + request = HttpClientRequest.setHeaders(request, plan.headers) + for (const field of plan.fields) { + if (field.location !== "header") continue + const item = own(input, field.inputName) + if (item === undefined) continue + const serialized = serializeSimple(field, item, String) + if (serialized instanceof ToolError) return yield* Effect.fail(serialized) + request = HttpClientRequest.setHeader(request, field.name, serialized) + } + + const setBody = (value: unknown, mediaType: string) => + HttpClientRequest.bodyJson(request, value).pipe( + Effect.map((next) => HttpClientRequest.setHeader(next, "content-type", mediaType)), + Effect.mapError((cause) => + toolError(`Invalid JSON body for ${plan.operation.method} ${plan.operation.path}.`, cause), + ), + ) + if (plan.body?.mode === "value") { + const field = plan.fields.find((field) => field.location === "body") + const body = field === undefined ? undefined : own(input, field.inputName) + if (body !== undefined) request = yield* setBody(body, plan.body.mediaType) + } + if (plan.body?.mode === "object") { + const entries = plan.fields.flatMap((field) => { + if (field.location !== "body") return [] + const item = own(input, field.inputName) + return item === undefined ? [] : [[field.name, item] as const] + }) + if (plan.body.required || entries.length > 0) { + request = yield* setBody(Object.fromEntries(entries), plan.body.mediaType) + } + } + return request + }) + +const resolveAuth = (plan: Plan): Effect.Effect => + Effect.gen(function* () { + const none: AppliedAuth = { headers: {}, query: {} } + if (plan.security.length === 0) return none + + const unavailable: Array = [] + alternatives: for (const requirement of plan.security) { + const names = Object.keys(requirement) + if (names.length === 0) return none + const credentials: Array = [] + for (const name of names) { + const scheme = own(plan.schemes, name) + if (scheme === undefined || plan.auth === undefined) { + unavailable.push(name) + continue alternatives + } + const credential = yield* plan.auth.resolve({ + name, + definition: scheme, + scopes: requirement[name] ?? [], + operation: plan.operation, + }) + if (credential === undefined) { + unavailable.push(name) + continue alternatives + } + credentials.push([name, scheme, credential]) + } + const applied = applyCredentials(credentials) + return applied instanceof ToolError ? yield* Effect.fail(applied) : applied + } + + return yield* Effect.fail( + toolError( + `${plan.operation.method} ${plan.operation.path} requires authentication; no credential available for: ${[...new Set(unavailable)].join(", ")}.`, + ), + ) + }) + +const applyCredentials = ( + credentials: ReadonlyArray, +): AppliedAuth | ToolError => { + const headers = new Map() + const query = new Map() + const add = (carrier: "header" | "query", name: string, value: string): ToolError | undefined => { + const target = carrier === "header" ? headers : query + if (target.has(name)) return toolError(`Authentication resolves multiple credentials for ${carrier} '${name}'.`) + target.set(name, value) + } + for (const [name, definition, credential] of credentials) { + if (credential.type === "bearer") { + const duplicate = add("header", "authorization", `Bearer ${credential.token}`) + if (duplicate !== undefined) return duplicate + continue + } + if (credential.type === "basic") { + // Buffer instead of btoa: btoa throws on non-Latin-1 credentials. + const duplicate = add( + "header", + "authorization", + `Basic ${Buffer.from(`${credential.username}:${credential.password}`, "utf8").toString("base64")}`, + ) + if (duplicate !== undefined) return duplicate + continue + } + if (credential.type === "header") { + const duplicate = add("header", credential.name.toLowerCase(), credential.value) + 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.`, + ) + } + if (definition.in === "cookie") return toolError(`Cookie authentication '${name}' is not supported.`) + const parameter = definition.in === "header" ? definition.name.toLowerCase() : definition.name + const duplicate = add(definition.in, parameter, credential.value) + if (duplicate !== undefined) return duplicate + } + return { headers: Object.fromEntries(headers), query: Object.fromEntries(query) } +} + +const buildUrl = (plan: Plan, input: Readonly>): string | ToolError => { + let url = plan.url + for (const field of plan.fields) { + if (field.location !== "path") continue + const item = own(input, field.inputName) + if (item === undefined) { + return toolError(`Missing required path parameter '${field.inputName}'.`) + } + const fieldValue = serializeSimple(field, item, (value) => + encodeURIComponent(value).replace( + /[!'()*]/g, + (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`, + ), + ) + 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. + if (fieldValue === "" || fieldValue === "." || fieldValue === "..") { + return toolError(`Invalid path parameter '${field.inputName}'.`) + } + url = url.replaceAll(`{${field.name}}`, fieldValue) + } + const unresolved = url.match(/\{[^{}]+\}/) + if (unresolved !== null) return toolError(`Unresolved path parameter ${unresolved[0]}.`) + return url +} + +const serializeSimple = ( + field: Plan["fields"][number], + value: unknown, + encode: (value: string) => string, +): string | ToolError => { + const scalar = (item: unknown): string | ToolError => + item !== null && typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean" + ? toolError(`Parameter '${field.inputName}' contains an unsupported nested value.`) + : encode(String(item)) + if (Array.isArray(value)) { + const items = value.map(scalar) + const invalid = items.find((item): item is ToolError => item instanceof ToolError) + return invalid ?? items.join(",") + } + if (!isRecord(value)) return scalar(value) + const entries = Object.entries(value).flatMap(([name, item]) => { + const rendered = scalar(item) + if (rendered instanceof ToolError) return [rendered] + return field.explode ? [`${encode(name)}=${rendered}`] : [encode(name), rendered] + }) + const invalid = entries.find((item): item is ToolError => item instanceof ToolError) + return invalid ?? entries.join(",") +} + +const serializeQuery = ( + request: HttpClientRequest.HttpClientRequest, + field: Plan["fields"][number], + value: unknown, +): HttpClientRequest.HttpClientRequest | 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 + 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) + } + 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.`) + } + return value.reduce((current, item) => HttpClientRequest.appendUrlParam(current, field.name, String(item)), request) + } + if (isRecord(value) && field.explode) { + return Object.entries(value).reduce((current, [name, item]) => { + if (current instanceof ToolError) return current + 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) + } + const rendered = serializeSimple(field, value, String) + return rendered instanceof ToolError ? rendered : HttpClientRequest.appendUrlParam(request, field.name, rendered) +} + +const readResponseBody = ( + response: HttpClientResponse.HttpClientResponse, + plan: Plan, +): Effect.Effect => + Effect.gen(function* () { + const contentLength = response.headers["content-length"] + const parsedSize = contentLength === undefined ? undefined : Number.parseInt(contentLength, 10) + const declaredSize = + parsedSize !== undefined && Number.isSafeInteger(parsedSize) && parsedSize >= 0 ? parsedSize : undefined + if (declaredSize !== undefined && declaredSize > maxResponseBodyBytes) { + return yield* Effect.fail(toolError(`${plan.operation.method} ${plan.operation.path} response exceeds 50 MiB.`)) + } + let body = Buffer.allocUnsafe(Math.min(maxResponseBodyBytes, declaredSize ?? 64 * 1024)) + let size = 0 + yield* Stream.runForEach(response.stream, (chunk) => { + if (size + chunk.byteLength > maxResponseBodyBytes) { + return Effect.fail(toolError(`${plan.operation.method} ${plan.operation.path} response exceeds 50 MiB.`)) + } + if (size + chunk.byteLength > body.byteLength) { + const grown = Buffer.allocUnsafe( + Math.min(maxResponseBodyBytes, Math.max(size + chunk.byteLength, body.byteLength * 2)), + ) + body.copy(grown, 0, 0, size) + body = grown + } + body.set(chunk, size) + size += chunk.byteLength + return Effect.void + }).pipe( + Effect.catch((cause) => { + if (cause instanceof ToolError) return Effect.fail(cause) + if (cause.reason._tag === "EmptyBodyError") return Effect.void + return Effect.fail( + toolError(`${plan.operation.method} ${plan.operation.path} failed while reading the response body.`, cause), + ) + }), + ) + return new TextDecoder().decode(body.subarray(0, size)) + }) diff --git a/packages/codemode/src/openapi/spec.ts b/packages/codemode/src/openapi/spec.ts new file mode 100644 index 00000000000..bd4dc5aed63 --- /dev/null +++ b/packages/codemode/src/openapi/spec.ts @@ -0,0 +1,511 @@ +import { fromSchemaOpenApi3_0, fromSchemaOpenApi3_1 } from "effect/JsonSchema" +import type { JsonSchema } from "../tool.js" +import { isBlockedMember } from "../tool-runtime.js" +import type { + Body, + Document, + InputField, + OperationInput, + Parsed, + SecurityRequirement, + SecurityScheme, +} from "./types.js" + +export const methods = new Set(["get", "put", "post", "delete", "options", "head", "patch", "trace"]) +const parameterLocations = ["path", "query", "header"] as const +const ignoredHeaderParameters = new Set(["accept", "content-type", "authorization"]) + +export const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value) + +const asArray = (value: unknown): ReadonlyArray => (Array.isArray(value) ? 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`). +export const own = (record: Readonly>, key: string): T | undefined => + Object.hasOwn(record, key) ? record[key] : undefined + +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) + 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) + return target === undefined ? current : next(target, new Set([...seen, ref])) + } + return next(value, new Set()) +} + +const projectSchema = (document: Document, value: unknown): JsonSchema => { + if (!isRecord(value)) return {} + const normalized = nonEmptyString(document.openapi)?.startsWith("3.0") + ? fromSchemaOpenApi3_0(value) + : fromSchemaOpenApi3_1(value) + return Object.keys(normalized.definitions).length === 0 + ? normalized.schema + : { ...normalized.schema, $defs: normalized.definitions } +} + +export const componentDefinitions = (document: Document): 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)])) +} + +const withDefinitions = (schema: JsonSchema, definitions: Readonly>): JsonSchema => { + if (Object.keys(definitions).length === 0) return schema + const local = isRecord(schema.$defs) ? schema.$defs : {} + return { ...schema, $defs: { ...definitions, ...local } } +} + +const isJsonMediaType = (mediaType: string): boolean => { + const normalized = mediaType.split(";")[0]?.trim().toLowerCase() ?? "" + return normalized === "application/json" || normalized.endsWith("+json") +} + +const isBinaryMediaType = (document: Document, mediaType: string, value: unknown): boolean => { + const normalized = mediaType.split(";")[0]?.trim().toLowerCase() ?? "" + if (!isJsonMediaType(normalized) && !normalized.startsWith("text/")) return true + if (!isRecord(value)) return false + const schema = resolve(document, value.schema) + return isRecord(schema) && schema.format === "binary" +} + +const jsonContent = ( + content: Record, +): { readonly mediaType: string; readonly schema: unknown } | undefined => { + const entry = Object.entries(content).find(([mediaType]) => isJsonMediaType(mediaType)) + return entry !== undefined && isRecord(entry[1]) ? { mediaType: entry[0], schema: entry[1].schema } : undefined +} + +const isFlattenableObjectBody = ( + schema: unknown, + requestRequired: boolean, +): schema is Record & { readonly properties: Record } => + isRecord(schema) && + requestRequired && + schema.type === "object" && + isRecord(schema.properties) && + schema.additionalProperties === false && + schema.nullable !== true && + schema.allOf === undefined && + schema.anyOf === undefined && + schema.oneOf === undefined + +type PlannedField = Omit + +const operationParameters = ( + document: Document, + pathItem: Record, + operation: Record, +): Parsed> => { + // Operation-level parameters override path-level ones sharing (location, name). + const declared = new Map< + string, + { readonly name: string; readonly location: string; readonly parameter: Record } + >() + for (const raw of [...asArray(pathItem.parameters), ...asArray(operation.parameters)]) { + const resolved = resolve(document, raw) + if (!isRecord(resolved)) return { ok: false, reason: "parameter declaration is invalid or unresolved" } + const name = nonEmptyString(resolved.name) + const location = nonEmptyString(resolved.in) + if (name === undefined || location === undefined) + return { ok: false, reason: "parameter declaration is missing name or location" } + declared.set(`${location}:${name}`, { name, location, parameter: resolved }) + } + const unordered: Array = [] + for (const item of declared.values()) { + const name = item.name + const location = item.location + const resolved = item.parameter + if (location === "cookie") return { ok: false, reason: `cookie parameter '${name}' is not supported` } + if (location !== "path" && location !== "query" && location !== "header") { + return { ok: false, reason: `parameter '${name}' uses unsupported location '${location}'` } + } + if (location === "header" && ignoredHeaderParameters.has(name.toLowerCase())) continue + if (resolved.schema === undefined && resolved.content === undefined) { + return { ok: false, reason: `parameter '${name}' declares neither schema nor content` } + } + if (resolved.content !== undefined) + return { ok: false, reason: `parameter '${name}' uses unsupported content encoding` } + if (resolved.style !== undefined && nonEmptyString(resolved.style) === undefined) { + return { ok: false, reason: `parameter '${name}' has an invalid style` } + } + if (resolved.explode !== undefined && typeof resolved.explode !== "boolean") { + return { ok: false, reason: `parameter '${name}' has an invalid explode value` } + } + if (resolved.allowReserved !== undefined && typeof resolved.allowReserved !== "boolean") { + return { ok: false, reason: `parameter '${name}' has an invalid allowReserved value` } + } + if (resolved.allowReserved === true) + return { ok: false, reason: `parameter '${name}' uses unsupported allowReserved encoding` } + const declaredStyle = nonEmptyString(resolved.style) ?? (location === "query" ? "form" : "simple") + if (location === "query" && declaredStyle !== "form" && declaredStyle !== "deepObject") { + return { ok: false, reason: `query parameter '${name}' uses unsupported style '${declaredStyle}'` } + } + if (location !== "query" && declaredStyle !== "simple") { + return { ok: false, reason: `${location} parameter '${name}' uses unsupported style '${declaredStyle}'` } + } + const style = declaredStyle === "deepObject" ? "deepObject" : declaredStyle === "form" ? "form" : "simple" + const explode = typeof resolved.explode === "boolean" ? resolved.explode : style === "form" + if (style === "deepObject" && !explode) { + return { ok: false, reason: `query parameter '${name}' uses deepObject with explode=false` } + } + const base = projectSchema(document, resolved.schema) + const description = nonEmptyString(resolved.description) + unordered.push({ + name, + location, + required: resolved.required === true || location === "path", + style, + explode, + schema: { + ...base, + ...(base.description === undefined && description !== undefined ? { description } : {}), + }, + }) + } + return { + ok: true, + value: parameterLocations.flatMap((location) => unordered.filter((field) => field.location === location)), + } +} + +const operationBody = ( + document: Document, + operation: Record, +): Parsed<{ readonly fields: ReadonlyArray; readonly body: Body | undefined }> => { + const resolved = resolve(document, operation.requestBody) + if (!isRecord(resolved)) return { ok: true, value: { fields: [], body: undefined } } + const content = isRecord(resolved.content) ? resolved.content : {} + const selected = jsonContent(content) + if (selected === undefined) { + return { + ok: false, + reason: `request body has no JSON content (declared: ${Object.keys(content).join(", ") || "none"})`, + } + } + const schema = resolve(document, selected.schema) + const required = resolved.required === true + if (!isFlattenableObjectBody(schema, required)) { + return { + ok: true, + value: { + fields: [ + { + name: "body", + location: "body", + required, + schema: projectSchema(document, selected.schema), + style: undefined, + explode: undefined, + }, + ], + body: { required, mode: "value", mediaType: selected.mediaType }, + }, + } + } + const requiredProperties = new Set( + Array.isArray(schema.required) ? schema.required.filter((item): item is string => typeof item === "string") : [], + ) + return { + ok: true, + value: { + fields: Object.entries(schema.properties).map(([name, value]) => ({ + name, + location: "body" as const, + required: required && requiredProperties.has(name), + schema: projectSchema(document, value), + style: undefined, + explode: undefined, + })), + body: { required, mode: "object", mediaType: selected.mediaType }, + }, + } +} + +export const operationInput = ( + document: Document, + pathItem: Record, + operation: Record, +): Parsed => { + const parameters = operationParameters(document, pathItem, operation) + if (!parameters.ok) return parameters + const requestBody = operationBody(document, operation) + if (!requestBody.ok) return requestBody + const fields = [...parameters.value, ...requestBody.value.fields] + + const conflicts = new Set( + [...Map.groupBy(fields, (field) => field.name)] + .filter(([, matches]) => new Set(matches.map((field) => field.location)).size > 1) + .map(([name]) => name), + ) + const used = new Set() + return { + ok: true, + value: { + fields: fields.map((field) => { + const visibleName = isBlockedMember(field.name) ? `${field.name}_2` : field.name + const base = conflicts.has(field.name) ? `${field.location}_${visibleName}` : visibleName + const next = (index: number): string => { + const candidate = index === 1 ? base : `${base}_${index}` + return used.has(candidate) ? next(index + 1) : candidate + } + const inputName = next(1) + used.add(inputName) + return { ...field, inputName } + }), + body: requestBody.value.body, + }, + } +} + +export const inputSchema = ( + fields: ReadonlyArray, + definitions: Readonly>, +): JsonSchema => { + const required = fields.filter((field) => field.required).map((field) => field.inputName) + return withDefinitions( + { + type: "object", + properties: Object.fromEntries(fields.map((field) => [field.inputName, field.schema])), + ...(required.length === 0 ? {} : { required }), + }, + definitions, + ) +} + +const successfulResponses = ( + document: Document, + operation: Record, +): Parsed>> => { + if (!isRecord(operation.responses)) return { ok: true, value: [] } + const entries = Object.entries(operation.responses) + const selected = [ + ...entries.filter(([status]) => /^2\d\d$/.test(status)).sort(([a], [b]) => a.localeCompare(b)), + ...entries.filter(([status]) => status.toUpperCase() === "2XX"), + ] + const responses: Array> = [] + for (const [, value] of selected) { + const resolved = resolve(document, value) + if (!isRecord(resolved) || nonEmptyString(resolved.$ref) !== undefined) { + return { ok: false, reason: "successful response declaration is invalid or unresolved" } + } + responses.push(resolved) + } + return { ok: true, value: responses } +} + +export const operationOutput = ( + document: Document, + operation: Record, + definitions: Readonly>, +): Parsed => { + if (operation["x-websocket"] === true) return { ok: false, reason: "WebSocket operations are not supported" } + const responses = successfulResponses(document, operation) + if (!responses.ok) return responses + const streams = responses.value.some( + (response) => + isRecord(response.content) && + Object.keys(response.content).some( + (mediaType) => mediaType.split(";")[0]?.trim().toLowerCase() === "text/event-stream", + ), + ) + if (streams) return { ok: false, reason: "SSE operations are not supported" } + const binary = responses.value.some( + (response) => + isRecord(response.content) && + Object.entries(response.content).some(([mediaType, value]) => isBinaryMediaType(document, mediaType, value)), + ) + if (binary) return { ok: false, reason: "binary responses are not supported" } + + const outcomes: Array = [] + for (const response of responses.value) { + if (response.content !== undefined && !isRecord(response.content)) return { ok: true, value: undefined } + const content = isRecord(response.content) ? response.content : {} + if (Object.keys(content).length === 0) { + outcomes.push({ type: "null" }) + continue + } + for (const [mediaType, value] of Object.entries(content)) { + if (!isJsonMediaType(mediaType)) { + outcomes.push({ type: "string" }) + continue + } + if (!isRecord(value) || value.schema === undefined) return { ok: true, value: undefined } + outcomes.push(projectSchema(document, value.schema)) + } + } + if (outcomes.length === 0) return { ok: true, value: undefined } + return { + ok: true, + value: withDefinitions(outcomes.length === 1 ? (outcomes[0] ?? {}) : { anyOf: outcomes }, definitions), + } +} + +const sanitizeOperationSegment = (raw: string): string => { + const base = + raw + .replaceAll(/[^A-Za-z0-9_$]+/g, "_") + .replace(/^_+|_+$/g, "") + .replace(/^([0-9])/, "_$1") || "operation" + return isBlockedMember(base) ? `${base}_2` : base +} + +const fallbackOperationId = (method: string, path: string): string => + [ + method, + ...path + .split("/") + .filter((part) => part !== "") + .flatMap((part) => (part.startsWith("{") && part.endsWith("}") ? ["by", part.slice(1, -1)] : [part])) + .flatMap((part) => part.split(/[^A-Za-z0-9]+/).filter((word) => word !== "")), + ] + .map((word, index) => { + const lower = word.toLowerCase() + return index === 0 ? lower : `${lower.charAt(0).toUpperCase()}${lower.slice(1)}` + }) + .join("") + +export const operationPath = ( + method: string, + path: string, + operation: Record, + used: ReadonlySet, + namespaces: ReadonlySet, +): ReadonlyArray => { + const raw = nonEmptyString(operation.operationId) + const segments = (raw === undefined ? [fallbackOperationId(method, path)] : raw.split(".")).map( + sanitizeOperationSegment, + ) + if (isOperationPathAvailable(segments, used, namespaces)) return segments + const conflict = segments.slice(0, -1).findIndex((_, index) => used.has(segments.slice(0, index + 1).join("."))) + if (conflict >= 0 && conflict + 1 < segments.length) { + const collapsed = segments.flatMap((segment, index) => { + if (index === conflict) { + const next = segments[index + 1] ?? "" + return [`${segment}${next.charAt(0).toUpperCase()}${next.slice(1)}`] + } + return index === conflict + 1 ? [] : [segment] + }) + if (isOperationPathAvailable(collapsed, used, namespaces)) return collapsed + } + const fallback = segments.join("_") + const next = (index: number): string => { + const candidate = `${fallback}_${index}` + return isOperationPathAvailable([candidate], used, namespaces) ? candidate : next(index + 1) + } + return [next(2)] +} + +const isOperationPathAvailable = ( + segments: ReadonlyArray, + used: ReadonlySet, + namespaces: ReadonlySet, +): boolean => { + const key = segments.join(".") + if (used.has(key) || namespaces.has(key)) return false + return segments.slice(0, -1).every((_, index) => !used.has(segments.slice(0, index + 1).join("."))) +} + +export const specServerUrl = (source: Record): Parsed => { + const server = asArray(source.servers).find(isRecord) + const url = server === undefined ? undefined : nonEmptyString(server.url) + if (url === undefined) return { ok: false, reason: "spec declares no servers; pass baseUrl" } + if (/\{[^{}]+\}/.test(url)) { + return { ok: false, reason: `server URL '${url}' is not an absolute URL; pass baseUrl` } + } + return validateBaseUrl(url) +} + +export const validateBaseUrl = (value: string): Parsed => { + if (!/^https?:\/\//i.test(value)) return { ok: false, reason: `server URL '${value}' is not an absolute HTTP(S) URL` } + const url = URL.parse(value) + if (url === null || (url.protocol !== "http:" && url.protocol !== "https:")) { + return { ok: false, reason: `server URL '${value}' is not an absolute HTTP(S) URL` } + } + if (url.search !== "" || url.hash !== "") { + return { ok: false, reason: `server URL '${value}' contains an unsupported query string or fragment` } + } + return { ok: true, value } +} + +export const securityRequirements = (value: unknown): Parsed> => { + if (value === undefined) return { ok: true, value: [] } + if (!Array.isArray(value)) return { ok: false, reason: "security declaration is not an array" } + const requirements: Array = [] + for (const item of value) { + if (!isRecord(item)) return { ok: false, reason: "security requirement is not an object" } + const requirement = Object.create(null) as Record> + for (const [name, scopes] of Object.entries(item)) { + if (!Array.isArray(scopes)) return { ok: false, reason: "security requirement scopes are not string arrays" } + const parsed = scopes.filter((scope): scope is string => typeof scope === "string") + if (parsed.length !== scopes.length) { + return { ok: false, reason: "security requirement scopes are not string arrays" } + } + requirement[name] = parsed + } + requirements.push(requirement) + } + return { ok: true, value: requirements } +} + +export const operationSecurityRequirements = ( + value: unknown, + defaults: Parsed>, + schemes: Readonly>, +): Parsed> => { + const parsed = value === undefined ? defaults : securityRequirements(value) + if (!parsed.ok) return parsed + const supported = parsed.value.filter((requirement) => + Object.keys(requirement).every((name) => { + const scheme = own(schemes, name) + return scheme !== undefined && !(scheme.type === "apiKey" && scheme.in === "cookie") + }), + ) + if (parsed.value.length === 0 || supported.length > 0) return { ok: true, value: supported } + + const names = [...new Set(parsed.value.flatMap((requirement) => Object.keys(requirement)))] + const cookieScheme = names.find((name) => { + const definition = own(schemes, name) + return definition?.type === "apiKey" && definition.in === "cookie" + }) + return { + ok: false, + reason: + cookieScheme === undefined + ? `security requirement references missing or malformed scheme: ${names.join(", ")}` + : `cookie authentication '${cookieScheme}' is not supported`, + } +} + +export const securitySchemes = (document: Document): Readonly> => { + const components = isRecord(document.components) ? document.components : {} + const declared = isRecord(components.securitySchemes) ? components.securitySchemes : {} + return Object.fromEntries( + Object.entries(declared).flatMap(([name, value]) => { + const resolved = resolve(document, value) + if (!isRecord(resolved)) return [] + const type = nonEmptyString(resolved.type) + if (type === "apiKey") { + const carrier = nonEmptyString(resolved.in) + const parameter = nonEmptyString(resolved.name) + if (parameter === undefined || (carrier !== "header" && carrier !== "query" && carrier !== "cookie")) return [] + return [[name, { type, name: parameter, in: carrier }] as const] + } + if (type === "http") { + const scheme = nonEmptyString(resolved.scheme)?.toLowerCase() + return scheme === undefined ? [] : [[name, { type, scheme }] as const] + } + if (type === "oauth2" || type === "openIdConnect") return [[name, { type }] as const] + return [] + }), + ) +} diff --git a/packages/codemode/src/openapi/types.ts b/packages/codemode/src/openapi/types.ts new file mode 100644 index 00000000000..cab772e7014 --- /dev/null +++ b/packages/codemode/src/openapi/types.ts @@ -0,0 +1,112 @@ +import { Effect } from "effect" +import { HttpClient } from "effect/unstable/http" +import type { Definition, JsonSchema } from "../tool.js" + +/** A parsed OpenAPI 3.x document. YAML must be parsed by the host. */ +export type Document = Record + +/** The operation identity handed to auth resolution and errors. */ +export type Operation = { + readonly operationId: string | undefined + readonly method: string + readonly path: string + readonly summary: string | undefined + readonly description: string | undefined +} + +/** A resolved OpenAPI security scheme from `components.securitySchemes`. */ +export type SecurityScheme = + | { readonly type: "apiKey"; readonly name: string; readonly in: "header" | "query" | "cookie" } + | { readonly type: "http"; readonly scheme: string } + | { readonly type: "oauth2" } + | { 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. + */ +export type Credential = + | { readonly type: "bearer"; readonly token: string } + | { readonly type: "basic"; readonly username: string; readonly password: string } + | { readonly type: "apiKey"; readonly value: string } + | { 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. + */ +export type AuthResolver = (context: { + readonly name: string + readonly definition: SecurityScheme + readonly scopes: ReadonlyArray + readonly operation: Operation +}) => Effect.Effect + +export type Options = { + readonly spec: Document + /** Overrides all document, path, and operation `servers`. Required when no applicable absolute server URL exists. */ + readonly baseUrl?: string | undefined + /** Host credential resolution, keyed by security scheme name. */ + readonly auth?: { readonly resolve: AuthResolver } | undefined + /** Static headers on every request. Not model-visible; declared header params may override them, auth always wins. */ + readonly headers?: Readonly> | undefined +} + +/** An operation that could not be represented as a tool, and why. */ +export type Skipped = { + readonly method: string + readonly path: string + readonly reason: string +} + +export type Tools = { [name: string]: Definition | Tools } + +export type Result = { + /** Tool subtree; the host places it under a key in its `tools` tree. */ + readonly tools: Tools + readonly skipped: ReadonlyArray +} + +export type Parsed = { readonly ok: true; readonly value: T } | { readonly ok: false; readonly reason: string } + +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 + readonly schema: JsonSchema + readonly style: "simple" | "form" | "deepObject" | undefined + readonly explode: boolean | undefined +} + +export type Body = { readonly required: boolean; readonly mode: "object" | "value"; readonly mediaType: string } + +export type OperationInput = { + readonly fields: ReadonlyArray + readonly body: Body | undefined +} + +/** One OR alternative: scheme name -> required scopes. Empty object = unauthenticated is acceptable. */ +export type SecurityRequirement = Readonly>> + +export type Plan = { + readonly operation: Operation + readonly url: string + readonly fields: ReadonlyArray + readonly body: Body | undefined + readonly security: ReadonlyArray + readonly schemes: Readonly> + readonly auth: { readonly resolve: AuthResolver } | undefined + readonly headers: Readonly> +} + +export type AppliedAuth = { + readonly headers: Readonly> + readonly query: Readonly> +} diff --git a/packages/codemode/src/stdlib/collections.ts b/packages/codemode/src/stdlib/collections.ts new file mode 100644 index 00000000000..f4760ff7061 --- /dev/null +++ b/packages/codemode/src/stdlib/collections.ts @@ -0,0 +1,51 @@ +export const arrayMethods = new Set([ + "map", + "filter", + "find", + "findIndex", + "findLast", + "findLastIndex", + "some", + "every", + "includes", + "join", + "reduce", + "reduceRight", + "flatMap", + "forEach", + "sort", + "toSorted", + "slice", + "concat", + "indexOf", + "lastIndexOf", + "at", + "flat", + "reverse", + "toReversed", + "with", + "push", + "pop", + "shift", + "unshift", + "splice", + "fill", + "copyWithin", + "keys", + "values", + "entries", +]) + +export const mapMethods = new Set(["get", "set", "has", "delete", "clear", "forEach", "keys", "values", "entries"]) + +export const setMethods = new Set(["add", "has", "delete", "clear", "forEach", "keys", "values", "entries"]) + +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]) + return undefined +} +import { SandboxMap, SandboxSet, SandboxURLSearchParams } from "../values.js" diff --git a/packages/codemode/src/stdlib/console.ts b/packages/codemode/src/stdlib/console.ts new file mode 100644 index 00000000000..798563128e5 --- /dev/null +++ b/packages/codemode/src/stdlib/console.ts @@ -0,0 +1,4 @@ +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 diff --git a/packages/codemode/src/stdlib/date.ts b/packages/codemode/src/stdlib/date.ts new file mode 100644 index 00000000000..c492f58f94b --- /dev/null +++ b/packages/codemode/src/stdlib/date.ts @@ -0,0 +1,94 @@ +export const dateMethods = new Set([ + "getTime", + "valueOf", + "toISOString", + "toJSON", + "toString", + "getFullYear", + "getMonth", + "getDate", + "getDay", + "getHours", + "getMinutes", + "getSeconds", + "getMilliseconds", + "getUTCFullYear", + "getUTCMonth", + "getUTCDate", + "getUTCDay", + "getUTCHours", + "getUTCMinutes", + "getUTCSeconds", + "getUTCMilliseconds", + "getTimezoneOffset", +]) + +export const dateStatics = new Set(["now", "parse", "UTC"]) + +export const invokeDateStatic = (name: string, args: Array, node: AstNode): number => { + switch (name) { + case "now": + return Date.now() + case "parse": + return Date.parse(coerceToString(args[0])) + case "UTC": + return Date.UTC(...(args.map((arg) => coerceToNumber(arg)) as Parameters)) + default: + throw new InterpreterRuntimeError(`Date.${name} is not available in CodeMode.`, node) + } +} + +export const invokeDateMethod = (value: SandboxDate, name: string, node: AstNode): unknown => { + const hosted = new Date(value.time) + switch (name) { + case "getTime": + case "valueOf": + return value.time + case "toISOString": + if (!Number.isFinite(value.time)) throw new InterpreterRuntimeError("Invalid time value.", node) + return hosted.toISOString() + case "toJSON": + return Number.isFinite(value.time) ? hosted.toISOString() : null + case "toString": + return coerceToString(value) + case "getFullYear": + return hosted.getFullYear() + case "getMonth": + return hosted.getMonth() + case "getDate": + return hosted.getDate() + case "getDay": + return hosted.getDay() + case "getHours": + return hosted.getHours() + case "getMinutes": + return hosted.getMinutes() + case "getSeconds": + return hosted.getSeconds() + case "getMilliseconds": + return hosted.getMilliseconds() + case "getUTCFullYear": + return hosted.getUTCFullYear() + case "getUTCMonth": + return hosted.getUTCMonth() + case "getUTCDate": + return hosted.getUTCDate() + case "getUTCDay": + return hosted.getUTCDay() + case "getUTCHours": + return hosted.getUTCHours() + case "getUTCMinutes": + return hosted.getUTCMinutes() + case "getUTCSeconds": + return hosted.getUTCSeconds() + case "getUTCMilliseconds": + return hosted.getUTCMilliseconds() + case "getTimezoneOffset": + return hosted.getTimezoneOffset() + default: + throw new InterpreterRuntimeError(`Date method '${name}' is not available in CodeMode.`, node) + } +} +import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" +import { SandboxDate } 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 new file mode 100644 index 00000000000..8a479d2c8c7 --- /dev/null +++ b/packages/codemode/src/stdlib/json.ts @@ -0,0 +1,42 @@ +import { + type AstNode, + CodeModeFunction, + InterpreterRuntimeError, + supportedSyntaxMessage, +} from "../interpreter/model.js" +import { copyIn, copyOut } from "../tool-runtime.js" + +export const jsonStatics = new Set(["stringify", "parse"]) + +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) { + throw new InterpreterRuntimeError( + "JSON.stringify replacers are not supported in CodeMode.", + node, + "UnsupportedSyntax", + [supportedSyntaxMessage], + ) + } + 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) + } + case "parse": { + const text = args[0] + if (typeof text !== "string") throw new InterpreterRuntimeError("JSON.parse expects a string.", node) + try { + return copyIn(JSON.parse(text), "JSON.parse result") + } catch (error) { + throw new InterpreterRuntimeError( + `JSON.parse received invalid JSON: ${error instanceof Error ? error.message : String(error)}`, + node, + ).as("SyntaxError") + } + } + } + throw new InterpreterRuntimeError(`JSON.${name} is not available in CodeMode.`, node) +} diff --git a/packages/codemode/src/stdlib/math.ts b/packages/codemode/src/stdlib/math.ts new file mode 100644 index 00000000000..cc8dd0670e9 --- /dev/null +++ b/packages/codemode/src/stdlib/math.ts @@ -0,0 +1,65 @@ +export const mathConstants = new Set(["PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT2", "SQRT1_2"]) + +export const mathMethods = new Set([ + "max", + "min", + "abs", + "floor", + "ceil", + "round", + "trunc", + "sign", + "sqrt", + "cbrt", + "pow", + "hypot", + "log", + "log2", + "log10", + "exp", +]) + +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 (typeof arg !== "number") throw new InterpreterRuntimeError(`Math.${name} expects number arguments.`, node) + return arg + }) + const [a = Number.NaN, b = Number.NaN] = nums + switch (name) { + case "max": + return Math.max(...nums) + case "min": + return Math.min(...nums) + case "abs": + return Math.abs(a) + case "floor": + return Math.floor(a) + case "ceil": + return Math.ceil(a) + case "round": + return Math.round(a) + case "trunc": + return Math.trunc(a) + case "sign": + return Math.sign(a) + case "sqrt": + return Math.sqrt(a) + case "cbrt": + return Math.cbrt(a) + case "pow": + return Math.pow(a, b) + case "hypot": + return Math.hypot(...nums) + case "log": + return Math.log(a) + case "log2": + return Math.log2(a) + case "log10": + return Math.log10(a) + case "exp": + return Math.exp(a) + } + throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node) +} +import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" diff --git a/packages/codemode/src/stdlib/number.ts b/packages/codemode/src/stdlib/number.ts new file mode 100644 index 00000000000..79710e1ad22 --- /dev/null +++ b/packages/codemode/src/stdlib/number.ts @@ -0,0 +1,66 @@ +export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString"]) + +export const numberConstants = new Set(["MAX_SAFE_INTEGER", "MIN_SAFE_INTEGER", "MAX_VALUE", "MIN_VALUE", "EPSILON"]) + +export const numberStatics = new Set(["isInteger", "isFinite", "isNaN", "isSafeInteger", "parseInt", "parseFloat"]) + +export const invokeNumberMethod = (value: number, name: string, args: Array, node: AstNode): unknown => { + const optNum = (index: number): number | undefined => { + const arg = args[index] + if (arg === undefined) return undefined + if (typeof arg !== "number") throw new InterpreterRuntimeError(`Number.${name} expects a number argument.`, node) + return arg + } + let result: unknown + switch (name) { + case "toFixed": + result = value.toFixed(optNum(0)) + break + case "toExponential": + result = value.toExponential(optNum(0)) + break + case "toPrecision": { + const digits = optNum(0) + result = digits === undefined ? value.toString() : value.toPrecision(digits) + break + } + case "toString": { + const radix = optNum(0) + if (radix !== undefined && (radix < 2 || radix > 36)) { + throw new InterpreterRuntimeError("Number.toString radix must be between 2 and 36.", node) + } + result = value.toString(radix) + break + } + default: + throw new InterpreterRuntimeError(`Number method '${name}' is not available in CodeMode.`, node) + } + return boundedData(result, `Number.${name} result`) +} + +export const invokeNumberStatic = (name: string, args: Array, node: AstNode): unknown => { + const value = args[0] + switch (name) { + case "isInteger": + return Number.isInteger(value) + case "isFinite": + return Number.isFinite(value) + case "isNaN": + return Number.isNaN(value) + case "isSafeInteger": + return Number.isSafeInteger(value) + case "parseInt": { + const radix = args[1] + if (radix !== undefined && typeof radix !== "number") { + throw new InterpreterRuntimeError("Number.parseInt expects a numeric radix.", node) + } + return parseInt(coerceToString(value), radix) + } + case "parseFloat": + return parseFloat(coerceToString(value)) + default: + throw new InterpreterRuntimeError(`Number.${name} is not available in CodeMode.`, node) + } +} +import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" +import { boundedData, coerceToString } from "./value.js" diff --git a/packages/codemode/src/stdlib/object.ts b/packages/codemode/src/stdlib/object.ts new file mode 100644 index 00000000000..2beda0bf5ca --- /dev/null +++ b/packages/codemode/src/stdlib/object.ts @@ -0,0 +1,77 @@ +import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" +import { isBlockedMember } from "../tool-runtime.js" +import { isSandboxValue, SandboxMap, SandboxURLSearchParams } from "../values.js" +import { boundedData, coerceToString } from "./value.js" + +export const objectStatics = new Set(["keys", "values", "entries", "hasOwn", "assign", "fromEntries"]) + +export const invokeObjectMethod = (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) + } + return value 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 + } + 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 "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 "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)) { + throw new InterpreterRuntimeError("Object.assign expects data objects.", node) + } + for (const [key, item] of Object.entries(value)) guardedSet(out, key, item) + } + return out + } + case "fromEntries": { + if (args[0] instanceof SandboxMap) { + const out: Record = Object.create(null) + for (const [key, item] of args[0].map.entries()) guardedSet(out, coerceToString(key), item) + return out + } + if (args[0] instanceof SandboxURLSearchParams) { + 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") + if (!Array.isArray(pairs)) { + 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]) + } + return out + } + } + throw new InterpreterRuntimeError(`Object.${name} is not available in CodeMode.`, node) +} diff --git a/packages/codemode/src/stdlib/promise.ts b/packages/codemode/src/stdlib/promise.ts new file mode 100644 index 00000000000..d0b442ccf74 --- /dev/null +++ b/packages/codemode/src/stdlib/promise.ts @@ -0,0 +1,6 @@ +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 diff --git a/packages/codemode/src/stdlib/regexp.ts b/packages/codemode/src/stdlib/regexp.ts new file mode 100644 index 00000000000..eac123bc16c --- /dev/null +++ b/packages/codemode/src/stdlib/regexp.ts @@ -0,0 +1,74 @@ +export const regexpMethods = new Set(["test", "exec", "toString"]) + +export const regexpProperties = new Set([ + "source", + "flags", + "lastIndex", + "global", + "ignoreCase", + "multiline", + "sticky", + "unicode", + "dotAll", +]) + +export const regexFailureReason = (error: unknown): string => + (error instanceof Error ? error.message : String(error)).replace(/^Invalid regular expression:\s*/i, "") + +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 + if (typeof arg === "string") { + try { + return new RegExp(arg, extraFlags) + } catch (error) { + throw new InterpreterRuntimeError( + `String.${method} received the string ${JSON.stringify(arg)}, which is not a valid regular expression pattern (${regexFailureReason(error)}). ${escapeRegexHint}`, + node, + ).as("SyntaxError") + } + } + throw new InterpreterRuntimeError( + `String.${method} expects a regular expression (a /pattern/flags literal or new RegExp(...)) or a string pattern, not ${arg === null ? "null" : typeof arg}.`, + node, + ) +} + +export const matchToValue = (match: RegExpMatchArray): Array => { + const result: Array = Array.from(match, (group) => group) + if (match.index !== undefined) (result as Record & Array).index = match.index + if (match.groups) { + const groups: SafeObject = Object.create(null) as SafeObject + for (const [key, group] of Object.entries(match.groups)) { + if (!isBlockedMember(key)) groups[key] = group + } + ;(result as Record & Array).groups = groups + } + return result +} + +export const invokeRegExpMethod = ( + value: SandboxRegExp, + name: string, + args: Array, + node: AstNode, +): unknown => { + switch (name) { + case "test": + return value.regex.test(coerceToString(args[0])) + case "exec": { + const matched = value.regex.exec(coerceToString(args[0])) + return matched === null ? null : matchToValue(matched) + } + case "toString": + return coerceToString(value) + default: + throw new InterpreterRuntimeError(`RegExp method '${name}' is not available in CodeMode.`, node) + } +} +import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" +import { isBlockedMember, type SafeObject } from "../tool-runtime.js" +import { SandboxRegExp } from "../values.js" +import { coerceToString } from "./value.js" diff --git a/packages/codemode/src/stdlib/string.ts b/packages/codemode/src/stdlib/string.ts new file mode 100644 index 00000000000..3ac4372e363 --- /dev/null +++ b/packages/codemode/src/stdlib/string.ts @@ -0,0 +1,52 @@ +export const stringMethods = new Set([ + "toLowerCase", + "toUpperCase", + "trim", + "trimStart", + "trimEnd", + "trimLeft", + "trimRight", + "split", + "slice", + "substring", + "substr", + "includes", + "startsWith", + "endsWith", + "indexOf", + "lastIndexOf", + "replace", + "replaceAll", + "repeat", + "padStart", + "padEnd", + "charAt", + "charCodeAt", + "codePointAt", + "at", + "concat", + "toString", + "match", + "matchAll", + "search", + "localeCompare", + "normalize", +]) + +export const stringStatics = new Set(["fromCharCode", "fromCodePoint"]) + +export const invokeStringStatic = (name: string, args: Array, node: AstNode): unknown => { + const codes = args.map((arg) => { + if (typeof arg !== "number") throw new InterpreterRuntimeError(`String.${name} expects number arguments.`, node) + return arg + }) + switch (name) { + case "fromCharCode": + return String.fromCharCode(...codes) + case "fromCodePoint": + return String.fromCodePoint(...codes) + default: + throw new InterpreterRuntimeError(`String.${name} is not available in CodeMode.`, node) + } +} +import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" diff --git a/packages/codemode/src/stdlib/url.ts b/packages/codemode/src/stdlib/url.ts new file mode 100644 index 00000000000..583d776d22a --- /dev/null +++ b/packages/codemode/src/stdlib/url.ts @@ -0,0 +1,90 @@ +export const urlProperties = new Set([ + "href", + "origin", + "protocol", + "username", + "password", + "host", + "hostname", + "port", + "pathname", + "search", + "hash", +]) + +export const urlWritableProperties = new Set([ + "href", + "protocol", + "username", + "password", + "host", + "hostname", + "port", + "pathname", + "search", + "hash", +]) + +export const urlMethods = new Set(["toString", "toJSON"]) +export const urlStatics = new Set(["canParse", "parse"]) +export const urlSearchParamsMethods = new Set([ + "append", + "delete", + "get", + "getAll", + "has", + "set", + "sort", + "forEach", + "keys", + "values", + "entries", + "toString", +]) + +export const uriArgument = (value: unknown, label: string): string => coerceToString(boundedData(value, label)) + +export const invokeUriFunction = (ref: UriFunction, args: Array, node: AstNode): string => { + const value = uriArgument(args[0], `${ref.name} input`) + try { + switch (ref.name) { + case "encodeURI": + return encodeURI(value) + case "encodeURIComponent": + return encodeURIComponent(value) + case "decodeURI": + return decodeURI(value) + case "decodeURIComponent": + return decodeURIComponent(value) + } + } catch (error) { + throw new InterpreterRuntimeError( + `${ref.name} received malformed URI data: ${error instanceof Error ? error.message : String(error)}`, + node, + ).as("URIError") + } +} + +export const urlArgument = (value: unknown, label: string): string => + value instanceof SandboxURL ? 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) + if (args.length === 0) throw new InterpreterRuntimeError(`URL.${name} requires a URL argument.`, node).as("TypeError") + const input = urlArgument(args[0], `URL.${name} input`) + 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) + } catch { + return name === "canParse" ? false : null + } +} + +export const invokeURLMethod = (value: SandboxURL, 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 { boundedData, coerceToString } from "./value.js" diff --git a/packages/codemode/src/stdlib/value.ts b/packages/codemode/src/stdlib/value.ts new file mode 100644 index 00000000000..ab40dc07ddb --- /dev/null +++ b/packages/codemode/src/stdlib/value.ts @@ -0,0 +1,86 @@ +export const errorConstructors = new Set([ + "Error", + "TypeError", + "RangeError", + "SyntaxError", + "ReferenceError", + "EvalError", + "URIError", +]) + +export const valueConstructors = new Set(["Date", "RegExp", "Map", "Set", "URL", "URLSearchParams"]) + +export const compoundOperators = new Set(["+=", "-=", "*=", "/=", "%=", "**=", "&=", "|=", "^=", "<<=", ">>=", ">>>="]) + +const ErrorBrand: unique symbol = Symbol("codemode.error") + +export const createErrorValue = (name: string, message: string): SafeObject => { + const value = Object.assign(Object.create(null) as SafeObject, { name, message }) + Object.defineProperty(value, ErrorBrand, { value: name }) + return value +} + +export const errorBrandName = (value: unknown): string | undefined => + value !== null && typeof value === "object" + ? ((value as Record)[ErrorBrand] as string | undefined) + : undefined + +export const boundedData = (value: unknown, label: string): unknown => copyIn(value, label, true) + +export const coerceToString = (value: unknown): string => { + if (value === null) return "null" + if (value === undefined) return "undefined" + if (value instanceof SandboxDate) + 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 (typeof value === "object") { + return Array.isArray(value) + ? value.map((item) => (item === null || item === undefined ? "" : coerceToString(item))).join(",") + : "[object Object]" + } + return String(value) +} + +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) +} + +export const invokeCoercion = (ref: CoercionFunction, args: Array, node: AstNode): unknown => { + const raw = args[0] + if (isSandboxValue(raw)) { + if (ref.name === "Boolean") return true + if (ref.name === "Number") return coerceToNumber(raw) + if (ref.name === "String") return coerceToString(raw) + if (ref.name === "parseInt") return parseInt(coerceToString(raw)) + return parseFloat(coerceToString(raw)) + } + const value = boundedData(args[0], `${ref.name} input`) + if (ref.name === "Number") return coerceToNumber(value) + if (ref.name === "Boolean") return Boolean(value) + if (ref.name === "parseInt") { + const radix = args[1] + if (radix !== undefined && typeof radix !== "number") { + throw new InterpreterRuntimeError("parseInt expects a numeric radix.", node) + } + return parseInt(coerceToString(value), radix) + } + if (ref.name === "parseFloat") return parseFloat(coerceToString(value)) + return coerceToString(value) +} +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, +} from "../values.js" diff --git a/packages/codemode/src/tool-error.ts b/packages/codemode/src/tool-error.ts new file mode 100644 index 00000000000..16460a019ae --- /dev/null +++ b/packages/codemode/src/tool-error.ts @@ -0,0 +1,11 @@ +import { Schema } from "effect" + +/** Safe operational refusal from a standard tool pack, reported as `ToolFailure`. */ +export class ToolError extends Schema.TaggedErrorClass()("ToolError", { + message: Schema.String, + cause: Schema.optionalKey(Schema.Defect()), +}) {} + +/** Creates a tool refusal whose message is safe to include in an execution diagnostic. */ +export const toolError = (message: string, cause?: unknown): ToolError => + new ToolError({ message, ...(cause === undefined ? {} : { cause }) }) diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts new file mode 100644 index 00000000000..f4ccc61d4c4 --- /dev/null +++ b/packages/codemode/src/tool-runtime.ts @@ -0,0 +1,806 @@ +import { Cause, Effect, Schema } from "effect" +import { ToolError, toolError } from "./tool-error.js" +import { + decodeInput as decodeToolInput, + decodeOutput as decodeToolOutput, + identifierSegment, + inputProperties, + inputTypeScript, + outputTypeScript, +} from "./tool-schema.js" +import { isDefinition as isToolDefinition, type Definition } from "./tool.js" +import { + SandboxDate, + SandboxMap, + SandboxPromise, + SandboxRegExp, + SandboxSet, + SandboxURL, + SandboxURLSearchParams, +} from "./values.js" + +const estimateTokens = (input: string) => Math.max(0, Math.round(input.length / 4)) + +export type HostTool = (...args: Array) => Effect.Effect + +export type HostTools = { + [name: string]: HostTool | Definition | HostTools +} + +export type Services = ServicesOf + +type ServicesOf> = Depth["length"] extends 8 + ? never + : Tools extends (...args: Array) => 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 + +/** 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 + readonly signature: string +} + +export type SafeObject = Record + +const reservedNamespace = "$codemode" +const defaultCatalogBudget = 2_000 +const defaultSearchLimit = 10 +const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0)) +const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) +const SearchInput = Schema.Struct({ + query: Schema.optionalKey(Schema.String), + namespace: Schema.optionalKey(Schema.String), + limit: Schema.optionalKey(PositiveInt), + offset: Schema.optionalKey(NonNegativeInt), +}) +const SearchItem = Schema.Struct({ + path: Schema.String, + description: Schema.String, + signature: Schema.String, +}) +const SearchOutput = Schema.Struct({ + items: Schema.Array(SearchItem), + remaining: NonNegativeInt, + next: Schema.NullOr(Schema.Struct({ offset: NonNegativeInt })), +}) +const toolExpression = (path: string) => + "tools" + + path + .split(".") + .map((segment) => (identifierSegment.test(segment) ? `.${segment}` : `[${JSON.stringify(segment)}]`)) + .join("") + +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 { + constructor( + readonly kind: + | "UnknownTool" + | "InvalidToolInput" + | "InvalidToolOutput" + | "InvalidDataValue" + | "ToolCallLimitExceeded", + message: string, + readonly suggestions: ReadonlyArray = [], + ) { + super(message) + this.name = "ToolRuntimeError" + } +} + +const isDefinition = (value: HostTool | Definition | HostTools): value is Definition => + isToolDefinition(value) + +const runHost = (effect: Effect.Effect): Effect.Effect => + effect.pipe( + Effect.catchCause((cause) => { + if (Cause.hasInterruptsOnly(cause)) return Effect.interrupt + const error = Cause.squash(cause) + return Effect.fail(error instanceof ToolError ? error : toolError("Tool execution failed", error)) + }), + ) + +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) + +const copyBounded = ( + value: unknown, + label: string, + depth: number, + seen: Set, + preserveSandboxValues: boolean, +): unknown => { + if (depth > MAX_VALUE_DEPTH) { + throw new ToolRuntimeError("InvalidDataValue", `${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`) + } + if ( + value === null || + 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 + } + + if (typeof value !== "object") { + 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) { + 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 ( + value instanceof SandboxDate || + value instanceof SandboxRegExp || + value instanceof SandboxMap || + value instanceof SandboxSet || + value instanceof SandboxURL || + value instanceof SandboxURLSearchParams + ) { + 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 Map) { + const wrapped = new SandboxMap() + 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() + 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)) + } + + // 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) { + 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 URL) return value.href + if ( + value instanceof SandboxRegExp || + value instanceof SandboxMap || + value instanceof SandboxSet || + value instanceof SandboxURLSearchParams || + value instanceof RegExp || + value instanceof Map || + value instanceof Set || + value instanceof URLSearchParams + ) { + return Object.create(null) as SafeObject + } + + if (seen.has(value)) { + throw new ToolRuntimeError("InvalidDataValue", `${label} contains a circular value.`) + } + + seen.add(value) + + if (Array.isArray(value)) { + const copied = value.map((item) => copyBounded(item, label, depth + 1, seen, preserveSandboxValues)) + seen.delete(value) + return copied + } + + const prototype = Object.getPrototypeOf(value) + if (prototype !== Object.prototype && prototype !== null) { + throw new ToolRuntimeError("InvalidDataValue", `${label} must contain plain objects only.`) + } + + const copied: SafeObject = Object.create(null) as SafeObject + for (const [key, item] of Object.entries(value)) { + if (isBlockedMember(key)) { + throw new ToolRuntimeError("InvalidDataValue", `${label} contains blocked property '${key}'.`) + } + copied[key] = copyBounded(item, label, depth + 1, seen, preserveSandboxValues) + } + 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. + if (typeof value === "number" && !Number.isFinite(value)) { + return null + } + if (Array.isArray(value)) { + return value.map((item) => copyOut(item, undefinedAsNull)) + } + + if (value !== null && typeof value === "object" && !(value instanceof ToolReference)) { + return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, copyOut(item, undefinedAsNull)])) + } + + 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 +} + +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 }) => ({ + 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 + readonly searchIndex: ReadonlyArray +} + +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") + .toLowerCase() + .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)) + if (term.endsWith("s") && term.length > 2) forms.push(term.slice(0, -1)) + return forms +} + +const makeSearchTool = (searchIndex: ReadonlyArray): Definition => ({ + _tag: "CodeModeTool", + description: "Search available Code Mode tools", + input: SearchInput, + output: SearchOutput, + run: (input) => + Effect.sync(() => { + const request = input as typeof SearchInput.Type + const query = request.query ?? "" + const offset = request.offset ?? 0 + const scoped = + 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 = + pathQuery === "" + ? undefined + : scoped.find( + (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] + : scoped + .map((entry) => { + const path = entry.description.path.toLowerCase() + const description = entry.description.description.toLowerCase() + const score = terms.reduce( + (total, forms) => + total + + (forms.some((form) => path === form || path.endsWith(`.${form}`)) ? 20 : 0) + + (forms.some((form) => path.includes(form)) ? 8 : 0) + + (forms.some((form) => description.includes(form)) ? 4 : 0) + + (forms.some((form) => entry.searchText.includes(form)) ? 2 : 0), + 0, + ) + return { entry, score } + }) + .filter(({ score }) => terms.length === 0 || score > 0) + .sort( + (left, right) => + right.score - left.score || left.entry.description.path.localeCompare(right.entry.description.path), + ) + .map(({ entry }) => entry) + const items = ranked.slice(offset, offset + (request.limit ?? defaultSearchLimit)).map(({ description }) => ({ + ...description, + path: toolExpression(description.path), + })) + const remaining = Math.max(0, ranked.length - offset - items.length) + return { + items, + remaining, + next: remaining > 0 ? { offset: offset + items.length } : null, + } + }), +}) + +const searchDescription = describeDefinition(`${reservedNamespace}.search`, makeSearchTool([])) + +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}` +} + +const toSearchEntry = (path: string, definition: Definition, description: ToolDescription): SearchEntry => ({ + description, + namespace: path.split(".", 1)[0]!, + searchText: [ + path, + definition.description, + ...inputProperties(definition).flatMap(({ name, description: property }) => + property === undefined ? [name] : [name, property], + ), + ] + .join("\n") + .toLowerCase(), +}) + +/** The runtime search index over every described tool. Search is always registered. */ +export const searchIndex = (tools: HostTools): 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 => { + if (!Number.isSafeInteger(catalogBudget) || catalogBudget < 0) { + throw new RangeError("discovery.catalogBudget must be a non-negative safe integer") + } + const visible = visibleDefinitions(tools) + const described = visible.map(({ description }) => description) + + const namespaces = new Map>() + for (const tool of described) { + const [namespace = tool.path] = tool.path.split(".") + const group = namespaces.get(namespace) ?? [] + group.push(tool) + namespaces.set(namespace, group) + } + 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(), + queue: [...group].sort( + (left, right) => + estimateTokens(catalogLine(left)) - estimateTokens(catalogLine(right)) || left.path.localeCompare(right.path), + ), + })) + let used = 0 + let active = selections.filter((selection) => selection.queue.length > 0) + while (active.length > 0) { + const stillActive: typeof active = [] + for (const selection of active) { + const tool = selection.queue[0]! + const cost = estimateTokens(catalogLine(tool)) + if (used + cost > catalogBudget) continue + selection.queue.shift() + selection.picked.add(tool) + used += cost + if (selection.queue.length > 0) stillActive.push(selection) + } + active = stillActive + } + const shown = new Map>( + selections.map(({ namespace, picked }) => [namespace, picked]), + ) + const totalShown = selections.reduce((total, { picked }) => total + picked.size, 0) + const complete = totalShown === described.length + + 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.", + ...(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 + ? [] + : [ + "", + "## Workflow", + "", + ...(complete + ? [ + "1. Pick a tool from the list under `## Available tools` - each line is the exact call signature; use it as-is rather than guessing segments.", + "2. Call it using the exact signature shown: `const result = await tools..(input)`; bracket notation and quotes are part of the path.", + "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: "" })`.', + "2. In the next execution, copy a returned path exactly, call it, and return only the needed fields.", + ]), + ] + + const rules = empty + ? [] + : [ + "", + "## 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.", + "- 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.', + "- `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: "" })`.', + "- If search returns `next`, repeat the same search with `offset: next.offset`.", + ]), + ] + + const language = [ + "", + "## 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.", + "Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.", + ] + + const toolSection: Array = [""] + if (empty) { + toolSection.push("## Available tools", "", "No tools are currently available.") + } else { + 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)`, + "", + ) + 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 + : picked.size === 0 + ? `${count}, none shown` + : `${count}, ${picked.size} shown` + toolSection.push(`- ${namespace} (${label})`) + for (const tool of group) if (picked.has(tool)) toolSection.push(catalogLine(tool)) + } + if (!complete) { + toolSection.push("", "Search returns complete callable signatures:", `- ${searchDescription.signature}`) + } + } + + const lines = [...intro, ...workflow, ...rules, ...language, ...toolSection] + return { + catalog: described, + instructions: lines.join("\n"), + searchIndex: visible.map(({ path, definition, description }) => toSearchEntry(path, definition, description)), + } +} + +/** + * 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 + } + if (typeof value === "function" || isDefinition(value)) return [] + return Object.keys(value) +} + +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 + } + + if (typeof value !== "function" && !isDefinition(value)) { + throw new ToolRuntimeError("UnknownTool", `Tool '${path.join(".")}' is not callable.`) + } + + return value +} + +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 keys: (path: ReadonlyArray) => ReadonlyArray +} + +export const make = ( + tools: HostTools, + /** Undefined means unlimited tool calls. */ + maxToolCalls: number | undefined, + searchIndex: ReadonlyArray, + hooks?: ToolCallHooks, +): ToolRuntime => { + const calls: Array = [] + const callableTools = { + ...tools, + [reservedNamespace]: { search: 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. + const observeEnd = (effect: Effect.Effect, call: ToolCallStarted): Effect.Effect => { + const onEnd = hooks?.onToolCallEnd + if (onEnd === undefined) return effect + const startedAt = Date.now() + return effect.pipe( + Effect.tap(() => onEnd({ ...call, durationMs: Date.now() - startedAt, outcome: "success" })), + Effect.tapError((error) => { + const message = + error instanceof ToolError || error instanceof ToolRuntimeError ? error.message : "Tool execution failed" + return onEnd({ + ...call, + durationMs: Date.now() - startedAt, + outcome: "failure", + message, + }) + }), + ) + } + + const decodeOutput = (value: unknown, name: string) => + Effect.try({ + try: () => copyIn(value, `Result from tool '${name}'`), + catch: () => new ToolRuntimeError("InvalidToolOutput", `Invalid output from tool '${name}'.`), + }) + + const recordCall = (call: ToolCall): void => { + if (maxToolCalls !== undefined && calls.length >= maxToolCalls) { + throw new ToolRuntimeError("ToolCallLimitExceeded", `Execution exceeded its tool-call limit of ${maxToolCalls}.`) + } + calls.push(call) + } + + return { + root: new ToolReference([]), + calls, + keys: (path) => namespaceKeys(callableTools, path), + 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, + ) + }), + } +} + +export * as ToolRuntime from "./tool-runtime.js" diff --git a/packages/codemode/src/tool-schema.ts b/packages/codemode/src/tool-schema.ts new file mode 100644 index 00000000000..16213fa8eeb --- /dev/null +++ b/packages/codemode/src/tool-schema.ts @@ -0,0 +1,301 @@ +import { JsonPointer, Schema } from "effect" +import type { Definition, JsonSchema, SchemaType } from "./tool.js" + +const isEffectSchema = (schema: SchemaType): schema is Schema.Decoder & Schema.Top => Schema.isSchema(schema) + +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) => + schema.type === "string" && + Array.isArray(schema.enum) && + schema.enum.length === 1 && + (schema.enum[0] === "NaN" || schema.enum[0] === "Infinity" || schema.enum[0] === "-Infinity") + +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" + 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 +} + +const hasUnresolvedRef = ( + schema: JsonSchema, + definitions: Readonly>, + seen: ReadonlySet = new Set(), + visited: ReadonlySet = new Set(), +): boolean => { + if (visited.has(schema)) return false + const nextVisited = new Set([...visited, schema]) + if (schema.$ref !== undefined) { + const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1] + const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment) + if (name === undefined || definitions[name] === undefined || seen.has(name)) return true + if (hasUnresolvedRef(definitions[name], definitions, new Set([...seen, name]), nextVisited)) return true + } + return [ + ...(schema.anyOf ?? []), + ...(schema.oneOf ?? []), + ...(schema.allOf ?? []), + ...Object.values(schema.properties ?? {}), + ...(schema.items === undefined ? [] : [schema.items]), + ...(typeof schema.additionalProperties === "object" ? [schema.additionalProperties] : []), + ].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") + if (schema.default !== undefined) { + try { + const rendered = JSON.stringify(schema.default) + if (rendered !== undefined) tags.push(`@default ${rendered}`) + } catch { + // unserializable default: skip rather than emit a broken tag + } + } + if (typeof schema.format === "string") tags.push(`@format ${schema.format}`) + if (typeof schema.minItems === "number") tags.push(`@minItems ${schema.minItems}`) + if (typeof schema.maxItems === "number") tags.push(`@maxItems ${schema.maxItems}`) + 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. + */ +const jsdoc = (description: string | undefined, tags: ReadonlyArray, pad: string): string => { + const lines = [...(description === undefined ? [] : description.split("\n")), ...tags].map((line) => + line.replaceAll("*/", "* /").replace(/\s+$/, ""), + ) + while (lines.length > 0 && lines[0]!.trim() === "") lines.shift() + while (lines.length > 0 && lines[lines.length - 1]!.trim() === "") lines.pop() + if (lines.length === 0) return "" + if (lines.length === 1) return `${pad}/** ${lines[0]} */\n` + const body = lines.map((line) => `${pad} *${line === "" ? "" : ` ${line}`}`).join("\n") + return `${pad}/**\n${body}\n${pad} */\n` +} + +const renderSchema = ( + schema: JsonSchema, + ctx: RenderContext, + depth = 0, + seen: ReadonlySet = new Set(), +): string => { + if (depth > MAX_RENDER_DEPTH) return "unknown" + const nested = + schema.definitions === undefined && schema.$defs === undefined + ? ctx + : { ...ctx, definitions: { ...ctx.definitions, ...(schema.definitions ?? {}), ...(schema.$defs ?? {}) } } + if (schema.$ref) { + const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1] + const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment) + if (!name || !nested.definitions[name] || seen.has(name)) return "unknown" + return intersection([ + renderSchema(nested.definitions[name], nested, depth, new Set([...seen, name])), + renderSchema({ ...schema, $ref: undefined }, nested, depth + 1, seen), + ]) + } + if (schema.const !== undefined) return renderLiteral(schema.const) + 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" && + alternatives[0].properties === undefined && + alternatives[1]?.type === "array" && + alternatives[1].items === undefined + ) { + return "{}" + } + const members = alternatives.map((item) => renderSchema(item, nested, depth + 1, seen)) + if (members.some((member) => member === "unknown")) return "unknown" + return intersection([ + members.join(" | "), + renderSchema({ ...schema, anyOf: undefined, oneOf: undefined }, nested, depth + 1, seen), + ]) + } + if (schema.allOf) { + const members = schema.allOf.map((item) => renderSchema(item, nested, depth + 1, seen)) + if (schema.allOf.some((item) => hasUnresolvedRef(item, nested.definitions))) return "unknown" + return intersection([renderSchema({ ...schema, allOf: undefined }, nested, depth + 1, seen), ...members]) + } + if (Array.isArray(schema.type)) { + return schema.type.map((item) => renderSchema({ ...schema, type: item }, nested, depth + 1, seen)).join(" | ") + } + if (schema.type === "string") return "string" + if (schema.type === "number" || schema.type === "integer") return "number" + if (schema.type === "boolean") return "boolean" + if (schema.type === "null") return "null" + if (schema.type === "array") return `Array<${renderSchema(schema.items ?? {}, nested, depth + 1, seen)}>` + if (schema.type === "object" || schema.properties) { + const required = new Set(schema.required ?? []) + const properties = Object.entries(schema.properties ?? {}) + const additional = schema.additionalProperties + const indexType = + additional && typeof additional === "object" ? renderSchema(additional, nested, depth + 1, seen) : undefined + const field = ([name, value]: readonly [string, JsonSchema]) => + `${renderKey(name)}${required.has(name) ? "" : "?"}: ${renderSchema(value, nested, depth + 1, seen)}` + + if (!ctx.pretty) { + const fields = properties.map(field) + if (indexType !== undefined) fields.push(`[key: string]: ${indexType}`) + 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( + (entry) => `${jsdoc(entry[1].description, docTags(entry[1]), pad)}${pad}${field(entry)},`, + ) + if (indexType !== undefined) lines.push(`${pad}[key: string]: ${indexType},`) + return `{\n${lines.join("\n")}\n${" ".repeat(depth)}}` + } + return "unknown" +} + +export const toTypeScript = (schema: Schema.Top, decoded = false, pretty = false): string => { + try { + const visible = decoded ? Schema.toType(schema) : schema + const document = Schema.toJsonSchemaDocument(visible) as { + readonly schema: JsonSchema + readonly definitions?: Readonly> + } + return renderSchema(document.schema, { definitions: document.definitions ?? {}, pretty }) + } catch { + return "unknown" + } +} + +/** 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 }) + } catch { + return "unknown" + } +} + +/** 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) + ? (Schema.toJsonSchemaDocument(definition.input) as { + readonly schema: JsonSchema + readonly definitions?: Readonly> + }) + : { + schema: definition.input, + definitions: { ...(definition.input.definitions ?? {}), ...(definition.input.$defs ?? {}) }, + } + const definitions = document.definitions ?? {} + let schema = document.schema + if (schema.$ref !== undefined) { + const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1] + const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment) + const resolved = name === undefined ? undefined : definitions[name] + if (resolved === undefined) return [] + schema = resolved + } + const required = new Set(schema.required ?? []) + return Object.entries(schema.properties ?? {}).map(([name, value]) => ({ + name, + description: typeof value.description === "string" ? value.description : undefined, + required: required.has(name), + })) + } catch { + return [] + } +} + +/** + * The model-visible TypeScript type of a tool's input. `pretty` renders an indented + * multiline block with schema descriptions and constraints as JSDoc comments on the + * fields; the default stays the compact single-line form. + */ +export const inputTypeScript = (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" + : isEffectSchema(definition.output) + ? 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) + : value diff --git a/packages/codemode/src/tool.ts b/packages/codemode/src/tool.ts new file mode 100644 index 00000000000..6c6863f99dc --- /dev/null +++ b/packages/codemode/src/tool.ts @@ -0,0 +1,96 @@ +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. + */ +export type JsonSchema = { + readonly type?: string | ReadonlyArray + readonly enum?: ReadonlyArray + readonly const?: unknown + readonly anyOf?: ReadonlyArray + readonly oneOf?: ReadonlyArray + readonly allOf?: ReadonlyArray + readonly properties?: Readonly> + readonly required?: ReadonlyArray + readonly items?: JsonSchema + readonly additionalProperties?: boolean | JsonSchema + readonly description?: string + readonly default?: unknown + readonly format?: string + readonly deprecated?: boolean + readonly minItems?: number + readonly maxItems?: number + readonly $ref?: string + readonly $defs?: Readonly> + readonly definitions?: Readonly> +} + +/** 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. */ +export type Definition = { + readonly _tag: "CodeModeTool" + readonly description: string + readonly input: SchemaType + readonly output: SchemaType | undefined + 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. */ +export type Options = { + readonly description: string + readonly input: I + readonly output?: O + readonly run: (input: InputType) => Effect.Effect, unknown, R> +} + +export const isDefinition = (value: unknown): value is Definition => + typeof value === "object" && value !== null && "_tag" in value && 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), + * }) + * ``` + */ +export const make = ( + options: Options, +): Definition => ({ + _tag: "CodeModeTool", + description: options.description, + input: options.input, + output: options.output, + run: (input) => options.run(input as InputType), +}) diff --git a/packages/codemode/src/values.ts b/packages/codemode/src/values.ts new file mode 100644 index 00000000000..4ca305d815e --- /dev/null +++ b/packages/codemode/src/values.ts @@ -0,0 +1,49 @@ +import type { Effect, Fiber } from "effect" + +export class SandboxPromise { + interrupted = false + constructor( + readonly fiber: Fiber.Fiber | undefined, + readonly immediate?: Effect.Effect, + ) {} +} + +export class SandboxDate { + constructor(readonly time: number) {} +} + +export class SandboxRegExp { + readonly regex: RegExp + constructor(pattern: string, flags: string) { + this.regex = new RegExp(pattern, flags) + } +} + +export class SandboxMap { + readonly map = new Map() +} + +export class SandboxSet { + readonly set = new Set() +} + +export class SandboxURLSearchParams { + constructor(readonly params: URLSearchParams) {} +} + +export class SandboxURL { + readonly searchParams: SandboxURLSearchParams + constructor(readonly url: URL) { + this.searchParams = new SandboxURLSearchParams(url.searchParams) + } +} + +export const isSandboxValue = ( + 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 diff --git a/packages/codemode/sst-env.d.ts b/packages/codemode/sst-env.d.ts new file mode 100644 index 00000000000..64441936d7a --- /dev/null +++ b/packages/codemode/sst-env.d.ts @@ -0,0 +1,10 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +/* deno-fmt-ignore-file */ +/* biome-ignore-all lint: auto-generated */ + +/// + +import "sst" +export {} \ No newline at end of file diff --git a/packages/codemode/test/codemode.test.ts b/packages/codemode/test/codemode.test.ts new file mode 100644 index 00000000000..221b5e07dfe --- /dev/null +++ b/packages/codemode/test/codemode.test.ts @@ -0,0 +1,1163 @@ +import { describe, expect, test } from "bun:test" +import { Cause, Effect, Schema } from "effect" +import { CodeMode, Tool, toolError } from "../src/index.js" + +const run = (tool: Tool.Definition) => + Effect.runPromise(CodeMode.make({ tools: { host: { call: tool } } }).execute("return await tools.host.call({})")) + +class UnsafeHostError extends Schema.TaggedErrorClass()("UnsafeHostError", { + reason: Schema.String, +}) {} + +describe("CodeMode host failure boundary", () => { + test("preserves explicit safe tool failures", async () => { + const result = await run( + Tool.make({ + description: "Fail safely", + input: Schema.Struct({}), + output: Schema.String, + run: () => Effect.fail(toolError("Authorized request was refused")), + }), + ) + + expect(result.ok ? undefined : result.error).toStrictEqual({ + kind: "ToolFailure", + message: "Authorized request was refused", + }) + }) + + test("sanitizes unknown host failures and defects", async () => { + for (const failure of [ + Effect.fail(new UnsafeHostError({ reason: "Authorization: Bearer typed-secret" })), + Effect.die(new Error("postgres://user:defect-secret@example.invalid")), + ]) { + const result = await run( + Tool.make({ + description: "Fail internally", + input: Schema.Struct({}), + output: Schema.String, + run: () => failure, + }), + ) + + expect(result.ok ? undefined : result.error).toStrictEqual({ + kind: "ToolFailure", + message: "Tool execution failed", + }) + expect(JSON.stringify(result)).not.toMatch(/typed-secret|defect-secret|Authorization: Bearer/) + } + }) + + test("sanitizes invalid host output", async () => { + const secret = "invalid-output-secret" + const result = await run( + Tool.make({ + description: "Return invalid output", + input: Schema.Struct({}), + output: Schema.Struct({ safe: Schema.String }), + run: () => Effect.succeed({ safe: 1, secret } as unknown as { readonly safe: string }), + }), + ) + + expect(result.ok ? undefined : result.error).toStrictEqual({ + kind: "InvalidToolOutput", + message: "Invalid output from tool 'host.call'.", + }) + expect(JSON.stringify(result)).not.toMatch(/invalid-output-secret/) + }) + + test("sanitizes host output that throws while being copied", async () => { + const result = await run( + Tool.make({ + description: "Return hostile output", + input: Schema.Struct({}), + output: Schema.Unknown, + run: () => + Effect.succeed( + new Proxy( + {}, + { + ownKeys: () => { + throw new Error("host-output-secret") + }, + }, + ), + ), + }), + ) + + expect(result.ok ? undefined : result.error).toStrictEqual({ + kind: "InvalidToolOutput", + message: "Invalid output from tool 'host.call'.", + }) + expect(JSON.stringify(result)).not.toMatch(/host-output-secret/) + }) + + test("caught tool failures are Error values in-program", async () => { + const result = await Effect.runPromise( + CodeMode.make({ + tools: { + host: { + call: Tool.make({ + description: "Refuse", + input: Schema.Struct({}), + output: Schema.String, + run: () => Effect.fail(toolError("Refused")), + }), + }, + }, + }).execute(` + try { + await tools.host.call({}) + return "no" + } catch (e) { + return { isError: e instanceof Error, message: e.message } + } + `), + ) + + expect(result.ok).toBe(true) + if (result.ok) expect(result.value).toStrictEqual({ isError: true, message: "Refused" }) + }) + + test("propagates host interruption instead of returning a diagnostic", async () => { + const exit = await Effect.runPromiseExit( + CodeMode.make({ + tools: { + host: { + call: Tool.make({ + description: "Interrupt", + input: Schema.Struct({}), + output: Schema.String, + run: () => Effect.interrupt, + }), + }, + }, + }).execute("return await tools.host.call({})"), + ) + + expect(exit._tag).toBe("Failure") + if (exit._tag === "Failure") { + expect(Cause.hasInterruptsOnly(exit.cause)).toBe(true) + } + }) +}) + +describe("CodeMode tool-call observation", () => { + test("reports the tools actually invoked with decoded input", async () => { + const calls: Array = [] + const lookup = Tool.make({ + description: "Look up a value", + input: Schema.Struct({ query: Schema.String }), + output: Schema.String, + run: ({ query }) => Effect.succeed(query), + }) + + const result = await Effect.runPromise( + CodeMode.make({ + tools: { context: { lookup } }, + onToolCallStart: (call) => Effect.sync(() => calls.push(call)), + }).execute(` + if (false) await tools.context.lookup({ query: "not called" }) + return await tools.context.lookup({ query: "deployment failure" }) + `), + ) + + expect(result.ok).toBe(true) + expect(calls).toStrictEqual([{ index: 0, name: "context.lookup", input: { query: "deployment failure" } }]) + }) + + test("observes settled calls with outcome and duration", async () => { + const events: Array<{ phase: string; index: number; name: string; outcome?: string; message?: string }> = [] + const lookup = Tool.make({ + description: "Look up a value", + input: Schema.Struct({ query: Schema.String }), + output: Schema.String, + run: ({ query }) => (query === "boom" ? Effect.fail(toolError("Lookup refused")) : Effect.succeed(query)), + }) + + const runtime = CodeMode.make({ + tools: { context: { lookup } }, + onToolCallStart: (call) => + Effect.sync(() => { + events.push({ phase: "start", index: call.index, name: call.name }) + }), + onToolCallEnd: (call) => + Effect.sync(() => { + expect(call.durationMs).toBeGreaterThanOrEqual(0) + events.push({ + phase: "end", + index: call.index, + name: call.name, + outcome: call.outcome, + ...(call.message === undefined ? {} : { message: call.message }), + }) + }), + }) + + const success = await Effect.runPromise(runtime.execute(`return await tools.context.lookup({ query: "ok" })`)) + expect(success.ok).toBe(true) + const failure = await Effect.runPromise(runtime.execute(`return await tools.context.lookup({ query: "boom" })`)) + expect(failure.ok).toBe(false) + + expect(events).toStrictEqual([ + { phase: "start", index: 0, name: "context.lookup" }, + { phase: "end", index: 0, name: "context.lookup", outcome: "success" }, + { phase: "start", index: 0, name: "context.lookup" }, + { phase: "end", index: 0, name: "context.lookup", outcome: "failure", message: "Lookup refused" }, + ]) + }) +}) + +describe("CodeMode console capture", () => { + test("captures console output as bounded result logs", async () => { + const result = await Effect.runPromise( + CodeMode.execute({ + code: ` + const returned = console.log("Thread info:", { name: "Demo", count: 2 }) + console.warn("careful") + return returned + `, + }), + ) + + expect(result).toStrictEqual({ + ok: true, + value: null, + logs: ['Thread info: {"name":"Demo","count":2}', "[warn] careful"], + toolCalls: [], + }) + expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result) + }) + + test("keeps logs captured before failures", async () => { + const result = await Effect.runPromise( + CodeMode.execute({ + code: ` + console.log("before failure") + throw new Error("boom") + `, + }), + ) + + expect(result.ok ? undefined : result.logs).toStrictEqual(["before failure"]) + expect(result.ok ? undefined : result.error.message).toBe("Uncaught: boom") + }) + + test("prints NaN and Infinity literally instead of the JSON null", async () => { + const result = await Effect.runPromise( + CodeMode.execute({ + code: ` + console.log(NaN) + console.log(Infinity, -Infinity) + console.log({ ratio: NaN, bounds: [Infinity] }) + return null + `, + }), + ) + + expect(result.ok).toBe(true) + expect(result.logs).toStrictEqual(["NaN", "Infinity -Infinity", '{"ratio":NaN,"bounds":[Infinity]}']) + }) + + test("renders sandbox values nested inside logged containers", async () => { + const result = await Effect.runPromise( + CodeMode.execute({ + code: ` + console.log({ m: new Map([["a", 1]]), when: new Date(0), r: /ab/g, s: new Set([1, 2]) }) + console.log([new Date(0)]) + return null + `, + }), + ) + + expect(result.ok).toBe(true) + expect(result.logs).toStrictEqual([ + '{"m":Map(1) [["a",1]],"when":1970-01-01T00:00:00.000Z,"r":/ab/g,"s":Set(2) [1,2]}', + "[1970-01-01T00:00:00.000Z]", + ]) + }) + + test("console formatting is total: cycles and opaque references render as markers", async () => { + const result = await Effect.runPromise( + CodeMode.execute({ + code: ` + const m = new Map() + m.set("self", m) + console.log({ box: m }) + console.log({ fn: (x) => x, ok: 1 }) + return null + `, + }), + ) + + expect(result.ok).toBe(true) + expect(result.logs).toStrictEqual(['{"box":Map(1) [["self",[Circular]]]}', '{"fn":[CodeMode reference],"ok":1}']) + }) + + test("console.table renders sandbox value cells", async () => { + const result = await Effect.runPromise( + CodeMode.execute({ + code: ` + console.table([{ when: new Date(0), n: NaN }]) + return null + `, + }), + ) + + expect(result.ok).toBe(true) + expect(result.logs).toStrictEqual(["(index)\twhen\tn\n0\t1970-01-01T00:00:00.000Z\tNaN"]) + }) + + test("captures console.dir and console.table output", async () => { + const result = await Effect.runPromise( + CodeMode.execute({ + code: ` + console.dir({ nested: { ok: true } }) + console.table([ + { name: "Kit", count: 1, hidden: "x" }, + { name: "Olive", count: 2, hidden: "y" } + ], ["name", "count"]) + return "done" + `, + }), + ) + + expect(result).toStrictEqual({ + ok: true, + value: "done", + logs: ['{"nested":{"ok":true}}', "(index)\tname\tcount\n0\tKit\t1\n1\tOlive\t2"], + toolCalls: [], + }) + }) +}) + +describe("CodeMode output budget", () => { + test("absent maxOutputBytes means no truncation at all", async () => { + const result = await Effect.runPromise( + CodeMode.execute({ + code: `console.log("z".repeat(50_000)); return "x".repeat(100_000)`, + }), + ) + + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.truncated).toBeUndefined() + expect(result.value).toBe("x".repeat(100_000)) + expect(result.logs).toStrictEqual(["z".repeat(50_000)]) + }) + + test("truncates an oversized result value with a marker instead of failing", async () => { + const limits: CodeMode.ExecutionLimits = { maxOutputBytes: 40 } + const result = await Effect.runPromise( + CodeMode.execute({ + code: `return { data: "${"x".repeat(200)}" }`, + limits, + }), + ) + + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.truncated).toBe(true) + expect(typeof result.value).toBe("string") + expect(result.value).toMatch( + /^\{"data":"x+ \[result truncated: \d+ bytes exceeds the 40-byte output limit; return a smaller value\]$/, + ) + expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result) + }) + + test("keeps leading logs within the remaining budget and marks the cut", async () => { + const limits: CodeMode.ExecutionLimits = { maxOutputBytes: 40 } + const result = await Effect.runPromise( + CodeMode.execute({ + code: ` + console.log("first line") + console.log("${"y".repeat(200)}") + return "ok" + `, + limits, + }), + ) + + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("ok") + expect(result.truncated).toBe(true) + expect(result.logs).toStrictEqual(["first line", "[logs truncated: showing 1 of 2 lines]"]) + }) + + test("does not mark results within the budget", async () => { + const result = await Effect.runPromise( + CodeMode.execute({ + code: ` + console.log("fits") + return { fits: true } + `, + }), + ) + expect(result).toStrictEqual({ + ok: true, + value: { fits: true }, + logs: ["fits"], + toolCalls: [], + }) + }) +}) + +describe("CodeMode schema flexibility", () => { + test("accepts render-only JSON Schema input and omitted output", async () => { + const observed: Array = [] + const call = Tool.make({ + description: "Call an adapter-described tool", + input: { + type: "object", + properties: { id: { type: "string" }, count: { type: "number" } }, + required: ["id"], + }, + run: (input) => + Effect.sync(() => { + observed.push(input) + return { echoed: input } + }), + }) + const runtime = CodeMode.make({ tools: { adapter: { call } } }) + + expect(runtime.catalog()).toStrictEqual([ + { + path: "adapter.call", + description: "Call an adapter-described tool", + signature: "tools.adapter.call(input: {\n id: string,\n count?: number,\n}): Promise", + }, + ]) + + // JSON Schema is render-only: mistyped input passes through unvalidated. + const result = await Effect.runPromise(runtime.execute(`return await tools.adapter.call({ id: 42 })`)) + expect(result.ok).toBe(true) + if (result.ok) expect(result.value).toStrictEqual({ echoed: { id: 42 } }) + expect(observed).toStrictEqual([{ id: 42 }]) + }) + + test("renders JSON Schema outputs and $defs references", async () => { + const lookup = Tool.make({ + description: "Look up a user", + input: { type: "object", properties: { login: { type: "string" } }, required: ["login"] }, + output: { + $ref: "#/$defs/User", + $defs: { + User: { + type: "object", + properties: { login: { type: "string" }, id: { type: "number" } }, + required: ["login", "id"], + }, + }, + }, + run: () => Effect.succeed({ login: "kit", id: 7 }), + }) + const runtime = CodeMode.make({ tools: { users: { lookup } } }) + + expect(runtime.catalog()).toStrictEqual([ + { + path: "users.lookup", + description: "Look up a user", + signature: "tools.users.lookup(input: {\n login: string,\n}): Promise<{\n login: string,\n id: number,\n}>", + }, + ]) + + const result = await Effect.runPromise(runtime.execute(`return await tools.users.lookup({ login: "kit" })`)) + expect(result.ok).toBe(true) + if (result.ok) expect(result.value).toStrictEqual({ login: "kit", id: 7 }) + }) + + test("Effect Schema output without an input transform still renders unknown when omitted", async () => { + const ping = Tool.make({ + description: "Ping", + input: Schema.Struct({ host: Schema.String }), + run: () => Effect.succeed("pong"), + }) + const runtime = CodeMode.make({ tools: { net: { ping } } }) + expect(runtime.catalog()[0]?.signature).toBe("tools.net.ping(input: {\n host: string,\n}): Promise") + + const result = await Effect.runPromise(runtime.execute(`return await tools.net.ping({ host: "example.test" })`)) + expect(result.ok).toBe(true) + if (result.ok) expect(result.value).toBe("pong") + }) +}) + +describe("CodeMode public contract", () => { + const lookup = Tool.make({ + description: "Look up an order by ID", + input: Schema.Struct({ id: Schema.String }), + output: Schema.Struct({ id: Schema.String, status: Schema.String }), + run: ({ id }) => Effect.succeed({ id, status: "open" }), + }) + const tools = { orders: { lookup } } + const source = `return await tools.orders.lookup({ id: "order_42" })` + + test("keeps one-shot and reusable execution equivalent", async () => { + const runtime = CodeMode.make({ tools }) + const [oneShot, reusable] = await Promise.all([ + Effect.runPromise(CodeMode.execute({ tools, code: source })), + Effect.runPromise(runtime.execute(source)), + ]) + + expect(reusable).toStrictEqual(oneShot) + const input: CodeMode.Input = { code: source } + expect(Schema.decodeUnknownSync(CodeMode.Input)(input)).toStrictEqual(input) + expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(reusable)))).toStrictEqual(reusable) + }) + + test("inlines a COMPLETE small catalog and keeps search registered but unadvertised", async () => { + const runtime = CodeMode.make({ tools }) + expect(runtime.catalog()).toStrictEqual([ + { + path: "orders.lookup", + description: "Look up an order by ID", + signature: "tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}>", + }, + ]) + expect(runtime.instructions()).toContain("Available tools (COMPLETE list") + expect(runtime.instructions()).toContain("- orders (1 tool)") + expect(runtime.instructions()).toContain( + " - 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/) + + // ...but the search tool stays registered, 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" })`)) + expect(result.ok).toBe(true) + if (result.ok) { + expect(result.value).toStrictEqual({ + items: [ + { + path: "tools.orders.lookup", + description: "Look up an order by ID", + signature: + "tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}>", + }, + ], + remaining: 0, + next: null, + }) + } + }) + + test("renders bracket notation for tool names that are not JavaScript identifiers", async () => { + const resolveLibrary = Tool.make({ + description: "Resolve a library ID", + input: Schema.Struct({ libraryName: Schema.String }), + output: Schema.String, + run: ({ libraryName }) => Effect.succeed(`/resolved/${libraryName}`), + }) + const runtime = CodeMode.make({ tools: { context7: { "resolve-library-id": resolveLibrary } } }) + + expect(runtime.catalog()).toStrictEqual([ + { + path: "context7.resolve-library-id", + description: "Resolve a library ID", + signature: 'tools.context7["resolve-library-id"](input: {\n libraryName: string,\n}): Promise', + }, + ]) + expect(runtime.instructions()).toContain( + '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" })`), + ) + expect(search.ok).toBe(true) + if (search.ok) { + expect(search.value).toStrictEqual({ + items: [ + { + path: 'tools.context7["resolve-library-id"]', + description: "Resolve a library ID", + signature: 'tools.context7["resolve-library-id"](input: {\n libraryName: string,\n}): Promise', + }, + ], + remaining: 0, + next: null, + }) + } + + const call = await Effect.runPromise( + runtime.execute(`return await tools.context7["resolve-library-id"]({ libraryName: "TypeScript" })`), + ) + expect(call.ok).toBe(true) + 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"]' })`), + ) + expect(exact.ok).toBe(true) + if (exact.ok) expect(exact.value).toMatchObject({ remaining: 0, next: null }) + }) + + test("instructions use markdown sections with placeholder-only call forms", () => { + const runtime = CodeMode.make({ tools }) + const instructions = runtime.instructions() + // Sections in order: workflow at the top, catalog at the bottom. + expect(instructions).toContain("## Workflow") + expect(instructions).toContain("## Rules") + expect(instructions).toContain("## Language") + expect(instructions.indexOf("## Workflow")).toBeLessThan(instructions.indexOf("## Rules")) + expect(instructions.indexOf("## Rules")).toBeLessThan(instructions.indexOf("## Language")) + expect(instructions.indexOf("## Language")).toBeLessThan( + instructions.indexOf("\n## Available tools (COMPLETE list"), + ) + expect(instructions).not.toContain("JSON.parse(res)") + expect(instructions).toContain("Return only the fields you need") + expect(instructions).toContain("avoid returning large raw payloads") + 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") + // 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)`") + expect(instructions).toContain("Return only the fields you need from structured results") + expect(instructions).toContain("check that it is a non-null object and not an array") + expect(instructions).not.toContain("result.") + expect(instructions).not.toContain("data.") + expect(instructions).not.toContain("total_count") + expect(instructions).not.toContain("list_issues") + expect(instructions).not.toContain("tools.orders.lookup({") + // COMPLETE: step 1 picks from the inlined list; search is not advertised. + expect(instructions).toContain("1. Pick a tool from the list under `## Available tools`") + expect(instructions).not.toContain("Browse one namespace") + + const partial = CodeMode.make({ tools, discovery: { catalogBudget: 0 } }).instructions() + // 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: "" })`.', + ) + 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("repeat the same search with `offset: next.offset`") + expect(partial).toContain(" limit?: number,\n offset?: number,") + expect(partial).not.toContain("total_count") + expect(partial).not.toContain("tools.orders.lookup({") + }) + + test("the language section describes the restricted runtime without overclaiming", () => { + const instructions = CodeMode.make({ tools }).instructions() + expect(instructions).toContain("restricted JavaScript language for calling tools") + 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"]) { + expect(instructions).toContain(missing) + } + 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( + "Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.", + ) + }) + + test("zero tools keep minimal sections and the no-tools notice", () => { + const runtime = CodeMode.make({}) + const instructions = runtime.instructions() + expect(instructions).toContain("No tools are currently available.") + expect(instructions).toContain("## Language") + expect(instructions).toContain("## Available tools") + expect(instructions).not.toContain("## Workflow") + expect(instructions).not.toContain("## Rules") + expect(instructions).not.toMatch(/\$codemode/) + }) + + test("uses one ranked search returning complete definitions for large catalogs", async () => { + const upload = Tool.make({ + description: "Upload one readable local file to the current Discord thread", + input: Schema.Struct({ path: Schema.String }), + output: Schema.Struct({ sent: Schema.Boolean }), + run: () => Effect.succeed({ sent: true }), + }) + const generate = Tool.make({ + description: "Generate an image and upload it to the current Discord thread", + input: Schema.Struct({ prompt: Schema.String }), + output: Schema.Struct({ sent: Schema.Boolean }), + run: () => Effect.succeed({ sent: true }), + }) + const runtime = CodeMode.make({ + 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("- thread (2 tools, none shown)") + expect(runtime.instructions()).toContain("- orders (1 tool, none shown)") + expect(runtime.instructions()).toMatch(/\$codemode\.search/) + expect(runtime.instructions()).not.toMatch(/tools\.thread\.uploadFile\(input/) + + const result = await Effect.runPromise( + runtime.execute(` + return await tools.$codemode.search({ + query: "send message attachment upload file to current Discord thread", + limit: 2 + }) + `), + ) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toStrictEqual({ + items: [ + { + path: "tools.thread.uploadFile", + description: "Upload one readable local file to the current Discord thread", + signature: "tools.thread.uploadFile(input: {\n path: string,\n}): Promise<{\n sent: boolean,\n}>", + }, + { + path: "tools.thread.generateImage", + description: "Generate an image and upload it to the current Discord thread", + signature: "tools.thread.generateImage(input: {\n prompt: string,\n}): Promise<{\n sent: boolean,\n}>", + }, + ], + remaining: 0, + next: null, + }) + expect(result.toolCalls).toStrictEqual([{ name: "$codemode.search" }]) + + const variants = await Effect.runPromise( + runtime.execute(` + return await Promise.all([ + tools.$codemode.search({ query: "file" }), + tools.$codemode.search({ query: "image" }) + ]) + `), + ) + expect(variants.ok).toBe(true) + if (variants.ok) { + expect((variants.value as Array<{ items: Array<{ path: string }> }>)[0]?.items[0]?.path).toBe( + "tools.thread.uploadFile", + ) + expect((variants.value as Array<{ items: Array<{ path: string }> }>)[1]?.items[0]?.path).toBe( + "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 defaults to 10 results and resolves exact tool paths", async () => { + const tool = (index: number) => + Tool.make({ + description: `Numbered tool ${index}`, + input: Schema.Struct({ id: Schema.String }), + output: Schema.String, + run: () => Effect.succeed("ok"), + }) + const runtime = CodeMode.make({ + tools: { + many: Object.fromEntries(Array.from({ length: 14 }, (_, index) => [`tool${index}`, tool(index)])), + }, + }) + + const browse = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({})`)) + expect(browse.ok).toBe(true) + if (browse.ok) { + const value = browse.value as { + items: Array<{ path: string }> + remaining: number + next: { offset: number } | null + } + expect(value.items).toHaveLength(10) + expect(value.remaining).toBe(4) + expect(value.next).toStrictEqual({ offset: 10 }) + } + + 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)} })`), + ) + expect(exact.ok).toBe(true) + if (exact.ok) { + expect(exact.value).toStrictEqual({ + items: [ + { + path: "tools.many.tool13", + description: "Numbered tool 13", + signature: "tools.many.tool13(input: {\n id: string,\n}): Promise", + }, + ], + remaining: 0, + next: null, + }) + } + } + }) + + test("scopes search to one namespace and browses it alphabetically", async () => { + const simple = (description: string) => + Tool.make({ + description, + input: Schema.Struct({ id: Schema.String }), + output: Schema.String, + run: () => Effect.succeed("ok"), + }) + const runtime = CodeMode.make({ + tools: { + github: { list_issues: simple("List issues"), create_issue: simple("Create an issue") }, + linear: { list_issues: simple("List Linear issues") }, + }, + }) + + // 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" })`), + ) + expect(browse.ok).toBe(true) + if (browse.ok) { + const value = browse.value as { items: Array<{ path: string }>; remaining: number } + expect(value.remaining).toBe(0) + expect(value.items.map((item) => item.path)).toStrictEqual([ + "tools.github.create_issue", + "tools.github.list_issues", + ]) + } + + // A query + namespace ranks within that namespace only. + const scoped = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.search({ query: "issues", namespace: "linear" })`), + ) + expect(scoped.ok).toBe(true) + if (scoped.ok) { + const value = scoped.value as { items: Array<{ path: string }>; remaining: number } + expect(value.remaining).toBe(0) + 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 })`), + ) + expect(invalid.ok).toBe(false) + if (!invalid.ok) expect(invalid.error.kind).toBe("InvalidToolInput") + }) + + test("matches input parameter names and partial-word substrings", async () => { + const upload = Tool.make({ + description: "Send a document to the workspace", + input: { + type: "object", + properties: { attachment: { type: "string", description: "Local path of the payload to send" } }, + required: ["attachment"], + }, + run: () => Effect.succeed("ok"), + }) + const other = Tool.make({ + description: "Rename the workspace", + input: Schema.Struct({ name: Schema.String }), + output: Schema.String, + run: () => Effect.succeed("ok"), + }) + const runtime = CodeMode.make({ tools: { files: { upload, other } } }) + + // "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" })`), + ) + expect(byParameter.ok).toBe(true) + if (byParameter.ok) { + const value = byParameter.value as { items: Array<{ path: string }>; remaining: number } + expect(value.remaining).toBe(0) + expect(value.items[0]?.path).toBe("tools.files.upload") + } + + // Substring matching: a partial word ("docum") still hits the description. + const bySubstring = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.search({ query: "docum" })`), + ) + expect(bySubstring.ok).toBe(true) + if (bySubstring.ok) { + const value = bySubstring.value as { items: Array<{ path: string }>; remaining: number } + expect(value.remaining).toBe(0) + expect(value.items[0]?.path).toBe("tools.files.upload") + } + }) + + test("a plural query term matches singular-only tool text", async () => { + const simple = (description: string) => + Tool.make({ + description, + input: Schema.Struct({ id: Schema.String }), + output: Schema.String, + run: () => Effect.succeed("ok"), + }) + const runtime = CodeMode.make({ + tools: { + // Neither path nor description contains "issues" - only the singular "issue". + tracker: { fetch_all: simple("Fetch every open issue in the project") }, + github: { list_issues: simple("List issues") }, + misc: { rename: simple("Rename the workspace") }, + }, + }) + + // "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" })`), + ) + expect(plural.ok).toBe(true) + if (plural.ok) { + const value = plural.value as { items: Array<{ path: string }>; remaining: number } + expect(value.remaining).toBe(0) + expect(value.items[0]?.path).toBe("tools.tracker.fetch_all") + } + + // ...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" })`)) + expect(ranked.ok).toBe(true) + if (ranked.ok) { + const value = ranked.value as { items: Array<{ path: string }>; remaining: number } + expect(value.remaining).toBe(0) + expect(value.items.map((item) => item.path)).toStrictEqual([ + "tools.github.list_issues", + "tools.tracker.fetch_all", + ]) + } + }) + + test("empty query lists everything alphabetically by path", async () => { + const simple = (description: string) => + Tool.make({ + description, + input: Schema.Struct({}), + output: Schema.String, + run: () => Effect.succeed("ok"), + }) + // Deliberately declared out of alphabetical order. + const runtime = CodeMode.make({ + tools: { + zeta: { last: simple("Last") }, + alpha: { beta: simple("Middle"), aardvark: simple("First") }, + }, + }) + const browse = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({})`)) + expect(browse.ok).toBe(true) + if (browse.ok) { + const value = browse.value as { items: Array<{ path: string }>; remaining: number; next: unknown } + expect(value.items.map((item) => item.path)).toStrictEqual([ + "tools.alpha.aardvark", + "tools.alpha.beta", + "tools.zeta.last", + ]) + expect(value.remaining).toBe(0) + expect(value.next).toBeNull() + } + + const middle = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.search({ limit: 1, offset: 1 })`), + ) + expect(middle.ok).toBe(true) + if (middle.ok) { + expect(middle.value).toMatchObject({ + items: [{ path: "tools.alpha.beta" }], + remaining: 1, + next: { offset: 2 }, + }) + } + + const exhausted = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.search({ limit: 1, offset: 3 })`), + ) + expect(exhausted.ok).toBe(true) + if (exhausted.ok) expect(exhausted.value).toStrictEqual({ items: [], remaining: 0, next: null }) + }) + + test("inlines round-robin across namespaces so one expensive namespace cannot starve the rest", () => { + const cheap = Tool.make({ + description: "Cheap", + input: Schema.Struct({ q: Schema.String }), + output: Schema.String, + run: () => Effect.succeed("ok"), + }) + const expensive = Tool.make({ + description: + "An expensive tool whose description alone consumes far more than the remaining inline catalog byte budget for this runtime", + input: Schema.Struct({ + someRatherLongParameterName: Schema.String, + anotherEvenLongerParameterName: Schema.Number, + }), + output: Schema.String, + run: () => Effect.succeed("ok"), + }) + // Round 1 places alpha.cheap (~17 estimated tokens) and beta.cheap (~17); in round 2 + // alpha.expensive does not fit, which marks only alpha done - it must NOT prevent + // other namespaces from inlining (beta already got its line in the same round). + const runtime = CodeMode.make({ + tools: { alpha: { cheap, expensive }, beta: { cheap } }, + discovery: { catalogBudget: 40 }, + }) + + const instructions = runtime.instructions() + expect(instructions).toContain( + "Available tools (PARTIAL - 2 of 3 shown; find the rest with tools.$codemode.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/) + }) + + test("charges inline JSDoc against the catalog token budget", () => { + const documented = Tool.make({ + description: "Look up a record", + input: { + type: "object", + properties: { + id: { type: "string", description: "A detailed identifier description. ".repeat(20) }, + }, + required: ["id"], + } as const, + run: () => Effect.succeed("ok"), + }) + const runtime = CodeMode.make({ + tools: { records: { lookup: documented } }, + discovery: { catalogBudget: 40 }, + }) + + 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()).not.toContain("tools.records.lookup(input:") + }) + + test("decodes tool input and output before exposing either side", async () => { + const observed: Array = [] + const transformed = Tool.make({ + description: "Double a number", + input: Schema.Struct({ value: Schema.NumberFromString }), + output: Schema.NumberFromString, + run: ({ value }) => + Effect.sync(() => { + observed.push(value) + return String(value * 2) + }), + }) + const runtime = CodeMode.make({ + tools: { math: { double: transformed } }, + onToolCallStart: (call) => Effect.sync(() => observed.push(call.input)), + }) + + const success = await Effect.runPromise(runtime.execute(`return await tools.math.double({ value: "21" })`)) + expect(success).toStrictEqual({ ok: true, value: 42, toolCalls: [{ name: "math.double" }] }) + expect(observed).toStrictEqual([{ value: 21 }, 21]) + + const invalid = await Effect.runPromise(runtime.execute(`return await tools.math.double({ value: 21 })`)) + expect(invalid.ok).toBe(false) + if (invalid.ok) return + expect(invalid.error.kind).toBe("InvalidToolInput") + expect(observed).toStrictEqual([{ value: 21 }, 21]) + }) + + test("returns JSON-safe data and normalizes undefined to null", async () => { + const result = await Effect.runPromise( + CodeMode.execute({ + code: `return { top: undefined, nested: [1, undefined] }`, + }), + ) + expect(result).toStrictEqual({ + ok: true, + value: { top: null, nested: [1, null] }, + toolCalls: [], + }) + expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result) + }) + + 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( + RangeError, + ) + expect(() => CodeMode.execute({ code: "return 1", limits: { maxToolCalls: -1 } })).toThrow(RangeError) + expect(() => CodeMode.execute({ code: "return 1", limits: { maxOutputBytes: -1 } })).toThrow(RangeError) + + expect(() => CodeMode.make({ tools, discovery: { catalogBudget: -1 } })).toThrow(RangeError) + + const result = await Effect.runPromise( + CodeMode.make({ + tools, + discovery: { catalogBudget: 0 }, + }).execute(`return await tools.$codemode.search({ query: "order", limit: 0.5 })`), + ) + expect(result.ok).toBe(false) + if (result.ok) return + expect(result.error.kind).toBe("InvalidToolInput") + + 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)} })`, + ), + ) + expect(invalidOffset.ok).toBe(false) + if (!invalidOffset.ok) expect(invalidOffset.error.kind).toBe("InvalidToolInput") + } + }) + + test("enforces the tool-call limit as a diagnostic", async () => { + const result = await Effect.runPromise(CodeMode.execute({ tools, code: source, limits: { maxToolCalls: 0 } })) + expect(result.ok).toBe(false) + if (!result.ok) expect(result.error.kind).toBe("ToolCallLimitExceeded") + }) + + test("timeoutMs and maxToolCalls have no defaults: absent means unlimited", async () => { + // 150 tool calls would have exceeded the old default cap of 100; with no limits + // provided, there is no cap and no timeout - budgets are host policy. + const counter = Tool.make({ + description: "Count invocations", + input: Schema.Struct({}), + output: Schema.Number, + run: () => Effect.succeed(1), + }) + const result = await Effect.runPromise( + CodeMode.execute({ + tools: { host: { count: counter } }, + code: ` + let total = 0 + for (let i = 0; i < 150; i += 1) total += await tools.host.count({}) + return total + `, + }), + ) + expect(result).toMatchObject({ ok: true, value: 150 }) + if (result.ok) expect(result.toolCalls.length).toBe(150) + }) + + test("the timeout interrupts a busy loop without any operation budget", async () => { + // Regression: timeout interruption must not depend on interpreter-side work accounting. + // The Effect fiber runtime auto-yields between interpreter steps, so a pure `while + // (true) {}` loop is interrupted by `timeoutMs` alone. + const startedAt = Date.now() + const result = await Effect.runPromise(CodeMode.execute({ code: "while (true) {}", limits: { timeoutMs: 200 } })) + const elapsedMs = Date.now() - startedAt + + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.error.kind).toBe("TimeoutExceeded") + expect(result.error.message).toContain("timed out after 200ms") + } + 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 new file mode 100644 index 00000000000..0de3dc3ea0d --- /dev/null +++ b/packages/codemode/test/enumeration.test.ts @@ -0,0 +1,159 @@ +import { describe, expect, test } from "bun:test" +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 +// 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. + +const echo = (description: string) => + Tool.make({ + description, + input: Schema.Struct({ value: Schema.String }), + output: Schema.String, + run: ({ value }) => Effect.succeed(value), + }) + +const tools = { + github: { list_issues: echo("List issues"), get_issue: echo("Get one issue") }, + memory: { search: echo("Search memory") }, + playwright: { navigate: echo("Navigate somewhere") }, +} + +const run = (code: string) => Effect.runPromise(CodeMode.execute({ tools, code })) +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 +} + +describe("Object.keys over tool references", () => { + test("enumerates top-level namespaces (the transcript program)", async () => { + expect( + await value(` + const namespaces = Object.keys(tools) + return { namespaces, count: namespaces.length } + `), + ).toEqual({ namespaces: ["github", "memory", "playwright", "$codemode"], count: 4 }) + }) + + test("enumerates tool names at a nested namespace", async () => { + expect(await value(`return Object.keys(tools.github)`)).toEqual(["list_issues", "get_issue"]) + }) + + test("a callable tool is a leaf and enumerates as []", async () => { + 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("an unknown namespace is an UnknownTool error pointing at the discovery idioms", 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 () => { + for (const method of ["values", "entries"] as const) { + 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.`, + ) + } + const nested = await error(`return Object.entries(tools.github)`) + expect(nested.message).toContain("Use Object.keys(tools) for names") + }) +}) + +describe("Object.keys over arrays", () => { + test("returns index strings, like JS", async () => { + expect(await value(`return Object.keys(["a", "b", "c"])`)).toEqual(["0", "1", "2"]) + expect(await value(`return Object.keys([])`)).toEqual([]) + }) + + test("objects keep their own enumerable keys", async () => { + expect(await value(`return Object.keys({ a: 1, b: 2 })`)).toEqual(["a", "b"]) + }) + + test("non-object inputs still fail clearly", async () => { + const failure = await error(`return Object.keys("nope")`) + expect(failure.message).toContain("Object.keys expects a data object or array") + }) +}) + +describe("for...in", () => { + test("iterates own enumerable keys of a plain object with break/continue", async () => { + expect( + await value(` + const seen = [] + for (const key in { a: 1, b: 2, c: 3, d: 4 }) { + if (key === "b") continue + if (key === "d") break + seen.push(key) + } + return seen + `), + ).toEqual(["a", "c"]) + }) + + test("iterates index strings over arrays", async () => { + expect( + await value(` + const indexes = [] + for (const i in ["x", "y", "z"]) { + if (i === "2") break + indexes.push(i) + } + return indexes + `), + ).toEqual(["0", "1"]) + }) + + test("supports let declarations and bare identifiers", async () => { + expect( + await value(` + let last = "" + for (let key in { a: 1, b: 2 }) last = key + return last + `), + ).toBe("b") + expect( + await value(` + let key = "before" + for (key in { only: 1 }) {} + return key + `), + ).toBe("only") + }) + + test("enumerates namespaces and tools from the callable tool tree", async () => { + expect( + await value(` + const names = [] + for (const ns in tools) { + for (const name in tools[ns]) names.push(ns + "." + name) + } + return names + `), + ).toEqual(["github.list_issues", "github.get_issue", "memory.search", "playwright.navigate", "$codemode.search"]) + }) + + test("unsupported values fail with a hint at for...of and Object.keys", async () => { + for (const expression of [`"text"`, "new Map([[1, 2]])", "new Set([1])", "42", "null"]) { + const failure = await error(`for (const key in ${expression}) {}; return "no"`) + expect(failure.message).toContain("for...in requires a plain object, array, or tools reference") + expect(failure.message).toContain("Use for...of for arrays/strings/Maps/Sets, or Object.keys(value)") + } + }) +}) diff --git a/packages/codemode/test/fixtures/openapi-happy-path.json b/packages/codemode/test/fixtures/openapi-happy-path.json new file mode 100644 index 00000000000..8052dd73911 --- /dev/null +++ b/packages/codemode/test/fixtures/openapi-happy-path.json @@ -0,0 +1,230 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "CodeMode Happy Path", + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://api.example.test/v1" + } + ], + "security": [ + { + "BearerAuth": [] + } + ], + "paths": { + "/users/{userId}": { + "parameters": [ + { + "$ref": "#/components/parameters/UserId" + } + ], + "get": { + "operationId": "users.get", + "summary": "Get a user", + "parameters": [ + { + "name": "include", + "in": "query", + "style": "form", + "explode": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "verbose", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "X-Trace-ID", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/UserResponse" + } + } + }, + "delete": { + "operationId": "users.remove", + "summary": "Remove a user", + "responses": { + "204": { + "description": "Removed" + } + } + } + }, + "/users": { + "post": { + "operationId": "users.create", + "summary": "Create a user", + "security": [ + { + "ApiKey": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "role": { + "type": "string", + "enum": ["admin", "member"] + } + }, + "required": ["name", "email"], + "additionalProperties": false + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/vnd.example+json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + }, + "/search": { + "get": { + "operationId": "search.run", + "summary": "Search users", + "security": [], + "parameters": [ + { + "name": "filter", + "in": "query", + "style": "deepObject", + "explode": true, + "schema": { + "type": "object", + "properties": { + "query": { + "type": "string" + }, + "page": { + "type": "integer" + } + }, + "required": ["query"], + "additionalProperties": false + } + }, + { + "name": "tags", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "Summary", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "parameters": { + "UserId": { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + }, + "responses": { + "UserResponse": { + "description": "A user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + } + }, + "schemas": { + "User": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "role": { + "type": "string", + "enum": ["admin", "member"] + } + }, + "required": ["id", "name", "email"], + "additionalProperties": false + } + }, + "securitySchemes": { + "BearerAuth": { + "type": "http", + "scheme": "bearer" + }, + "ApiKey": { + "type": "apiKey", + "in": "query", + "name": "api_key" + } + } + } +} diff --git a/packages/codemode/test/fixtures/opencode-v2-openapi.json b/packages/codemode/test/fixtures/opencode-v2-openapi.json new file mode 100644 index 00000000000..c78194e669f --- /dev/null +++ b/packages/codemode/test/fixtures/opencode-v2-openapi.json @@ -0,0 +1,23730 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "opencode HttpApi", + "version": "0.0.1", + "description": "Experimental HttpApi surface for selected instance routes." + }, + "paths": { + "/api/health": { + "get": { + "tags": ["server.health"], + "operationId": "v2.health.get", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "healthy": { + "type": "boolean", + "enum": [true] + } + }, + "required": ["healthy"], + "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": "Check whether the API server is ready to accept requests.", + "summary": "Check server health" + } + }, + "/api/location": { + "get": { + "tags": ["server.location"], + "operationId": "v2.location.get", + "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": "Location.Info", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Location.Info" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Resolve the requested location or the server default location.", + "summary": "Get location" + } + }, + "/api/agent": { + "get": { + "tags": ["server.agent"], + "operationId": "v2.agent.list", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentV2.Info" + } + } + }, + "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 currently registered agents.", + "summary": "List agents" + } + }, + "/api/plugin": { + "get": { + "tags": ["plugins"], + "operationId": "v2.plugin.list", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Plugin.Info" + } + } + }, + "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 currently loaded plugins.", + "summary": "List plugins" + } + }, + "/api/session": { + "get": { + "tags": ["sessions"], + "operationId": "v2.session.list", + "parameters": [ + { + "name": "workspace", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^wrk" + } + ] + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "limit", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Maximum number of sessions to return. Defaults to the newest 50 sessions." + }, + "required": false + }, + { + "name": "order", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string", + "enum": ["asc", "desc"] + }, + { + "type": "null" + } + ], + "description": "Session order for the first page. Use desc for newest first or asc for oldest first." + }, + "required": false + }, + { + "name": "search", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "parentID", + "in": "query", + "schema": { + "anyOf": [ + { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + { + "type": "string", + "enum": ["null"] + } + ], + "description": "Filter by parent session. Use null to return only root sessions." + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "directory", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "project", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "subpath", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "cursor", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string", + "description": "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response." + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "SessionsResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionsResponse" + } + } + } + }, + "400": { + "description": "InvalidCursorError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidCursorError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.", + "summary": "List sessions" + }, + "post": { + "tags": ["sessions"], + "operationId": "v2.session.create", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SessionV2.Info" + } + }, + "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" + } + } + } + } + }, + "description": "Create a session at the requested location.", + "summary": "Create session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + { + "type": "null" + } + ] + }, + "agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Ref" + }, + { + "type": "null" + } + ] + }, + "location": { + "anyOf": [ + { + "$ref": "#/components/schemas/Location.Ref" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/active": { + "get": { + "tags": ["sessions"], + "operationId": "v2.session.active", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "patternProperties": { + "^ses": { + "$ref": "#/components/schemas/SessionActive" + } + } + }, + "watermarks": { + "$ref": "#/components/schemas/SessionWatermarks" + } + }, + "required": ["data", "watermarks"], + "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 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.", + "summary": "List active sessions" + } + }, + "/api/session/{sessionID}": { + "get": { + "tags": ["sessions"], + "operationId": "v2.session.get", + "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/SessionV2.Info" + } + }, + "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": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve a session by ID.", + "summary": "Get session" + } + }, + "/api/session/{sessionID}/fork": { + "post": { + "tags": ["sessions"], + "operationId": "v2.session.fork", + "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/SessionV2.Info" + } + }, + "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 | MessageNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessageNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Create a child session by copying projected history from the parent. When messageID is supplied, copy messages before that boundary.", + "summary": "Fork session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messageID": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/agent": { + "post": { + "tags": ["sessions"], + "operationId": "v2.session.switchAgent", + "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": "Switch the agent used by subsequent provider turns.", + "summary": "Switch session agent", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agent": { + "type": "string" + } + }, + "required": ["agent"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/model": { + "post": { + "tags": ["sessions"], + "operationId": "v2.session.switchModel", + "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": "Switch the model used by subsequent provider turns.", + "summary": "Switch session model", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "model": { + "$ref": "#/components/schemas/Model.Ref" + } + }, + "required": ["model"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/rename": { + "post": { + "tags": ["sessions"], + "operationId": "v2.session.rename", + "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": "Update the session title.", + "summary": "Rename session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + } + }, + "required": ["title"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/prompt": { + "post": { + "tags": ["sessions"], + "operationId": "v2.session.prompt", + "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.Admitted" + } + }, + "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": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictError" + } + } + } + } + }, + "description": "Durably admit one session input and schedule agent-loop execution unless resume is false.", + "summary": "Send message", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + }, + "prompt": { + "$ref": "#/components/schemas/PromptInput" + }, + "delivery": { + "anyOf": [ + { + "type": "string", + "enum": ["steer", "queue"] + }, + { + "type": "null" + } + ] + }, + "resume": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": ["prompt"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/command": { + "post": { + "tags": ["sessions"], + "operationId": "v2.session.command", + "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.Admitted" + } + }, + "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 | CommandNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/CommandNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictError" + } + } + } + }, + "500": { + "description": "CommandEvaluationError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommandEvaluationError" + } + } + } + } + }, + "description": "Resolve a slash command into prompt input, admit it durably, and schedule execution unless resume is false.", + "summary": "Run command", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + }, + "command": { + "type": "string" + }, + "arguments": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Ref" + }, + { + "type": "null" + } + ] + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PromptInput.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "delivery": { + "anyOf": [ + { + "type": "string", + "enum": ["steer", "queue"] + }, + { + "type": "null" + } + ] + }, + "resume": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": ["command"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/skill": { + "post": { + "tags": ["sessions"], + "operationId": "v2.session.skill", + "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 | SkillNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SkillNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Activate a skill for a session by appending a skill message and resuming execution.", + "summary": "Activate skill", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + }, + "skill": { + "type": "string" + }, + "resume": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": ["skill"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/synthetic": { + "post": { + "tags": ["sessions"], + "operationId": "v2.session.synthetic", + "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": "Append a synthetic message to a session and resume execution.", + "summary": "Add synthetic message", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "type": "object" + } + }, + "required": ["text"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/compact": { + "post": { + "tags": ["sessions"], + "operationId": "v2.session.compact", + "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" + } + ] + } + } + } + }, + "409": { + "description": "SessionBusyError", + "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" + } + } + } + } + }, + "description": "Compact a session conversation.", + "summary": "Compact session" + } + }, + "/api/session/{sessionID}/wait": { + "post": { + "tags": ["sessions"], + "operationId": "v2.session.wait", + "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" + } + ] + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Wait for a session agent loop to become idle.", + "summary": "Wait for session" + } + }, + "/api/session/{sessionID}/revert/stage": { + "post": { + "tags": ["sessions"], + "operationId": "v2.session.revert.stage", + "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/Revert.State" + } + }, + "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": "MessageNotFoundError | SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessageNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "SessionBusyError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionBusyError" + } + } + } + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" + } + } + } + } + }, + "description": "Stage or move a reversible session boundary and optionally apply its file changes.", + "summary": "Stage session revert", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "files": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": ["messageID"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/revert/clear": { + "post": { + "tags": ["sessions"], + "operationId": "v2.session.revert.clear", + "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" + } + ] + } + } + } + }, + "409": { + "description": "SessionBusyError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionBusyError" + } + } + } + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" + } + } + } + } + }, + "summary": "Clear staged revert" + } + }, + "/api/session/{sessionID}/revert/commit": { + "post": { + "tags": ["sessions"], + "operationId": "v2.session.revert.commit", + "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" + } + ] + } + } + } + }, + "409": { + "description": "SessionBusyError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionBusyError" + } + } + } + } + }, + "summary": "Commit staged revert" + } + }, + "/api/session/{sessionID}/context": { + "get": { + "tags": ["sessions"], + "operationId": "v2.session.context", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session.Message" + } + } + }, + "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": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" + } + } + } + } + }, + "description": "Retrieve the active context messages for a session (all messages after the last compaction).", + "summary": "Get session context" + } + }, + "/api/session/{sessionID}/context-entry": { + "get": { + "tags": ["sessions"], + "operationId": "v2.session.context.entry.list", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionContextEntry.Info" + } + } + }, + "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": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "List API-managed context entries attached to the session's system context.", + "summary": "List context entries" + } + }, + "/api/session/{sessionID}/context-entry/{key}": { + "put": { + "tags": ["sessions"], + "operationId": "v2.session.context.entry.put", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "key", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SessionContextEntry.Key" + }, + "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": "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", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "value": {} + }, + "required": ["value"], + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { + "tags": ["sessions"], + "operationId": "v2.session.context.entry.remove", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "key", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SessionContextEntry.Key" + }, + "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": "Remove one context entry; the removal is announced to the model at the next turn boundary.", + "summary": "Remove context entry" + } + }, + "/api/session/{sessionID}/log": { + "get": { + "tags": ["sessions"], + "operationId": "v2.session.log", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "after", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "follow", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string", + "enum": ["true", "false"] + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "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/SessionLogItemStream" + } + }, + "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" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "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.", + "summary": "Read the session log" + } + }, + "/api/session/{sessionID}/interrupt": { + "post": { + "tags": ["sessions"], + "operationId": "v2.session.interrupt", + "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": "Interrupt active execution owned by this OpenCode process. Idle interruption is a no-op.", + "summary": "Interrupt session execution" + } + }, + "/api/session/{sessionID}/background": { + "post": { + "tags": ["sessions"], + "operationId": "v2.session.background", + "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": "Move active foreground backgroundable tools for this session into background observation. Idle requests are a no-op.", + "summary": "Background blocking session tools" + } + }, + "/api/session/{sessionID}/message/{messageID}": { + "get": { + "tags": ["sessions"], + "operationId": "v2.session.message", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "messageID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Session.Message" + } + }, + "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 | MessageNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessageNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve one projected message owned by the Session.", + "summary": "Get session message" + } + }, + "/api/session/{sessionID}/message": { + "get": { + "tags": ["messages"], + "operationId": "v2.session.messages", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "limit", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Maximum number of messages to return. When omitted, the endpoint returns its default page size." + }, + "required": false + }, + { + "name": "order", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string", + "enum": ["asc", "desc"] + }, + { + "type": "null" + } + ], + "description": "Message order for the first page. Use desc for newest first or asc for oldest first." + }, + "required": false + }, + { + "name": "cursor", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string", + "description": "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order." + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "SessionMessagesResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionMessagesResponse" + } + } + } + }, + "400": { + "description": "InvalidCursorError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidCursorError" + }, + { + "$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" + } + ] + } + } + } + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" + } + } + } + } + }, + "description": "Retrieve projected messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.", + "summary": "Get session messages" + } + }, + "/api/model": { + "get": { + "tags": ["models"], + "operationId": "v2.model.list", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ModelV2.Info" + } + } + }, + "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" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Retrieve available models ordered by release date.", + "summary": "List models" + } + }, + "/api/model/default": { + "get": { + "tags": ["models"], + "operationId": "v2.model.default", + "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": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelV2.Info" + }, + { + "type": "null" + } + ] + } + }, + "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" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Retrieve the model used when a session has no explicit model selection.", + "summary": "Get default model" + } + }, + "/api/generate": { + "post": { + "tags": ["generate"], + "operationId": "v2.generate.text", + "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": "GenerateTextResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenerateTextResponse" + } + } + } + }, + "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" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Run one stateless model generation at the requested location and return the assistant text. Uses the location's default model when none is specified.", + "summary": "Generate text", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "prompt": { + "type": "string" + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Ref" + }, + { + "type": "null" + } + ] + } + }, + "required": ["prompt"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/provider": { + "get": { + "tags": ["providers"], + "operationId": "v2.provider.list", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProviderV2.Info" + } + } + }, + "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" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Retrieve active AI providers so clients can show provider availability and configuration.", + "summary": "List providers" + } + }, + "/api/provider/{providerID}": { + "get": { + "tags": ["providers"], + "operationId": "v2.provider.get", + "parameters": [ + { + "name": "providerID", + "in": "path", + "schema": { + "type": "string" + }, + "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/ProviderV2.Info" + } + }, + "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": "ProviderNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderNotFoundError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Retrieve a single AI provider so clients can inspect its availability and endpoint settings.", + "summary": "Get provider" + } + }, + "/api/integration": { + "get": { + "tags": ["integrations"], + "operationId": "v2.integration.list", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Integration.Info" + } + } + }, + "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 available integrations and their authentication methods.", + "summary": "List integrations" + } + }, + "/api/integration/{integrationID}": { + "get": { + "tags": ["integrations"], + "operationId": "v2.integration.get", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "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": { + "anyOf": [ + { + "$ref": "#/components/schemas/Integration.Info" + }, + { + "type": "null" + } + ] + } + }, + "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 one integration and its authentication methods.", + "summary": "Get integration" + } + }, + "/api/integration/{integrationID}/connect/key": { + "post": { + "tags": ["integrations"], + "operationId": "v2.integration.connect.key", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "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": { + "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" + } + } + } + } + }, + "description": "Run a key authentication method and store the resulting credential.", + "summary": "Connect with key", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["key"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/integration/{integrationID}/connect/oauth": { + "post": { + "tags": ["integrations"], + "operationId": "v2.integration.connect.oauth", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "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/Integration.Attempt" + } + }, + "required": ["location", "data"], + "additionalProperties": false + } + } + } + }, + "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" + } + } + } + } + }, + "description": "Start an OAuth attempt and return the authorization details.", + "summary": "Begin OAuth connection", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "methodID": { + "type": "string" + }, + "inputs": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["methodID", "inputs"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/integration/attempt/{attemptID}": { + "get": { + "tags": ["integrations"], + "operationId": "v2.integration.attempt.status", + "parameters": [ + { + "name": "attemptID", + "in": "path", + "schema": { + "type": "string" + }, + "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/Integration.AttemptStatus" + } + }, + "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": "Poll the current status of an OAuth attempt.", + "summary": "Get OAuth attempt status" + }, + "delete": { + "tags": ["integrations"], + "operationId": "v2.integration.attempt.cancel", + "parameters": [ + { + "name": "attemptID", + "in": "path", + "schema": { + "type": "string" + }, + "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": { + "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": "Cancel an OAuth attempt and release its resources.", + "summary": "Cancel OAuth connection" + } + }, + "/api/integration/attempt/{attemptID}/complete": { + "post": { + "tags": ["integrations"], + "operationId": "v2.integration.attempt.complete", + "parameters": [ + { + "name": "attemptID", + "in": "path", + "schema": { + "type": "string" + }, + "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": { + "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" + } + } + } + } + }, + "description": "Complete a code-based OAuth attempt and store the resulting credential.", + "summary": "Complete OAuth connection", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/mcp": { + "get": { + "tags": ["mcp"], + "operationId": "v2.mcp.list", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Mcp.Server" + } + } + }, + "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 configured MCP servers and their connection status.", + "summary": "List MCP servers" + } + }, + "/api/credential/{credentialID}": { + "patch": { + "tags": ["server.credential"], + "operationId": "v2.credential.update", + "parameters": [ + { + "name": "credentialID", + "in": "path", + "schema": { + "type": "string" + }, + "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": { + "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": "Update a stored credential label.", + "summary": "Update credential", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "label": { + "type": "string" + } + }, + "required": ["label"], + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { + "tags": ["server.credential"], + "operationId": "v2.credential.remove", + "parameters": [ + { + "name": "credentialID", + "in": "path", + "schema": { + "type": "string" + }, + "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": { + "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": "Remove a stored integration credential.", + "summary": "Remove credential" + } + }, + "/api/project/current": { + "get": { + "tags": ["projects"], + "operationId": "v2.project.current", + "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": "Project.Current", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project.Current" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Resolve the project for the requested location.", + "summary": "Get current project" + } + }, + "/api/project/{projectID}/directories": { + "get": { + "tags": ["projects"], + "operationId": "v2.project.directories", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "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": "Project.Directories", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project.Directories" + } + } + } + }, + "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 local absolute directories for a project.", + "summary": "List project directories" + } + }, + "/api/form/request": { + "get": { + "tags": ["forms"], + "operationId": "v2.form.request.list", + "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": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.FormInfo" + }, + { + "$ref": "#/components/schemas/Form.UrlInfo" + } + ] + } + } + }, + "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 pending forms for a location.", + "summary": "List pending form requests" + } + }, + "/api/session/{sessionID}/form": { + "get": { + "tags": ["forms"], + "operationId": "v2.session.form.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.FormInfo" + }, + { + "$ref": "#/components/schemas/Form.UrlInfo" + } + ] + } + } + }, + "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": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve pending forms for a session.", + "summary": "List session forms" + }, + "post": { + "tags": ["forms"], + "operationId": "v2.session.form.create", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.FormInfo" + }, + { + "$ref": "#/components/schemas/Form.UrlInfo" + } + ] + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + }, + "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" + } + ] + } + } + } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictError" + } + } + } + } + }, + "description": "Create a form for a session.", + "summary": "Create session form", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Form.CreatePayload" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/form/{formID}": { + "get": { + "tags": ["forms"], + "operationId": "v2.session.form.get", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.FormInfo" + }, + { + "$ref": "#/components/schemas/Form.UrlInfo" + } + ] + } + }, + "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 | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve a form for a session.", + "summary": "Get session form" + } + }, + "/api/session/{sessionID}/form/{formID}/state": { + "get": { + "tags": ["forms"], + "operationId": "v2.session.form.state", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Form.State" + } + }, + "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 | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve the current state for a form.", + "summary": "Get form state" + } + }, + "/api/session/{sessionID}/form/{formID}/reply": { + "post": { + "tags": ["forms"], + "operationId": "v2.session.form.reply", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "FormInvalidAnswerError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormInvalidAnswerError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "FormAlreadySettledError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormAlreadySettledError" + } + } + } + } + }, + "description": "Submit an answer to a pending form.", + "summary": "Reply to form", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Form.Reply" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/form/{formID}/cancel": { + "post": { + "tags": ["forms"], + "operationId": "v2.session.form.cancel", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "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 | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "FormAlreadySettledError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormAlreadySettledError" + } + } + } + } + }, + "description": "Cancel a pending form.", + "summary": "Cancel form" + } + }, + "/api/permission/request": { + "get": { + "tags": ["permissions"], + "operationId": "v2.permission.request.list", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionV2.Request" + } + } + }, + "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 pending permission requests for a location.", + "summary": "List pending permission requests" + } + }, + "/api/permission/saved": { + "get": { + "tags": ["permissions"], + "operationId": "v2.permission.saved.list", + "parameters": [ + { + "name": "projectID", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionSaved.Info" + } + } + }, + "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" + } + } + } + } + }, + "description": "Retrieve saved permissions, optionally filtered by project.", + "summary": "List saved permissions" + } + }, + "/api/permission/saved/{id}": { + "delete": { + "tags": ["permissions"], + "operationId": "v2.permission.saved.remove", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "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" + } + } + } + } + }, + "description": "Remove a saved permission by ID.", + "summary": "Remove saved permission" + } + }, + "/api/session/{sessionID}/permission": { + "post": { + "tags": ["permissions"], + "operationId": "v2.session.permission.create", + "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": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "effect": { + "$ref": "#/components/schemas/PermissionV2.Effect" + } + }, + "required": ["id", "effect"], + "additionalProperties": false + } + }, + "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": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Evaluate and, when approval is required, create a permission request for a session.", + "summary": "Create permission request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + { + "type": "null" + } + ] + }, + "action": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "save": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object" + }, + "source": { + "$ref": "#/components/schemas/PermissionV2.Source" + }, + "agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["action", "resources"], + "additionalProperties": false + } + } + }, + "required": true + } + }, + "get": { + "tags": ["permissions"], + "operationId": "v2.session.permission.list", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionV2.Request" + } + } + }, + "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": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve pending permission requests owned by a session.", + "summary": "List session permission requests" + } + }, + "/api/session/{sessionID}/permission/{requestID}": { + "get": { + "tags": ["permissions"], + "operationId": "v2.session.permission.get", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/PermissionV2.Request" + } + }, + "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 | PermissionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/PermissionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve a pending permission request owned by a session.", + "summary": "Get permission request" + } + }, + "/api/session/{sessionID}/permission/{requestID}/reply": { + "post": { + "tags": ["permissions"], + "operationId": "v2.session.permission.reply", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "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 | PermissionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/PermissionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Respond to a pending permission request owned by a session.", + "summary": "Reply to pending permission request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "reply": { + "$ref": "#/components/schemas/PermissionV2.Reply" + }, + "message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["reply"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/fs/read/*": { + "get": { + "tags": ["filesystem"], + "operationId": "v2.fs.read", + "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/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Serve one file relative to the requested location.", + "summary": "Read file" + } + }, + "/api/fs/list": { + "get": { + "tags": ["filesystem"], + "operationId": "v2.fs.list", + "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 + }, + { + "name": "path", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystem.Entry" + } + } + }, + "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": "List direct children of one directory relative to the requested location.", + "summary": "List directory" + } + }, + "/api/fs/find": { + "get": { + "tags": ["filesystem"], + "operationId": "v2.fs.find", + "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 + }, + { + "name": "query", + "in": "query", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string", + "enum": ["file", "directory"] + }, + "required": false + }, + { + "name": "limit", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystem.Entry" + } + } + }, + "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": "Find recursively ranked filesystem entries relative to the requested location.", + "summary": "Find files" + } + }, + "/api/command": { + "get": { + "tags": ["commands"], + "operationId": "v2.command.list", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CommandV2.Info" + } + } + }, + "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 currently registered commands.", + "summary": "List commands" + } + }, + "/api/skill": { + "get": { + "tags": ["skills"], + "operationId": "v2.skill.list", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SkillV2.Info" + } + } + }, + "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 currently registered skills.", + "summary": "List skills" + } + }, + "/api/event": { + "get": { + "tags": ["events"], + "operationId": "v2.event.subscribe", + "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/V2EventStream" + } + }, + "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": "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.", + "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"], + "operationId": "v2.pty.list", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pty" + } + } + }, + "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": "List PTY sessions for a location, including exited sessions retained until removal.", + "summary": "List PTY sessions" + }, + "post": { + "tags": ["pty"], + "operationId": "v2.pty.create", + "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/Pty" + } + }, + "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": "Create a pseudo-terminal session for a location.", + "summary": "Create PTY session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "cwd": { + "type": "string" + }, + "title": { + "type": "string" + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/pty/{ptyID}": { + "get": { + "tags": ["pty"], + "operationId": "v2.pty.get", + "parameters": [ + { + "name": "ptyID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "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/Pty" + } + }, + "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": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } + } + }, + "description": "Get one PTY session, including its exit code once exited.", + "summary": "Get PTY session" + }, + "put": { + "tags": ["pty"], + "operationId": "v2.pty.update", + "parameters": [ + { + "name": "ptyID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "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/Pty" + } + }, + "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": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } + } + }, + "description": "Update the title or viewport size of one PTY session.", + "summary": "Update PTY session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "size": { + "type": "object", + "properties": { + "rows": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "cols": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + } + }, + "required": ["rows", "cols"], + "additionalProperties": false + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { + "tags": ["pty"], + "operationId": "v2.pty.remove", + "parameters": [ + { + "name": "ptyID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "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": { + "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": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } + } + }, + "description": "Terminate and remove one PTY session.", + "summary": "Remove PTY session" + } + }, + "/api/pty/{ptyID}/connect-token": { + "post": { + "tags": ["pty"], + "operationId": "v2.pty.connectToken", + "parameters": [ + { + "name": "ptyID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "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/PtyTicket.ConnectToken" + } + }, + "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" + } + } + } + }, + "403": { + "description": "ForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, + "404": { + "description": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } + } + }, + "description": "Create a short-lived single-use ticket for opening a PTY WebSocket connection.", + "summary": "Create PTY WebSocket token" + } + }, + "/api/pty/{ptyID}/connect": { + "get": { + "tags": ["pty"], + "operationId": "v2.pty.connect", + "x-websocket": true, + "parameters": [ + { + "name": "ptyID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "required": true + }, + { + "in": "query", + "name": "location[directory]", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "location[workspace]", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "cursor", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "ticket", + "schema": { + "type": "string" + } + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "403": { + "description": "ForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, + "404": { + "description": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } + } + }, + "description": "Establish a WebSocket connection streaming PTY output and accepting terminal input.", + "summary": "Connect to PTY session" + } + }, + "/api/shell": { + "get": { + "tags": ["shell"], + "operationId": "v2.shell.list", + "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": { + "type": "array", + "items": { + "$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" + } + } + } + } + }, + "description": "List currently running shell commands for a location. Exited commands are not included.", + "summary": "List running shell commands" + }, + "post": { + "tags": ["shell"], + "operationId": "v2.shell.create", + "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/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" + } + } + } + } + }, + "description": "Spawn one non-interactive shell command for a location. Combined stdout/stderr is captured to a file pageable via output.", + "summary": "Run shell command", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "timeout": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "metadata": { + "type": "object" + } + }, + "required": ["command"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/shell/{id}": { + "get": { + "tags": ["shell"], + "operationId": "v2.shell.get", + "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": "Get one shell command, including its status and exit code once exited.", + "summary": "Get shell command" + }, + "delete": { + "tags": ["shell"], + "operationId": "v2.shell.remove", + "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": { + "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": "ShellNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShellNotFoundError" + } + } + } + } + }, + "description": "Terminate and remove one shell command and its retained output.", + "summary": "Remove shell command" + } + }, + "/api/shell/{id}/output": { + "get": { + "tags": ["shell"], + "operationId": "v2.shell.output", + "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 + }, + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" + } + ] + }, + "required": false + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "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": ["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": "Page through captured combined output by absolute byte cursor.", + "summary": "Read shell output" + } + }, + "/api/question/request": { + "get": { + "tags": ["session questions"], + "operationId": "v2.question.request.list", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Request" + } + } + }, + "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 pending question requests for a location.", + "summary": "List pending question requests" + } + }, + "/api/session/{sessionID}/question": { + "get": { + "tags": ["session questions"], + "operationId": "v2.session.question.list", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Request" + } + } + }, + "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": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve pending question requests owned by a session.", + "summary": "List session question requests" + } + }, + "/api/session/{sessionID}/question/{requestID}/reply": { + "post": { + "tags": ["session questions"], + "operationId": "v2.session.question.reply", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | QuestionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Answer a pending question request owned by a session.", + "summary": "Reply to pending question request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QuestionV2.Reply" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/question/{requestID}/reject": { + "post": { + "tags": ["session questions"], + "operationId": "v2.session.question.reject", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | QuestionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Reject a pending question request owned by a session.", + "summary": "Reject pending question request" + } + }, + "/api/reference": { + "get": { + "tags": ["reference"], + "operationId": "v2.reference.list", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Reference.Info" + } + } + }, + "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": "List references available in the requested location.", + "summary": "List references" + } + }, + "/experimental/project/{projectID}/copy": { + "post": { + "tags": ["projectCopy"], + "operationId": "v2.projectCopy.create", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "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": "ProjectCopy.Copy", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectCopy.Copy" + } + } + } + }, + "400": { + "description": "ProjectCopyError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "strategy": { + "type": "string" + }, + "directory": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": ["strategy", "directory"], + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { + "tags": ["projectCopy"], + "operationId": "v2.projectCopy.remove", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "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": { + "204": { + "description": "" + }, + "400": { + "description": "ProjectCopyError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "force": { + "type": "boolean" + } + }, + "required": ["directory", "force"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/experimental/project/{projectID}/copy/refresh": { + "post": { + "tags": ["projectCopy"], + "operationId": "v2.projectCopy.refresh", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "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": { + "204": { + "description": "" + }, + "400": { + "description": "ProjectCopyError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + } + } + }, + "/api/vcs/status": { + "get": { + "tags": ["vcs"], + "operationId": "v2.vcs.status", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Vcs.FileStatus" + } + } + }, + "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": "List uncommitted working-copy changes relative to the requested location.", + "summary": "VCS status" + } + }, + "/api/vcs/diff": { + "get": { + "tags": ["vcs"], + "operationId": "v2.vcs.diff", + "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 + }, + { + "name": "mode", + "in": "query", + "schema": { + "$ref": "#/components/schemas/Vcs.Mode" + }, + "required": true + }, + { + "name": "context", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnapshotFileDiff" + } + } + }, + "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": "Diff the working copy against HEAD (mode git) or the default-branch merge base (mode branch) for the requested location.", + "summary": "VCS diff" + } + } + }, + "components": { + "schemas": { + "UnauthorizedError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["UnauthorizedError"] + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "message"], + "additionalProperties": false + }, + "InvalidRequestError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["InvalidRequestError"] + }, + "message": { + "type": "string" + }, + "kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "field": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["_tag", "message"], + "additionalProperties": false + }, + "Location.Info": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspaceID": { + "type": "string", + "allOf": [ + { + "pattern": "^wrk" + } + ] + }, + "project": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "directory": { + "type": "string" + } + }, + "required": ["id", "directory"], + "additionalProperties": false + } + }, + "required": ["directory", "project"], + "additionalProperties": false + }, + "Model.Ref": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": ["id", "providerID"], + "additionalProperties": false + }, + "Provider.Settings": { + "type": "object" + }, + "Provider.Request": { + "type": "object", + "properties": { + "settings": { + "$ref": "#/components/schemas/Provider.Settings" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + } + }, + "required": ["settings", "headers", "body"], + "additionalProperties": false + }, + "Agent.Color": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^#[0-9a-fA-F]{6}$" + } + ] + }, + { + "type": "string", + "enum": ["primary", "secondary", "accent", "success", "warning", "error", "info"] + } + ] + }, + "PermissionV2.Effect": { + "type": "string", + "enum": ["allow", "deny", "ask"] + }, + "PermissionV2.Rule": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "effect": { + "$ref": "#/components/schemas/PermissionV2.Effect" + } + }, + "required": ["action", "resource", "effect"], + "additionalProperties": false + }, + "PermissionV2.Ruleset": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionV2.Rule" + } + }, + "AgentV2.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "request": { + "$ref": "#/components/schemas/Provider.Request" + }, + "system": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": ["subagent", "primary", "all"] + }, + "hidden": { + "type": "boolean" + }, + "color": { + "$ref": "#/components/schemas/Agent.Color" + }, + "steps": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "permissions": { + "$ref": "#/components/schemas/PermissionV2.Ruleset" + } + }, + "required": ["id", "request", "mode", "hidden", "permissions"], + "additionalProperties": false + }, + "Plugin.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "Location.Ref": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspaceID": { + "type": "string", + "allOf": [ + { + "pattern": "^wrk" + } + ] + } + }, + "required": ["directory"], + "additionalProperties": false + }, + "File.Diff": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "status": { + "type": "string", + "enum": ["added", "modified", "deleted"] + }, + "additions": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "deletions": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "patch": { + "type": "string" + } + }, + "required": ["path", "status", "additions", "deletions", "patch"], + "additionalProperties": false + }, + "Revert.State": { + "type": "object", + "properties": { + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "partID": { + "type": "string" + }, + "snapshot": { + "type": "string" + }, + "diff": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/File.Diff" + } + } + }, + "required": ["messageID"], + "additionalProperties": false + }, + "SessionV2.Info": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "parentID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "projectID": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "cost": { + "type": "number" + }, + "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 + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "updated": { + "type": "number" + }, + "archived": { + "type": "number" + } + }, + "required": ["created", "updated"], + "additionalProperties": false + }, + "title": { + "type": "string" + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "subpath": { + "type": "string" + }, + "revert": { + "$ref": "#/components/schemas/Revert.State" + } + }, + "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" + } + }, + "watermarks": { + "$ref": "#/components/schemas/SessionWatermarks" + }, + "cursor": { + "type": "object", + "properties": { + "previous": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "next": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "required": ["data", "watermarks", "cursor"], + "additionalProperties": false + }, + "InvalidCursorError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["InvalidCursorError"] + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "message"], + "additionalProperties": false + }, + "InvalidRequestError1": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["InvalidRequestError"] + }, + "message": { + "type": "string" + }, + "kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "field": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["_tag", "message"], + "additionalProperties": false + }, + "SessionActive": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["running"] + } + }, + "required": ["type"], + "additionalProperties": false + }, + "SessionNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["SessionNotFoundError"] + }, + "sessionID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "sessionID", "message"], + "additionalProperties": false + }, + "MessageNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["MessageNotFoundError"] + }, + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "sessionID", "messageID", "message"], + "additionalProperties": false + }, + "Prompt.Source": { + "type": "object", + "properties": { + "start": { + "type": "number" + }, + "end": { + "type": "number" + }, + "text": { + "type": "string" + } + }, + "required": ["start", "end", "text"], + "additionalProperties": false + }, + "PromptInput.FileAttachment": { + "type": "object", + "properties": { + "uri": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "source": { + "$ref": "#/components/schemas/Prompt.Source" + } + }, + "required": ["uri"], + "additionalProperties": false + }, + "Prompt.AgentAttachment": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "source": { + "$ref": "#/components/schemas/Prompt.Source" + } + }, + "required": ["name"], + "additionalProperties": false + }, + "PromptInput": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PromptInput.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + } + }, + "required": ["text"], + "additionalProperties": false + }, + "Prompt.FileAttachment": { + "type": "object", + "properties": { + "uri": { + "type": "string" + }, + "mime": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "source": { + "$ref": "#/components/schemas/Prompt.Source" + } + }, + "required": ["uri", "mime"], + "additionalProperties": false + }, + "Prompt": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + } + }, + "required": ["text"], + "additionalProperties": false + }, + "SessionInput.Admitted": { + "type": "object", + "properties": { + "admittedSeq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "prompt": { + "$ref": "#/components/schemas/Prompt" + }, + "delivery": { + "type": "string", + "enum": ["steer", "queue"] + }, + "timeCreated": { + "type": "number" + }, + "promotedSeq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": ["admittedSeq", "id", "sessionID", "prompt", "delivery", "timeCreated"], + "additionalProperties": false + }, + "ConflictError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["ConflictError"] + }, + "message": { + "type": "string" + }, + "resource": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["_tag", "message"], + "additionalProperties": false + }, + "CommandNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["CommandNotFoundError"] + }, + "command": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "command", "message"], + "additionalProperties": false + }, + "CommandEvaluationError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["CommandEvaluationError"] + }, + "command": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "command", "message"], + "additionalProperties": false + }, + "SkillNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["SkillNotFoundError"] + }, + "skill": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "skill", "message"], + "additionalProperties": false + }, + "SessionBusyError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["SessionBusyError"] + }, + "sessionID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "sessionID", "message"], + "additionalProperties": false + }, + "ServiceUnavailableError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["ServiceUnavailableError"] + }, + "message": { + "type": "string" + }, + "service": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["_tag", "message"], + "additionalProperties": false + }, + "UnknownError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["UnknownError"] + }, + "message": { + "type": "string" + }, + "ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["_tag", "message"], + "additionalProperties": false + }, + "Session.Message.AgentSwitched": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": ["created"], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": ["agent-switched"] + }, + "agent": { + "type": "string" + } + }, + "required": ["id", "time", "type", "agent"], + "additionalProperties": false + }, + "Session.Message.ModelSwitched": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": ["created"], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": ["model-switched"] + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + } + }, + "required": ["id", "time", "type", "model"], + "additionalProperties": false + }, + "Session.Message.User": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": ["created"], + "additionalProperties": false + }, + "text": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "type": { + "type": "string", + "enum": ["user"] + } + }, + "required": ["id", "time", "text", "type"], + "additionalProperties": false + }, + "Session.Message.Synthetic": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": ["created"], + "additionalProperties": false + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "text": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["synthetic"] + } + }, + "required": ["id", "time", "sessionID", "text", "type"], + "additionalProperties": false + }, + "Session.Message.System": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": ["created"], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": ["system"] + }, + "text": { + "type": "string" + } + }, + "required": ["id", "time", "type", "text"], + "additionalProperties": false + }, + "Session.Message.Skill": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": ["created"], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": ["skill"] + }, + "name": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": ["id", "time", "type", "name", "text"], + "additionalProperties": false + }, + "Session.Message.Shell": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "completed": { + "type": "number" + } + }, + "required": ["created"], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": ["shell"] + }, + "callID": { + "type": "string" + }, + "command": { + "type": "string" + }, + "output": { + "type": "string" + } + }, + "required": ["id", "time", "type", "callID", "command", "output"], + "additionalProperties": false + }, + "Session.Message.Assistant.Text": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["text"] + }, + "id": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": ["type", "id", "text"], + "additionalProperties": false + }, + "LLM.ProviderMetadata": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "Session.Message.Assistant.Reasoning": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["reasoning"] + }, + "id": { + "type": "string" + }, + "text": { + "type": "string" + }, + "providerMetadata": { + "$ref": "#/components/schemas/LLM.ProviderMetadata" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "completed": { + "type": "number" + } + }, + "required": ["created"], + "additionalProperties": false + } + }, + "required": ["type", "id", "text"], + "additionalProperties": false + }, + "Session.Message.ToolState.Pending": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["pending"] + }, + "input": { + "type": "string" + } + }, + "required": ["status", "input"], + "additionalProperties": false + }, + "Tool.TextContent": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["text"] + }, + "text": { + "type": "string" + } + }, + "required": ["type", "text"], + "additionalProperties": false + }, + "Tool.FileContent": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["file"] + }, + "uri": { + "type": "string" + }, + "mime": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": ["type", "uri", "mime"], + "additionalProperties": false + }, + "LLM.ToolContent": { + "anyOf": [ + { + "$ref": "#/components/schemas/Tool.TextContent" + }, + { + "$ref": "#/components/schemas/Tool.FileContent" + } + ] + }, + "Session.Message.ToolState.Running": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["running"] + }, + "input": { + "type": "object" + }, + "structured": { + "type": "object" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } + } + }, + "required": ["status", "input", "structured", "content"], + "additionalProperties": false + }, + "Session.Message.ToolState.Completed": { + "type": "object", + "properties": { + "status": { + "type": "string", + "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"], + "additionalProperties": false + }, + "Session.Error.Unknown": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["unknown"] + }, + "message": { + "type": "string" + } + }, + "required": ["type", "message"], + "additionalProperties": false + }, + "Session.Message.ToolState.Error": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["error"] + }, + "input": { + "type": "object" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } + }, + "structured": { + "type": "object" + }, + "error": { + "$ref": "#/components/schemas/Session.Error.Unknown" + }, + "result": {} + }, + "required": ["status", "input", "content", "structured", "error"], + "additionalProperties": false + }, + "Session.Message.Assistant.Tool": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["tool"] + }, + "id": { + "type": "string" + }, + "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 + }, + "state": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.ToolState.Pending" + }, + { + "$ref": "#/components/schemas/Session.Message.ToolState.Running" + }, + { + "$ref": "#/components/schemas/Session.Message.ToolState.Completed" + }, + { + "$ref": "#/components/schemas/Session.Message.ToolState.Error" + } + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "ran": { + "type": "number" + }, + "completed": { + "type": "number" + }, + "pruned": { + "type": "number" + } + }, + "required": ["created"], + "additionalProperties": false + } + }, + "required": ["type", "id", "name", "state", "time"], + "additionalProperties": false + }, + "Session.Message.Assistant": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "completed": { + "type": "number" + } + }, + "required": ["created"], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": ["assistant"] + }, + "agent": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "content": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.Assistant.Text" + }, + { + "$ref": "#/components/schemas/Session.Message.Assistant.Reasoning" + }, + { + "$ref": "#/components/schemas/Session.Message.Assistant.Tool" + } + ] + } + }, + "snapshot": { + "type": "object", + "properties": { + "start": { + "type": "string" + }, + "end": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "finish": { + "type": "string" + }, + "cost": { + "type": "number" + }, + "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 + }, + "error": { + "$ref": "#/components/schemas/Session.Error.Unknown" + } + }, + "required": ["id", "time", "type", "agent", "model", "content"], + "additionalProperties": false + }, + "Session.Message.Compaction": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["compaction"] + }, + "reason": { + "type": "string", + "enum": ["auto", "manual"] + }, + "summary": { + "type": "string" + }, + "recent": { + "type": "string" + }, + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": ["created"], + "additionalProperties": false + } + }, + "required": ["type", "reason", "summary", "recent", "id", "time"], + "additionalProperties": false + }, + "Session.Message": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.AgentSwitched" + }, + { + "$ref": "#/components/schemas/Session.Message.ModelSwitched" + }, + { + "$ref": "#/components/schemas/Session.Message.User" + }, + { + "$ref": "#/components/schemas/Session.Message.Synthetic" + }, + { + "$ref": "#/components/schemas/Session.Message.System" + }, + { + "$ref": "#/components/schemas/Session.Message.Skill" + }, + { + "$ref": "#/components/schemas/Session.Message.Shell" + }, + { + "$ref": "#/components/schemas/Session.Message.Assistant" + }, + { + "$ref": "#/components/schemas/Session.Message.Compaction" + } + ] + }, + "SessionContextEntry.Key": { + "type": "string", + "allOf": [ + { + "pattern": "^[a-z0-9][a-z0-9._-]*$", + "description": "Context entry key (lowercase alphanumerics plus . _ -)" + } + ] + }, + "SessionContextEntry.Info": { + "type": "object", + "properties": { + "key": { + "$ref": "#/components/schemas/SessionContextEntry.Key" + }, + "value": {} + }, + "required": ["key", "value"], + "additionalProperties": false + }, + "session.next.agent.switched": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.agent.switched"] + }, + "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" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "agent": { + "type": "string" + } + }, + "required": ["timestamp", "sessionID", "messageID", "agent"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.model.switched": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.model.switched"] + }, + "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" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + } + }, + "required": ["timestamp", "sessionID", "messageID", "model"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.moved": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.moved"] + }, + "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" + } + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "subdirectory": { + "type": "string" + } + }, + "required": ["timestamp", "sessionID", "location"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.renamed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.renamed"] + }, + "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" + } + ] + }, + "title": { + "type": "string" + } + }, + "required": ["timestamp", "sessionID", "title"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.forked": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.forked"] + }, + "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" + } + ] + }, + "parentID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": ["timestamp", "sessionID", "parentID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.prompted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.prompted"] + }, + "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" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "prompt": { + "$ref": "#/components/schemas/Prompt" + }, + "delivery": { + "type": "string", + "enum": ["steer", "queue"] + } + }, + "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.prompt.admitted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.prompt.admitted"] + }, + "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" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "prompt": { + "$ref": "#/components/schemas/Prompt" + }, + "delivery": { + "type": "string", + "enum": ["steer", "queue"] + } + }, + "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.context.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.context.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": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "text": { + "type": "string" + } + }, + "required": ["timestamp", "sessionID", "messageID", "text"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.synthetic": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.synthetic"] + }, + "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" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "text": { + "type": "string" + }, + "description": { + "type": "string" + }, + "metadata": { + "type": "object" + } + }, + "required": ["timestamp", "sessionID", "messageID", "text"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.skill.activated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.skill.activated"] + }, + "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" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "name": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": ["timestamp", "sessionID", "messageID", "name", "text"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.shell.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.shell.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" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "command": { + "type": "string" + } + }, + "required": ["timestamp", "sessionID", "messageID", "callID", "command"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.shell.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.shell.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" + } + ] + }, + "callID": { + "type": "string" + }, + "output": { + "type": "string" + } + }, + "required": ["timestamp", "sessionID", "callID", "output"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.step.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.step.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_" + } + ] + }, + "agent": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "snapshot": { + "type": "string" + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "agent", "model"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.step.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.step.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_" + } + ] + }, + "finish": { + "type": "string" + }, + "cost": { + "type": "number" + }, + "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 + }, + "snapshot": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "finish", "cost", "tokens"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.step.failed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.step.failed"] + }, + "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_" + } + ] + }, + "error": { + "$ref": "#/components/schemas/Session.Error.Unknown" + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "error"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.text.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.text.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_" + } + ] + }, + "textID": { + "type": "string" + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "textID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.text.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.text.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_" + } + ] + }, + "textID": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "text"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.tool.input.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.tool.input.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_" + } + ] + }, + "callID": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "name"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.tool.input.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.tool.input.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_" + } + ] + }, + "callID": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "text"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "LLM.ProviderMetadata3": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "session.next.tool.called": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.tool.called"] + }, + "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_" + } + ] + }, + "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 + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "tool", "input", "provider"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.tool.progress": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.tool.progress"] + }, + "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_" + } + ] + }, + "callID": { + "type": "string" + }, + "structured": { + "type": "object" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "LLM.ProviderMetadata4": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "session.next.tool.success": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.tool.success"] + }, + "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_" + } + ] + }, + "callID": { + "type": "string" + }, + "structured": { + "type": "object" + }, + "content": { + "type": "array", + "items": { + "$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 + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content", "provider"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "LLM.ProviderMetadata5": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "session.next.tool.failed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.tool.failed"] + }, + "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_" + } + ] + }, + "callID": { + "type": "string" + }, + "error": { + "$ref": "#/components/schemas/Session.Error.Unknown" + }, + "result": {}, + "provider": { + "type": "object", + "properties": { + "executed": { + "type": "boolean" + }, + "metadata": { + "$ref": "#/components/schemas/LLM.ProviderMetadata5" + } + }, + "required": ["executed"], + "additionalProperties": false + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "error", "provider"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "LLM.ProviderMetadata6": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "session.next.reasoning.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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": { + "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"] + }, + "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" + } + ] + }, + "attempt": { + "type": "number" + }, + "error": { + "$ref": "#/components/schemas/session.next.retry_error" + } + }, + "required": ["timestamp", "sessionID", "attempt", "error"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.compaction.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.compaction.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" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "reason": { + "type": "string", + "enum": ["auto", "manual"] + } + }, + "required": ["timestamp", "sessionID", "messageID", "reason"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.compaction.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.compaction.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" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "reason": { + "type": "string", + "enum": ["auto", "manual"] + }, + "text": { + "type": "string" + }, + "recent": { + "type": "string" + } + }, + "required": ["timestamp", "sessionID", "messageID", "reason", "text", "recent"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.revert.staged": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.revert.staged"] + }, + "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" + } + ] + }, + "revert": { + "$ref": "#/components/schemas/Revert.State" + } + }, + "required": ["timestamp", "sessionID", "revert"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.revert.cleared": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.revert.cleared"] + }, + "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" + } + ] + } + }, + "required": ["timestamp", "sessionID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.revert.committed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.revert.committed"] + }, + "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" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": ["timestamp", "sessionID", "messageID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "SessionDurableEvent": { + "oneOf": [ + { + "$ref": "#/components/schemas/session.next.agent.switched" + }, + { + "$ref": "#/components/schemas/session.next.model.switched" + }, + { + "$ref": "#/components/schemas/session.next.moved" + }, + { + "$ref": "#/components/schemas/session.next.renamed" + }, + { + "$ref": "#/components/schemas/session.next.forked" + }, + { + "$ref": "#/components/schemas/session.next.prompted" + }, + { + "$ref": "#/components/schemas/session.next.prompt.admitted" + }, + { + "$ref": "#/components/schemas/session.next.context.updated" + }, + { + "$ref": "#/components/schemas/session.next.synthetic" + }, + { + "$ref": "#/components/schemas/session.next.skill.activated" + }, + { + "$ref": "#/components/schemas/session.next.shell.started" + }, + { + "$ref": "#/components/schemas/session.next.shell.ended" + }, + { + "$ref": "#/components/schemas/session.next.step.started" + }, + { + "$ref": "#/components/schemas/session.next.step.ended" + }, + { + "$ref": "#/components/schemas/session.next.step.failed" + }, + { + "$ref": "#/components/schemas/session.next.text.started" + }, + { + "$ref": "#/components/schemas/session.next.text.ended" + }, + { + "$ref": "#/components/schemas/session.next.tool.input.started" + }, + { + "$ref": "#/components/schemas/session.next.tool.input.ended" + }, + { + "$ref": "#/components/schemas/session.next.tool.called" + }, + { + "$ref": "#/components/schemas/session.next.tool.progress" + }, + { + "$ref": "#/components/schemas/session.next.tool.success" + }, + { + "$ref": "#/components/schemas/session.next.tool.failed" + }, + { + "$ref": "#/components/schemas/session.next.reasoning.started" + }, + { + "$ref": "#/components/schemas/session.next.reasoning.ended" + }, + { + "$ref": "#/components/schemas/session.next.retried" + }, + { + "$ref": "#/components/schemas/session.next.compaction.started" + }, + { + "$ref": "#/components/schemas/session.next.compaction.ended" + }, + { + "$ref": "#/components/schemas/session.next.revert.staged" + }, + { + "$ref": "#/components/schemas/session.next.revert.cleared" + }, + { + "$ref": "#/components/schemas/session.next.revert.committed" + } + ] + }, + "EventLog.Synced": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["log.synced"] + }, + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "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/EventLog.Synced" + } + ] + }, + "SessionLogItemStream": { + "type": "string", + "contentSchema": { + "$ref": "#/components/schemas/SessionLogItem" + }, + "contentMediaType": "application/json" + }, + "SessionMessagesResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session.Message" + } + }, + "watermark": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "cursor": { + "type": "object", + "properties": { + "previous": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "next": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "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": { + "tools": { + "type": "boolean" + }, + "input": { + "type": "array", + "items": { + "type": "string" + } + }, + "output": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["tools", "input", "output"], + "additionalProperties": false + }, + "Model.Cost": { + "type": "object", + "properties": { + "tier": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["context"] + }, + "size": { + "type": "integer" + } + }, + "required": ["type", "size"], + "additionalProperties": false + }, + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": ["read", "write"], + "additionalProperties": false + } + }, + "required": ["input", "output", "cache"], + "additionalProperties": false + }, + "ModelV2.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "family": { + "type": "string" + }, + "name": { + "type": "string" + }, + "api": { + "$ref": "#/components/schemas/Model.Api" + }, + "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 + } + }, + "time": { + "type": "object", + "properties": { + "released": { + "type": "number" + } + }, + "required": ["released"], + "additionalProperties": false + }, + "cost": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Model.Cost" + } + }, + "status": { + "type": "string", + "enum": ["alpha", "beta", "deprecated", "active"] + }, + "enabled": { + "type": "boolean" + }, + "limit": { + "type": "object", + "properties": { + "context": { + "type": "integer" + }, + "input": { + "type": "integer" + }, + "output": { + "type": "integer" + } + }, + "required": ["context", "output"], + "additionalProperties": false + } + }, + "required": [ + "id", + "providerID", + "name", + "api", + "capabilities", + "request", + "variants", + "time", + "cost", + "status", + "enabled", + "limit" + ], + "additionalProperties": false + }, + "GenerateTextResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": ["text"], + "additionalProperties": false + } + }, + "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": { + "id": { + "type": "string" + }, + "integrationID": { + "type": "string" + }, + "name": { + "type": "string" + }, + "disabled": { + "type": "boolean" + }, + "api": { + "$ref": "#/components/schemas/Provider.Api" + }, + "request": { + "$ref": "#/components/schemas/Provider.Request" + } + }, + "required": ["id", "name", "api", "request"], + "additionalProperties": false + }, + "ProviderNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["ProviderNotFoundError"] + }, + "providerID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "providerID", "message"], + "additionalProperties": false + }, + "Integration.When": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "op": { + "type": "string", + "enum": ["eq", "neq"] + }, + "value": { + "type": "string" + } + }, + "required": ["key", "op", "value"], + "additionalProperties": false + }, + "Integration.TextPrompt": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["text"] + }, + "key": { + "type": "string" + }, + "message": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "when": { + "$ref": "#/components/schemas/Integration.When" + } + }, + "required": ["type", "key", "message"], + "additionalProperties": false + }, + "Integration.SelectPrompt": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["select"] + }, + "key": { + "type": "string" + }, + "message": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + }, + "hint": { + "type": "string" + } + }, + "required": ["label", "value"], + "additionalProperties": false + } + }, + "when": { + "$ref": "#/components/schemas/Integration.When" + } + }, + "required": ["type", "key", "message", "options"], + "additionalProperties": false + }, + "Integration.OAuthMethod": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["oauth"] + }, + "label": { + "type": "string" + }, + "prompts": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Integration.TextPrompt" + }, + { + "$ref": "#/components/schemas/Integration.SelectPrompt" + } + ] + } + } + }, + "required": ["id", "type", "label"], + "additionalProperties": false + }, + "Integration.KeyMethod": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["key"] + }, + "label": { + "type": "string" + } + }, + "required": ["type"], + "additionalProperties": false + }, + "Integration.EnvMethod": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["env"] + }, + "names": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["type", "names"], + "additionalProperties": false + }, + "Integration.Method": { + "anyOf": [ + { + "$ref": "#/components/schemas/Integration.OAuthMethod" + }, + { + "$ref": "#/components/schemas/Integration.KeyMethod" + }, + { + "$ref": "#/components/schemas/Integration.EnvMethod" + } + ] + }, + "Connection.CredentialInfo": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["credential"] + }, + "id": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": ["type", "id", "label"], + "additionalProperties": false + }, + "Connection.EnvInfo": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["env"] + }, + "name": { + "type": "string" + } + }, + "required": ["type", "name"], + "additionalProperties": false + }, + "Connection.Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/Connection.CredentialInfo" + }, + { + "$ref": "#/components/schemas/Connection.EnvInfo" + } + ] + }, + "Integration.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Integration.Method" + } + }, + "connections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Connection.Info" + } + } + }, + "required": ["id", "name", "methods", "connections"], + "additionalProperties": false + }, + "Integration.Attempt": { + "type": "object", + "properties": { + "attemptID": { + "type": "string" + }, + "url": { + "type": "string" + }, + "instructions": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": ["auto", "code"] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["created", "expires"], + "additionalProperties": false + } + }, + "required": ["attemptID", "url", "instructions", "mode", "time"], + "additionalProperties": false + }, + "Integration.AttemptStatus": { + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["pending"] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["created", "expires"], + "additionalProperties": false + } + }, + "required": ["status", "time"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["complete"] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["created", "expires"], + "additionalProperties": false + } + }, + "required": ["status", "time"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["failed"] + }, + "message": { + "type": "string" + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["created", "expires"], + "additionalProperties": false + } + }, + "required": ["status", "message", "time"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["expired"] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["created", "expires"], + "additionalProperties": false + } + }, + "required": ["status", "time"], + "additionalProperties": false + } + ] + }, + "Mcp.Status.Connected": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["connected"] + } + }, + "required": ["status"], + "additionalProperties": false + }, + "Mcp.Status.Disconnected": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["disconnected"] + } + }, + "required": ["status"], + "additionalProperties": false + }, + "Mcp.Status.Disabled": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["disabled"] + } + }, + "required": ["status"], + "additionalProperties": false + }, + "Mcp.Status.Failed": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["failed"] + }, + "error": { + "type": "string" + } + }, + "required": ["status", "error"], + "additionalProperties": false + }, + "Mcp.Status.NeedsAuth": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["needs_auth"] + } + }, + "required": ["status"], + "additionalProperties": false + }, + "Mcp.Status.NeedsClientRegistration": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["needs_client_registration"] + }, + "error": { + "type": "string" + } + }, + "required": ["status", "error"], + "additionalProperties": false + }, + "Mcp.Server": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "status": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.Status.Connected" + }, + { + "$ref": "#/components/schemas/Mcp.Status.Disconnected" + }, + { + "$ref": "#/components/schemas/Mcp.Status.Disabled" + }, + { + "$ref": "#/components/schemas/Mcp.Status.Failed" + }, + { + "$ref": "#/components/schemas/Mcp.Status.NeedsAuth" + }, + { + "$ref": "#/components/schemas/Mcp.Status.NeedsClientRegistration" + } + ] + }, + "integrationID": { + "type": "string" + } + }, + "required": ["name", "status"], + "additionalProperties": false + }, + "Project.Current": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "directory": { + "type": "string" + } + }, + "required": ["id", "directory"], + "additionalProperties": false + }, + "Project.Directory": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "strategy": { + "type": "string" + } + }, + "required": ["directory"], + "additionalProperties": false + }, + "Project.Directories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Project.Directory" + } + }, + "Form.Metadata": { + "type": "object" + }, + "Form.When": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "op": { + "type": "string", + "enum": ["eq", "neq"] + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + { + "type": "boolean" + } + ] + } + }, + "required": ["key", "op", "value"], + "additionalProperties": false + }, + "Form.Option": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "label": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": ["value", "label"], + "additionalProperties": false + }, + "Form.StringField": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } + }, + "type": { + "type": "string", + "enum": ["string"] + }, + "format": { + "type": "string", + "enum": ["email", "uri", "date", "date-time"] + }, + "minLength": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "maxLength": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "pattern": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "default": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Option" + } + }, + "custom": { + "type": "boolean" + } + }, + "required": ["key", "type"], + "additionalProperties": false + }, + "Form.NumberField": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } + }, + "type": { + "type": "string", + "enum": ["number"] + }, + "minimum": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "maximum": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "default": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["key", "type"], + "additionalProperties": false + }, + "Form.IntegerField": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } + }, + "type": { + "type": "string", + "enum": ["integer"] + }, + "minimum": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "maximum": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "default": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["key", "type"], + "additionalProperties": false + }, + "Form.BooleanField": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } + }, + "type": { + "type": "string", + "enum": ["boolean"] + }, + "default": { + "type": "boolean" + } + }, + "required": ["key", "type"], + "additionalProperties": false + }, + "Form.MultiselectField": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } + }, + "type": { + "type": "string", + "enum": ["multiselect"] + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Option" + } + }, + "minItems": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "maxItems": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "custom": { + "type": "boolean" + }, + "default": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["key", "type", "options"], + "additionalProperties": false + }, + "Form.FormInfo": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "sessionID": { + "type": "string" + }, + "title": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/Form.Metadata" + }, + "mode": { + "type": "string", + "enum": ["form"] + }, + "fields": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.StringField" + }, + { + "$ref": "#/components/schemas/Form.NumberField" + }, + { + "$ref": "#/components/schemas/Form.IntegerField" + }, + { + "$ref": "#/components/schemas/Form.BooleanField" + }, + { + "$ref": "#/components/schemas/Form.MultiselectField" + } + ] + } + } + }, + "required": ["id", "sessionID", "mode", "fields"], + "additionalProperties": false + }, + "Form.UrlInfo": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "sessionID": { + "type": "string" + }, + "title": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/Form.Metadata" + }, + "mode": { + "type": "string", + "enum": ["url"] + }, + "url": { + "type": "string" + } + }, + "required": ["id", "sessionID", "mode", "url"], + "additionalProperties": false + }, + "Form.CreatePayload": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + { + "type": "null" + } + ] + }, + "title": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/Form.Metadata" + }, + "mode": { + "type": "string", + "enum": ["form", "url"] + }, + "fields": { + "anyOf": [ + { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.StringField" + }, + { + "$ref": "#/components/schemas/Form.NumberField" + }, + { + "$ref": "#/components/schemas/Form.IntegerField" + }, + { + "$ref": "#/components/schemas/Form.BooleanField" + }, + { + "$ref": "#/components/schemas/Form.MultiselectField" + } + ] + } + }, + { + "type": "null" + } + ] + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["mode"], + "additionalProperties": false + }, + "FormNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["FormNotFoundError"] + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "id", "message"], + "additionalProperties": false + }, + "Form.Value": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "Form.Answer": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Form.Value" + } + }, + "Form.State": { + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["pending"] + } + }, + "required": ["status"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["answered"] + }, + "answer": { + "$ref": "#/components/schemas/Form.Answer" + } + }, + "required": ["status", "answer"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["cancelled"] + } + }, + "required": ["status"], + "additionalProperties": false + } + ] + }, + "Form.Reply": { + "type": "object", + "properties": { + "answer": { + "$ref": "#/components/schemas/Form.Answer" + } + }, + "required": ["answer"], + "additionalProperties": false + }, + "FormAlreadySettledError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["FormAlreadySettledError"] + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "id", "message"], + "additionalProperties": false + }, + "FormInvalidAnswerError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["FormInvalidAnswerError"] + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "id", "message"], + "additionalProperties": false + }, + "PermissionV2.Source": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["tool"] + }, + "messageID": { + "type": "string" + }, + "callID": { + "type": "string" + } + }, + "required": ["type", "messageID", "callID"], + "additionalProperties": false + } + ] + }, + "PermissionV2.Request": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "action": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "save": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object" + }, + "source": { + "$ref": "#/components/schemas/PermissionV2.Source" + } + }, + "required": ["id", "sessionID", "action", "resources"], + "additionalProperties": false + }, + "PermissionSaved.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "projectID": { + "type": "string" + }, + "action": { + "type": "string" + }, + "resource": { + "type": "string" + } + }, + "required": ["id", "projectID", "action", "resource"], + "additionalProperties": false + }, + "PermissionNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["PermissionNotFoundError"] + }, + "requestID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "requestID", "message"], + "additionalProperties": false + }, + "PermissionV2.Reply": { + "type": "string", + "enum": ["once", "always", "reject"] + }, + "FileSystem.Entry": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["file", "directory"] + } + }, + "required": ["path", "type"], + "additionalProperties": false + }, + "CommandV2.Info": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "template": { + "type": "string" + }, + "description": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "subtask": { + "type": "boolean" + } + }, + "required": ["name", "template"], + "additionalProperties": false + }, + "SkillV2.Info": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "slash": { + "type": "boolean" + }, + "autoinvoke": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "content": { + "type": "string" + } + }, + "required": ["name", "location", "content"], + "additionalProperties": false + }, + "models-dev.refreshed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "integration.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "integration.connection.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "integrationID": { + "type": "string" + } + }, + "required": ["integrationID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "catalog.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "agent.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "SnapshotFileDiff": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "patch": { + "type": "string" + }, + "additions": { + "type": "number" + }, + "deletions": { + "type": "number" + }, + "status": { + "type": "string", + "enum": ["added", "deleted", "modified"] + } + }, + "required": ["additions", "deletions"], + "additionalProperties": false + }, + "PermissionAction": { + "type": "string", + "enum": ["allow", "deny", "ask"] + }, + "PermissionRule": { + "type": "object", + "properties": { + "permission": { + "type": "string" + }, + "pattern": { + "type": "string" + }, + "action": { + "$ref": "#/components/schemas/PermissionAction" + } + }, + "required": ["permission", "pattern", "action"], + "additionalProperties": false + }, + "PermissionRuleset": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionRule" + } + }, + "Session": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "slug": { + "type": "string" + }, + "projectID": { + "type": "string" + }, + "workspaceID": { + "type": "string", + "allOf": [ + { + "pattern": "^wrk" + } + ] + }, + "directory": { + "type": "string" + }, + "path": { + "type": "string" + }, + "parentID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "summary": { + "type": "object", + "properties": { + "additions": { + "type": "number" + }, + "deletions": { + "type": "number" + }, + "files": { + "type": "number" + }, + "diffs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnapshotFileDiff" + } + } + }, + "required": ["additions", "deletions", "files"], + "additionalProperties": false + }, + "cost": { + "type": "number" + }, + "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 + }, + "share": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "required": ["url"], + "additionalProperties": false + }, + "title": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": ["id", "providerID"], + "additionalProperties": false + }, + "version": { + "type": "string" + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "updated": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "compacting": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "archived": { + "type": "number" + } + }, + "required": ["created", "updated"], + "additionalProperties": false + }, + "permission": { + "$ref": "#/components/schemas/PermissionRuleset" + }, + "revert": { + "type": "object", + "properties": { + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "partID": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "snapshot": { + "type": "string" + }, + "diff": { + "type": "string" + } + }, + "required": ["messageID"], + "additionalProperties": false + } + }, + "required": ["id", "slug", "projectID", "directory", "title", "version", "time"], + "additionalProperties": false + }, + "session.created": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "info": { + "$ref": "#/components/schemas/Session" + } + }, + "required": ["sessionID", "info"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.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" + } + ] + }, + "info": { + "$ref": "#/components/schemas/Session" + } + }, + "required": ["sessionID", "info"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.deleted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "info": { + "$ref": "#/components/schemas/Session" + } + }, + "required": ["sessionID", "info"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "JSONSchema": { + "type": "object" + }, + "OutputFormat": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["text"] + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["json_schema"] + }, + "schema": { + "$ref": "#/components/schemas/JSONSchema" + }, + "retryCount": { + "anyOf": [ + { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": ["type", "schema"], + "additionalProperties": false + } + ] + }, + "UserMessage": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "role": { + "type": "string", + "enum": ["user"] + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": ["created"], + "additionalProperties": false + }, + "format": { + "anyOf": [ + { + "$ref": "#/components/schemas/OutputFormat" + }, + { + "type": "null" + } + ] + }, + "summary": { + "anyOf": [ + { + "type": "object", + "properties": { + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "body": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "diffs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnapshotFileDiff" + } + } + }, + "required": ["diffs"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "agent": { + "type": "string" + }, + "model": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "modelID": { + "type": "string" + }, + "variant": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["providerID", "modelID"], + "additionalProperties": false + }, + "system": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "tools": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "sessionID", "role", "time", "agent", "model"], + "additionalProperties": false + }, + "ProviderAuthError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["ProviderAuthError"] + }, + "data": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["providerID", "message"], + "additionalProperties": false + } + }, + "required": ["name", "data"], + "additionalProperties": false + }, + "UnknownError1": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["UnknownError"] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["message"], + "additionalProperties": false + } + }, + "required": ["name", "data"], + "additionalProperties": false + }, + "MessageOutputLengthError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["MessageOutputLengthError"] + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": ["name", "data"], + "additionalProperties": false + }, + "MessageAbortedError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["MessageAbortedError"] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"], + "additionalProperties": false + } + }, + "required": ["name", "data"], + "additionalProperties": false + }, + "StructuredOutputError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["StructuredOutputError"] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "retries": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": ["message", "retries"], + "additionalProperties": false + } + }, + "required": ["name", "data"], + "additionalProperties": false + }, + "ContextOverflowError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["ContextOverflowError"] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "responseBody": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["message"], + "additionalProperties": false + } + }, + "required": ["name", "data"], + "additionalProperties": false + }, + "ContentFilterError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["ContentFilterError"] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"], + "additionalProperties": false + } + }, + "required": ["name", "data"], + "additionalProperties": false + }, + "APIError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["APIError"] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "statusCode": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "isRetryable": { + "type": "boolean" + }, + "responseHeaders": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "responseBody": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "required": ["message", "isRetryable"], + "additionalProperties": false + } + }, + "required": ["name", "data"], + "additionalProperties": false + }, + "AssistantMessage": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "role": { + "type": "string", + "enum": ["assistant"] + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "completed": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": ["created"], + "additionalProperties": false + }, + "error": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderAuthError" + }, + { + "$ref": "#/components/schemas/UnknownError1" + }, + { + "$ref": "#/components/schemas/MessageOutputLengthError" + }, + { + "$ref": "#/components/schemas/MessageAbortedError" + }, + { + "$ref": "#/components/schemas/StructuredOutputError" + }, + { + "$ref": "#/components/schemas/ContextOverflowError" + }, + { + "$ref": "#/components/schemas/ContentFilterError" + }, + { + "$ref": "#/components/schemas/APIError" + } + ] + }, + { + "type": "null" + } + ] + }, + "parentID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "modelID": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "path": { + "type": "object", + "properties": { + "cwd": { + "type": "string" + }, + "root": { + "type": "string" + } + }, + "required": ["cwd", "root"], + "additionalProperties": false + }, + "summary": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "cost": { + "type": "number" + }, + "tokens": { + "type": "object", + "properties": { + "total": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "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 + }, + "structured": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + }, + "variant": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "finish": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "role", + "time", + "parentID", + "modelID", + "providerID", + "mode", + "agent", + "path", + "cost", + "tokens" + ], + "additionalProperties": false + }, + "Message": { + "anyOf": [ + { + "$ref": "#/components/schemas/UserMessage" + }, + { + "$ref": "#/components/schemas/AssistantMessage" + } + ] + }, + "message.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["message.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" + } + ] + }, + "info": { + "$ref": "#/components/schemas/Message" + } + }, + "required": ["sessionID", "info"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "message.removed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["message.removed"] + }, + "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" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + } + }, + "required": ["sessionID", "messageID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "TextPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": ["text"] + }, + "text": { + "type": "string" + }, + "synthetic": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "ignored": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "time": { + "anyOf": [ + { + "type": "object", + "properties": { + "start": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "end": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": ["start"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "sessionID", "messageID", "type", "text"], + "additionalProperties": false + }, + "SubtaskPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": ["subtask"] + }, + "prompt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "anyOf": [ + { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "modelID": { + "type": "string" + } + }, + "required": ["providerID", "modelID"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "command": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "sessionID", "messageID", "type", "prompt", "description", "agent"], + "additionalProperties": false + }, + "ReasoningPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": ["reasoning"] + }, + "text": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "end": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": ["start"], + "additionalProperties": false + } + }, + "required": ["id", "sessionID", "messageID", "type", "text", "time"], + "additionalProperties": false + }, + "FilePartSourceText": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "start": { + "type": "number" + }, + "end": { + "type": "number" + } + }, + "required": ["value", "start", "end"], + "additionalProperties": false + }, + "FileSource": { + "type": "object", + "properties": { + "text": { + "$ref": "#/components/schemas/FilePartSourceText" + }, + "type": { + "type": "string", + "enum": ["file"] + }, + "path": { + "type": "string" + } + }, + "required": ["text", "type", "path"], + "additionalProperties": false + }, + "Range": { + "type": "object", + "properties": { + "start": { + "type": "object", + "properties": { + "line": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "character": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": ["line", "character"], + "additionalProperties": false + }, + "end": { + "type": "object", + "properties": { + "line": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "character": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": ["line", "character"], + "additionalProperties": false + } + }, + "required": ["start", "end"], + "additionalProperties": false + }, + "SymbolSource": { + "type": "object", + "properties": { + "text": { + "$ref": "#/components/schemas/FilePartSourceText" + }, + "type": { + "type": "string", + "enum": ["symbol"] + }, + "path": { + "type": "string" + }, + "range": { + "$ref": "#/components/schemas/Range" + }, + "name": { + "type": "string" + }, + "kind": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": ["text", "type", "path", "range", "name", "kind"], + "additionalProperties": false + }, + "ResourceSource": { + "type": "object", + "properties": { + "text": { + "$ref": "#/components/schemas/FilePartSourceText" + }, + "type": { + "type": "string", + "enum": ["resource"] + }, + "clientName": { + "type": "string" + }, + "uri": { + "type": "string" + } + }, + "required": ["text", "type", "clientName", "uri"], + "additionalProperties": false + }, + "FilePartSource": { + "anyOf": [ + { + "$ref": "#/components/schemas/FileSource" + }, + { + "$ref": "#/components/schemas/SymbolSource" + }, + { + "$ref": "#/components/schemas/ResourceSource" + } + ] + }, + "FilePart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": ["file"] + }, + "mime": { + "type": "string" + }, + "filename": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "url": { + "type": "string" + }, + "source": { + "anyOf": [ + { + "$ref": "#/components/schemas/FilePartSource" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "sessionID", "messageID", "type", "mime", "url"], + "additionalProperties": false + }, + "ToolStatePending": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["pending"] + }, + "input": { + "type": "object" + }, + "raw": { + "type": "string" + } + }, + "required": ["status", "input", "raw"], + "additionalProperties": false + }, + "ToolStateRunning": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["running"] + }, + "input": { + "type": "object" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": ["start"], + "additionalProperties": false + } + }, + "required": ["status", "input", "time"], + "additionalProperties": false + }, + "ToolStateCompleted": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["completed"] + }, + "input": { + "type": "object" + }, + "output": { + "type": "string" + }, + "title": { + "type": "string" + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "end": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "compacted": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": ["start", "end"], + "additionalProperties": false + }, + "attachments": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/FilePart" + } + }, + { + "type": "null" + } + ] + } + }, + "required": ["status", "input", "output", "title", "metadata", "time"], + "additionalProperties": false + }, + "ToolStateError": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["error"] + }, + "input": { + "type": "object" + }, + "error": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "end": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": ["start", "end"], + "additionalProperties": false + } + }, + "required": ["status", "input", "error", "time"], + "additionalProperties": false + }, + "ToolState": { + "anyOf": [ + { + "$ref": "#/components/schemas/ToolStatePending" + }, + { + "$ref": "#/components/schemas/ToolStateRunning" + }, + { + "$ref": "#/components/schemas/ToolStateCompleted" + }, + { + "$ref": "#/components/schemas/ToolStateError" + } + ] + }, + "ToolPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": ["tool"] + }, + "callID": { + "type": "string" + }, + "tool": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/ToolState" + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "sessionID", "messageID", "type", "callID", "tool", "state"], + "additionalProperties": false + }, + "StepStartPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": ["step-start"] + }, + "snapshot": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "sessionID", "messageID", "type"], + "additionalProperties": false + }, + "StepFinishPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": ["step-finish"] + }, + "reason": { + "type": "string" + }, + "snapshot": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "cost": { + "type": "number" + }, + "tokens": { + "type": "object", + "properties": { + "total": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "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 + } + }, + "required": ["id", "sessionID", "messageID", "type", "reason", "cost", "tokens"], + "additionalProperties": false + }, + "SnapshotPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": ["snapshot"] + }, + "snapshot": { + "type": "string" + } + }, + "required": ["id", "sessionID", "messageID", "type", "snapshot"], + "additionalProperties": false + }, + "PatchPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": ["patch"] + }, + "hash": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["id", "sessionID", "messageID", "type", "hash", "files"], + "additionalProperties": false + }, + "AgentPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": ["agent"] + }, + "name": { + "type": "string" + }, + "source": { + "anyOf": [ + { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "start": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "end": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": ["value", "start", "end"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "sessionID", "messageID", "type", "name"], + "additionalProperties": false + }, + "RetryPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": ["retry"] + }, + "attempt": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "error": { + "$ref": "#/components/schemas/APIError" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": ["created"], + "additionalProperties": false + } + }, + "required": ["id", "sessionID", "messageID", "type", "attempt", "error", "time"], + "additionalProperties": false + }, + "CompactionPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": ["compaction"] + }, + "auto": { + "type": "boolean" + }, + "overflow": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "tail_start_id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "sessionID", "messageID", "type", "auto"], + "additionalProperties": false + }, + "Part": { + "anyOf": [ + { + "$ref": "#/components/schemas/TextPart" + }, + { + "$ref": "#/components/schemas/SubtaskPart" + }, + { + "$ref": "#/components/schemas/ReasoningPart" + }, + { + "$ref": "#/components/schemas/FilePart" + }, + { + "$ref": "#/components/schemas/ToolPart" + }, + { + "$ref": "#/components/schemas/StepStartPart" + }, + { + "$ref": "#/components/schemas/StepFinishPart" + }, + { + "$ref": "#/components/schemas/SnapshotPart" + }, + { + "$ref": "#/components/schemas/PatchPart" + }, + { + "$ref": "#/components/schemas/AgentPart" + }, + { + "$ref": "#/components/schemas/RetryPart" + }, + { + "$ref": "#/components/schemas/CompactionPart" + } + ] + }, + "message.part.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["message.part.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" + } + ] + }, + "part": { + "$ref": "#/components/schemas/Part" + }, + "time": { + "type": "number" + } + }, + "required": ["sessionID", "part", "time"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "message.part.removed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["message.part.removed"] + }, + "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" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "partID": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + } + }, + "required": ["sessionID", "messageID", "partID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.execution.settled": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "outcome": { + "type": "string", + "enum": ["success", "failure", "interrupted"] + }, + "error": { + "$ref": "#/components/schemas/Session.Error.Unknown" + } + }, + "required": ["timestamp", "sessionID", "outcome"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.text.delta": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "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_" + } + ] + }, + "textID": { + "type": "string" + }, + "delta": { + "type": "string" + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "delta"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.reasoning.delta": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "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" + }, + "delta": { + "type": "string" + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "delta"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.tool.input.delta": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "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_" + } + ] + }, + "callID": { + "type": "string" + }, + "delta": { + "type": "string" + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "delta"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.compaction.delta": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "text": { + "type": "string" + } + }, + "required": ["timestamp", "sessionID", "messageID", "text"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "file.edited": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "file": { + "type": "string" + } + }, + "required": ["file"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "reference.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "permission.v2.asked": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "action": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "save": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object" + }, + "source": { + "$ref": "#/components/schemas/PermissionV2.Source" + } + }, + "required": ["id", "sessionID", "action", "resources"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "permission.v2.replied": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "reply": { + "$ref": "#/components/schemas/PermissionV2.Reply" + } + }, + "required": ["sessionID", "requestID", "reply"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "plugin.added": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": ["id"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "project.directories.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "projectID": { + "type": "string" + } + }, + "required": ["projectID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "command.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "skill.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "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"], + "additionalProperties": false + }, + "Pty": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "title": { + "type": "string" + }, + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "cwd": { + "type": "string" + }, + "status": { + "type": "string", + "enum": ["running", "exited"] + }, + "pid": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "exitCode": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": ["id", "title", "command", "args", "cwd", "status", "pid"], + "additionalProperties": false + }, + "pty.created": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": ["info"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "pty.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": ["info"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "pty.exited": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "exitCode": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": ["id", "exitCode"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "pty.deleted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + } + }, + "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"], + "additionalProperties": false + }, + "shell.created": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Shell" + } + }, + "required": ["info"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "shell.exited": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "exit": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "status": { + "type": "string", + "enum": ["running", "exited", "timeout", "killed"] + } + }, + "required": ["id", "status"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "shell.deleted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + } + }, + "required": ["id"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "QuestionV2.Option": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Display text (1-5 words, concise)" + }, + "description": { + "type": "string", + "description": "Explanation of choice" + } + }, + "required": ["label", "description"], + "additionalProperties": false + }, + "QuestionV2.Info": { + "type": "object", + "properties": { + "question": { + "type": "string", + "description": "Complete question" + }, + "header": { + "type": "string", + "description": "Very short label (max 30 chars)" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Option" + }, + "description": "Available choices" + }, + "multiple": { + "type": "boolean" + }, + "custom": { + "type": "boolean" + } + }, + "required": ["question", "header", "options"], + "additionalProperties": false + }, + "QuestionV2.Tool": { + "type": "object", + "properties": { + "messageID": { + "type": "string" + }, + "callID": { + "type": "string" + } + }, + "required": ["messageID", "callID"], + "additionalProperties": false + }, + "question.v2.asked": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "questions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Info" + }, + "description": "Questions to ask" + }, + "tool": { + "$ref": "#/components/schemas/QuestionV2.Tool" + } + }, + "required": ["id", "sessionID", "questions"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "QuestionV2.Answer": { + "type": "array", + "items": { + "type": "string" + } + }, + "question.v2.replied": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Answer" + } + } + }, + "required": ["sessionID", "requestID", "answers"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "question.v2.rejected": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + } + }, + "required": ["sessionID", "requestID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "Form.Metadata1": { + "type": "object" + }, + "Form.When1": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "op": { + "type": "string", + "enum": ["eq", "neq"] + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "boolean" + } + ] + } + }, + "required": ["key", "op", "value"], + "additionalProperties": false + }, + "Form.StringField1": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } + }, + "type": { + "type": "string", + "enum": ["string"] + }, + "format": { + "type": "string", + "enum": ["email", "uri", "date", "date-time"] + }, + "minLength": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "maxLength": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "pattern": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "default": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Option" + } + }, + "custom": { + "type": "boolean" + } + }, + "required": ["key", "type"], + "additionalProperties": false + }, + "Form.NumberField1": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } + }, + "type": { + "type": "string", + "enum": ["number"] + }, + "minimum": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "maximum": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "default": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + } + }, + "required": ["key", "type"], + "additionalProperties": false + }, + "Form.IntegerField1": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } + }, + "type": { + "type": "string", + "enum": ["integer"] + }, + "minimum": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "maximum": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "default": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + } + }, + "required": ["key", "type"], + "additionalProperties": false + }, + "Form.BooleanField1": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } + }, + "type": { + "type": "string", + "enum": ["boolean"] + }, + "default": { + "type": "boolean" + } + }, + "required": ["key", "type"], + "additionalProperties": false + }, + "Form.MultiselectField1": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } + }, + "type": { + "type": "string", + "enum": ["multiselect"] + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Option" + } + }, + "minItems": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "maxItems": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "custom": { + "type": "boolean" + }, + "default": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["key", "type", "options"], + "additionalProperties": false + }, + "Form.FormInfo1": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "sessionID": { + "type": "string" + }, + "title": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/Form.Metadata1" + }, + "mode": { + "type": "string", + "enum": ["form"] + }, + "fields": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.StringField1" + }, + { + "$ref": "#/components/schemas/Form.NumberField1" + }, + { + "$ref": "#/components/schemas/Form.IntegerField1" + }, + { + "$ref": "#/components/schemas/Form.BooleanField1" + }, + { + "$ref": "#/components/schemas/Form.MultiselectField1" + } + ] + } + } + }, + "required": ["id", "sessionID", "mode", "fields"], + "additionalProperties": false + }, + "Form.UrlInfo1": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "sessionID": { + "type": "string" + }, + "title": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/Form.Metadata1" + }, + "mode": { + "type": "string", + "enum": ["url"] + }, + "url": { + "type": "string" + } + }, + "required": ["id", "sessionID", "mode", "url"], + "additionalProperties": false + }, + "form.created": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "form": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.FormInfo1" + }, + { + "$ref": "#/components/schemas/Form.UrlInfo1" + } + ] + } + }, + "required": ["form"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "Form.Value1": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "Form.Answer1": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Form.Value1" + } + }, + "form.replied": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "sessionID": { + "type": "string" + }, + "answer": { + "$ref": "#/components/schemas/Form.Answer1" + } + }, + "required": ["id", "sessionID", "answer"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "form.cancelled": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "sessionID": { + "type": "string" + } + }, + "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"], + "additionalProperties": false + }, + "SessionStatus": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["idle"] + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["retry"] + }, + "attempt": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "message": { + "type": "string" + }, + "action": { + "type": "object", + "properties": { + "reason": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "title": { + "type": "string" + }, + "message": { + "type": "string" + }, + "label": { + "type": "string" + }, + "link": { + "type": "string" + } + }, + "required": ["reason", "provider", "title", "message", "label"], + "additionalProperties": false + }, + "next": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": ["type", "attempt", "message", "next"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["busy"] + } + }, + "required": ["type"], + "additionalProperties": false + } + ] + }, + "session.status": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "status": { + "$ref": "#/components/schemas/SessionStatus" + } + }, + "required": ["sessionID", "status"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.idle": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + } + }, + "required": ["sessionID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "tui.prompt.append": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": ["text"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "tui.command.execute": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "command": { + "anyOf": [ + { + "type": "string", + "enum": [ + "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" + ] + }, + { + "type": "string" + } + ] + } + }, + "required": ["command"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "tui.toast.show": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "message": { + "type": "string" + }, + "variant": { + "type": "string", + "enum": ["info", "success", "warning", "error"] + }, + "duration": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": ["message", "variant"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "tui.session.select": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses", + "description": "Session ID to navigate to" + } + ] + } + }, + "required": ["sessionID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "installation.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "installation.update-available": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "vcs.branch.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "branch": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "mcp.status.changed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "server": { + "type": "string" + } + }, + "required": ["server"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "permission.asked": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "permission": { + "type": "string" + }, + "patterns": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object" + }, + "always": { + "type": "array", + "items": { + "type": "string" + } + }, + "tool": { + "anyOf": [ + { + "type": "object", + "properties": { + "messageID": { + "type": "string" + }, + "callID": { + "type": "string" + } + }, + "required": ["messageID", "callID"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "sessionID", "permission", "patterns", "metadata", "always"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "permission.replied": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "reply": { + "type": "string", + "enum": ["once", "always", "reject"] + } + }, + "required": ["sessionID", "requestID", "reply"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "QuestionOption": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Display text (1-5 words, concise)" + }, + "description": { + "type": "string", + "description": "Explanation of choice" + } + }, + "required": ["label", "description"], + "additionalProperties": false + }, + "QuestionInfo": { + "type": "object", + "properties": { + "question": { + "type": "string", + "description": "Complete question" + }, + "header": { + "type": "string", + "description": "Very short label (max 30 chars)" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionOption" + }, + "description": "Available choices" + }, + "multiple": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Allow selecting multiple choices" + }, + "custom": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Allow typing a custom answer (default: true)" + } + }, + "required": ["question", "header", "options"], + "additionalProperties": false + }, + "QuestionTool": { + "type": "object", + "properties": { + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "callID": { + "type": "string" + } + }, + "required": ["messageID", "callID"], + "additionalProperties": false + }, + "question.asked": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "questions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionInfo" + }, + "description": "Questions to ask" + }, + "tool": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionTool" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "sessionID", "questions"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "QuestionAnswer": { + "type": "array", + "items": { + "type": "string" + } + }, + "question.replied": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionAnswer" + } + } + }, + "required": ["sessionID", "requestID", "answers"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "question.rejected": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + } + }, + "required": ["sessionID", "requestID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.error": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "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 + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + { + "type": "null" + } + ] + }, + "error": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderAuthError" + }, + { + "$ref": "#/components/schemas/UnknownError1" + }, + { + "$ref": "#/components/schemas/MessageOutputLengthError" + }, + { + "$ref": "#/components/schemas/MessageAbortedError" + }, + { + "$ref": "#/components/schemas/StructuredOutputError" + }, + { + "$ref": "#/components/schemas/ContextOverflowError" + }, + { + "$ref": "#/components/schemas/ContentFilterError" + }, + { + "$ref": "#/components/schemas/APIError" + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "V2Event.server.connected": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "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": [ + { + "$ref": "#/components/schemas/Location.Ref" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "enum": ["server.connected"] + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "V2Event": { + "anyOf": [ + { + "$ref": "#/components/schemas/models-dev.refreshed" + }, + { + "$ref": "#/components/schemas/integration.updated" + }, + { + "$ref": "#/components/schemas/integration.connection.updated" + }, + { + "$ref": "#/components/schemas/catalog.updated" + }, + { + "$ref": "#/components/schemas/agent.updated" + }, + { + "$ref": "#/components/schemas/session.created" + }, + { + "$ref": "#/components/schemas/session.updated" + }, + { + "$ref": "#/components/schemas/session.deleted" + }, + { + "$ref": "#/components/schemas/message.updated" + }, + { + "$ref": "#/components/schemas/message.removed" + }, + { + "$ref": "#/components/schemas/message.part.updated" + }, + { + "$ref": "#/components/schemas/message.part.removed" + }, + { + "$ref": "#/components/schemas/session.next.agent.switched" + }, + { + "$ref": "#/components/schemas/session.next.model.switched" + }, + { + "$ref": "#/components/schemas/session.next.moved" + }, + { + "$ref": "#/components/schemas/session.next.renamed" + }, + { + "$ref": "#/components/schemas/session.next.forked" + }, + { + "$ref": "#/components/schemas/session.next.prompted" + }, + { + "$ref": "#/components/schemas/session.next.prompt.admitted" + }, + { + "$ref": "#/components/schemas/session.next.execution.settled" + }, + { + "$ref": "#/components/schemas/session.next.context.updated" + }, + { + "$ref": "#/components/schemas/session.next.synthetic" + }, + { + "$ref": "#/components/schemas/session.next.skill.activated" + }, + { + "$ref": "#/components/schemas/session.next.shell.started" + }, + { + "$ref": "#/components/schemas/session.next.shell.ended" + }, + { + "$ref": "#/components/schemas/session.next.step.started" + }, + { + "$ref": "#/components/schemas/session.next.step.ended" + }, + { + "$ref": "#/components/schemas/session.next.step.failed" + }, + { + "$ref": "#/components/schemas/session.next.text.started" + }, + { + "$ref": "#/components/schemas/session.next.text.delta" + }, + { + "$ref": "#/components/schemas/session.next.text.ended" + }, + { + "$ref": "#/components/schemas/session.next.reasoning.started" + }, + { + "$ref": "#/components/schemas/session.next.reasoning.delta" + }, + { + "$ref": "#/components/schemas/session.next.reasoning.ended" + }, + { + "$ref": "#/components/schemas/session.next.tool.input.started" + }, + { + "$ref": "#/components/schemas/session.next.tool.input.delta" + }, + { + "$ref": "#/components/schemas/session.next.tool.input.ended" + }, + { + "$ref": "#/components/schemas/session.next.tool.called" + }, + { + "$ref": "#/components/schemas/session.next.tool.progress" + }, + { + "$ref": "#/components/schemas/session.next.tool.success" + }, + { + "$ref": "#/components/schemas/session.next.tool.failed" + }, + { + "$ref": "#/components/schemas/session.next.retried" + }, + { + "$ref": "#/components/schemas/session.next.compaction.started" + }, + { + "$ref": "#/components/schemas/session.next.compaction.delta" + }, + { + "$ref": "#/components/schemas/session.next.compaction.ended" + }, + { + "$ref": "#/components/schemas/session.next.revert.staged" + }, + { + "$ref": "#/components/schemas/session.next.revert.cleared" + }, + { + "$ref": "#/components/schemas/session.next.revert.committed" + }, + { + "$ref": "#/components/schemas/file.edited" + }, + { + "$ref": "#/components/schemas/reference.updated" + }, + { + "$ref": "#/components/schemas/permission.v2.asked" + }, + { + "$ref": "#/components/schemas/permission.v2.replied" + }, + { + "$ref": "#/components/schemas/plugin.added" + }, + { + "$ref": "#/components/schemas/project.directories.updated" + }, + { + "$ref": "#/components/schemas/command.updated" + }, + { + "$ref": "#/components/schemas/skill.updated" + }, + { + "$ref": "#/components/schemas/file.watcher.updated" + }, + { + "$ref": "#/components/schemas/pty.created" + }, + { + "$ref": "#/components/schemas/pty.updated" + }, + { + "$ref": "#/components/schemas/pty.exited" + }, + { + "$ref": "#/components/schemas/pty.deleted" + }, + { + "$ref": "#/components/schemas/shell.created" + }, + { + "$ref": "#/components/schemas/shell.exited" + }, + { + "$ref": "#/components/schemas/shell.deleted" + }, + { + "$ref": "#/components/schemas/question.v2.asked" + }, + { + "$ref": "#/components/schemas/question.v2.replied" + }, + { + "$ref": "#/components/schemas/question.v2.rejected" + }, + { + "$ref": "#/components/schemas/form.created" + }, + { + "$ref": "#/components/schemas/form.replied" + }, + { + "$ref": "#/components/schemas/form.cancelled" + }, + { + "$ref": "#/components/schemas/todo.updated" + }, + { + "$ref": "#/components/schemas/session.status" + }, + { + "$ref": "#/components/schemas/session.idle" + }, + { + "$ref": "#/components/schemas/tui.prompt.append" + }, + { + "$ref": "#/components/schemas/tui.command.execute" + }, + { + "$ref": "#/components/schemas/tui.toast.show" + }, + { + "$ref": "#/components/schemas/tui.session.select" + }, + { + "$ref": "#/components/schemas/installation.updated" + }, + { + "$ref": "#/components/schemas/installation.update-available" + }, + { + "$ref": "#/components/schemas/vcs.branch.updated" + }, + { + "$ref": "#/components/schemas/mcp.status.changed" + }, + { + "$ref": "#/components/schemas/permission.asked" + }, + { + "$ref": "#/components/schemas/permission.replied" + }, + { + "$ref": "#/components/schemas/question.asked" + }, + { + "$ref": "#/components/schemas/question.replied" + }, + { + "$ref": "#/components/schemas/question.rejected" + }, + { + "$ref": "#/components/schemas/session.error" + }, + { + "$ref": "#/components/schemas/V2Event.server.connected" + } + ] + }, + "V2EventStream": { + "type": "string", + "contentSchema": { + "$ref": "#/components/schemas/V2Event" + }, + "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"] + }, + "ptyID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "ptyID", "message"], + "additionalProperties": false + }, + "PtyTicket.ConnectToken": { + "type": "object", + "properties": { + "ticket": { + "type": "string" + }, + "expires_in": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + } + }, + "required": ["ticket", "expires_in"], + "additionalProperties": false + }, + "ForbiddenError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["ForbiddenError"] + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "message"], + "additionalProperties": false + }, + "Shell1": { + "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": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "started": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "completed": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + } + }, + "required": ["started"], + "additionalProperties": false + } + }, + "required": ["id", "status", "command", "cwd", "shell", "file", "metadata", "time"], + "additionalProperties": false + }, + "ShellNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["ShellNotFoundError"] + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "id", "message"], + "additionalProperties": false + }, + "QuestionV2.Request": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "questions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Info" + }, + "description": "Questions to ask" + }, + "tool": { + "$ref": "#/components/schemas/QuestionV2.Tool" + } + }, + "required": ["id", "sessionID", "questions"], + "additionalProperties": false + }, + "QuestionV2.Reply": { + "type": "object", + "properties": { + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Answer" + }, + "description": "User answers in order of questions (each answer is an array of selected labels)" + } + }, + "required": ["answers"], + "additionalProperties": false + }, + "QuestionNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["QuestionNotFoundError"] + }, + "requestID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "requestID", "message"], + "additionalProperties": false + }, + "Reference.LocalSource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["local"] + }, + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + } + }, + "required": ["type", "path"], + "additionalProperties": false + }, + "Reference.GitSource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["git"] + }, + "repository": { + "type": "string" + }, + "branch": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + } + }, + "required": ["type", "repository"], + "additionalProperties": false + }, + "Reference.Source": { + "anyOf": [ + { + "$ref": "#/components/schemas/Reference.LocalSource" + }, + { + "$ref": "#/components/schemas/Reference.GitSource" + } + ] + }, + "Reference.Info": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + }, + "source": { + "$ref": "#/components/schemas/Reference.Source" + } + }, + "required": ["name", "path", "source"], + "additionalProperties": false + }, + "ProjectCopy.Copy": { + "type": "object", + "properties": { + "directory": { + "type": "string" + } + }, + "required": ["directory"], + "additionalProperties": false + }, + "ProjectCopyError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["ProjectCopyError"] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "forceRequired": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": ["message"], + "additionalProperties": false + } + }, + "required": ["name", "data"], + "additionalProperties": false + }, + "Vcs.FileStatus": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "additions": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "deletions": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "status": { + "type": "string", + "enum": ["added", "deleted", "modified"] + } + }, + "required": ["file", "additions", "deletions", "status"], + "additionalProperties": false + }, + "Vcs.Mode": { + "type": "string", + "enum": ["working", "branch"] + } + }, + "securitySchemes": {} + }, + "security": [], + "tags": [ + { + "name": "server.health" + }, + { + "name": "server.location" + }, + { + "name": "server.agent" + }, + { + "name": "plugins", + "description": "Experimental plugin routes." + }, + { + "name": "sessions", + "description": "Experimental session routes." + }, + { + "name": "messages", + "description": "Experimental message routes." + }, + { + "name": "models", + "description": "Experimental model routes." + }, + { + "name": "generate", + "description": "Experimental one-shot generation routes." + }, + { + "name": "providers", + "description": "Experimental provider routes." + }, + { + "name": "integrations", + "description": "Integration discovery and authentication routes." + }, + { + "name": "mcp", + "description": "MCP server status routes." + }, + { + "name": "server.credential" + }, + { + "name": "projects", + "description": "Location-scoped project routes." + }, + { + "name": "forms", + "description": "Session form routes." + }, + { + "name": "permissions", + "description": "Experimental permission routes." + }, + { + "name": "filesystem", + "description": "Experimental location-scoped filesystem routes." + }, + { + "name": "commands", + "description": "Experimental command routes." + }, + { + "name": "skills", + "description": "Experimental skill routes." + }, + { + "name": "events", + "description": "Experimental event stream routes." + }, + { + "name": "pty", + "description": "Experimental location-scoped PTY routes." + }, + { + "name": "shell", + "description": "Experimental location-scoped shell command routes." + }, + { + "name": "session questions", + "description": "Experimental session question routes." + }, + { + "name": "reference", + "description": "Location-scoped project references." + }, + { + "name": "projectCopy", + "description": "Project copy management routes." + }, + { + "name": "vcs", + "description": "Location-scoped version control routes." + } + ] +} diff --git a/packages/codemode/test/openapi.test.ts b/packages/codemode/test/openapi.test.ts new file mode 100644 index 00000000000..12a094e033b --- /dev/null +++ b/packages/codemode/test/openapi.test.ts @@ -0,0 +1,964 @@ +import { describe, expect, test } from "bun:test" +import { Effect, Layer, Option } from "effect" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { CodeMode, OpenAPI, Tool } from "../src/index.js" +import { inputTypeScript, outputTypeScript } from "../src/tool-schema.js" + +const baseUrl = "http://localhost:4096" +type Document = OpenAPI.Document + +type Recorded = { + readonly method: string + readonly url: string + readonly headers: Record + readonly body: unknown +} + +const opencodeSpec = async (): Promise => { + return Bun.file(new URL("./fixtures/opencode-v2-openapi.json", import.meta.url)).json() as Promise +} + +const happyPathSpec = async (): Promise => { + return Bun.file(new URL("./fixtures/openapi-happy-path.json", import.meta.url)).json() as Promise +} + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value) + +const toolAt = (tools: unknown, name: string) => + name.split(".").reduce((current, segment) => (isRecord(current) ? current[segment] : undefined), tools) + +const recordingClient = (respond: (request: HttpClientRequest.HttpClientRequest) => Response) => { + const requests: Array = [] + const layer = Layer.succeed(HttpClient.HttpClient)( + HttpClient.make((request) => + Effect.gen(function* () { + const body = + request.body._tag === "Uint8Array" ? JSON.parse(new TextDecoder().decode(request.body.body)) : undefined + const url = Option.map(HttpClientRequest.toUrl(request), (resolved) => resolved.toString()) + requests.push({ + method: request.method, + url: Option.getOrElse(url, () => request.url), + headers: { ...request.headers }, + body, + }) + return HttpClientResponse.fromWeb(request, respond(request)) + }), + ), + ) + return { requests, layer } +} + +const json = (value: unknown, status = 200) => + new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } }) + +const singleOperation = (operation: Record, method = "get"): Document => ({ + openapi: "3.1.0", + paths: { + "/test": { [method]: { operationId: "test", responses: { 200: { description: "Success" } }, ...operation } }, + }, +}) + +describe("OpenAPI.fromSpec", () => { + test("covers a representative API from generation through execution", async () => { + const resolutions: Array = [] + const client = recordingClient((request) => { + const url = Option.getOrElse(HttpClientRequest.toUrl(request), () => new URL(request.url)) + if (request.method === "POST") { + return new Response( + JSON.stringify({ id: "user-2", name: "Grace", email: "grace@example.test", role: "admin" }), + { status: 201, headers: { "content-type": "application/vnd.example+json" } }, + ) + } + if (request.method === "DELETE") return new Response(null, { status: 204 }) + if (url.pathname === "/search") { + return new Response("2 matches", { headers: { "content-type": "text/plain" } }) + } + return json({ id: "user-1", name: "Ada", email: "ada@example.test", role: "member" }) + }) + const api = OpenAPI.fromSpec({ + spec: await happyPathSpec(), + baseUrl, + auth: { + resolve: ({ name }) => { + resolutions.push(name) + return Effect.succeed( + name === "BearerAuth" + ? { type: "bearer", token: "bearer-secret" } + : { type: "apiKey", value: "api-secret" }, + ) + }, + }, + }) + const get = toolAt(api.tools, "users.get") + const create = toolAt(api.tools, "users.create") + const search = toolAt(api.tools, "search.run") + const remove = toolAt(api.tools, "users.remove") + + expect(api.skipped).toEqual([]) + if ( + !Tool.isDefinition(get) || + !Tool.isDefinition(create) || + !Tool.isDefinition(search) || + !Tool.isDefinition(remove) + ) { + throw new Error("happy-path fixture did not generate every operation") + } + expect(inputTypeScript(get)).toBe( + '{ userId: string; include?: Array; verbose?: boolean; "X-Trace-ID"?: string }', + ) + expect(inputTypeScript(create)).toBe('{ name: string; email: string; role?: "admin" | "member" }') + expect(inputTypeScript(search)).toBe("{ filter?: { query: string; page?: number }; tags?: Array }") + expect(inputTypeScript(remove)).toBe("{ userId: string }") + expect(outputTypeScript(get)).toContain("id: string") + expect(outputTypeScript(create)).toContain('role?: "admin" | "member"') + expect(outputTypeScript(search)).toBe("string") + expect(outputTypeScript(remove)).toBe("null") + + const result = await Effect.runPromise( + CodeMode.make({ tools: { api: api.tools } }) + .execute( + ` + const user = await tools.api.users.get({ + userId: "user-1", + include: ["profile", "permissions"], + verbose: true, + "X-Trace-ID": "trace-1", + }) + const created = await tools.api.users.create({ + name: "Grace", + email: "grace@example.test", + role: "admin", + }) + const summary = await tools.api.search.run({ + filter: { query: "effect", page: 2 }, + tags: ["typescript", "runtime"], + }) + const removed = await tools.api.users.remove({ userId: "user-1" }) + return { user, created, summary, removed } + `, + ) + .pipe(Effect.provide(client.layer)), + ) + + expect(result).toMatchObject({ + ok: true, + value: { + user: { id: "user-1", name: "Ada" }, + created: { id: "user-2", name: "Grace" }, + summary: "2 matches", + removed: null, + }, + }) + expect(resolutions).toEqual(["BearerAuth", "ApiKey", "BearerAuth"]) + expect(client.requests).toHaveLength(4) + + const getUrl = new URL(client.requests[0]!.url) + expect(getUrl.pathname).toBe("/users/user-1") + expect(getUrl.searchParams.get("include")).toBe("profile,permissions") + expect(getUrl.searchParams.get("verbose")).toBe("true") + expect(client.requests[0]!.headers["x-trace-id"]).toBe("trace-1") + expect(client.requests[0]!.headers.authorization).toBe("Bearer bearer-secret") + + const createUrl = new URL(client.requests[1]!.url) + expect(createUrl.searchParams.get("api_key")).toBe("api-secret") + expect(client.requests[1]!.body).toEqual({ name: "Grace", email: "grace@example.test", role: "admin" }) + + const searchUrl = new URL(client.requests[2]!.url) + expect(searchUrl.searchParams.get("filter[query]")).toBe("effect") + expect(searchUrl.searchParams.get("filter[page]")).toBe("2") + expect(searchUrl.searchParams.getAll("tags")).toEqual(["typescript", "runtime"]) + expect(client.requests[2]!.headers.authorization).toBeUndefined() + expect(new URL(client.requests[3]!.url).pathname).toBe("/users/user-1") + expect(client.requests[3]!.headers.authorization).toBe("Bearer bearer-secret") + }) + + test("converts representative opencode operations into the expected tool shape", async () => { + const spec = await opencodeSpec() + const result = OpenAPI.fromSpec({ spec, baseUrl }) + + expect(result.skipped).toHaveLength(5) + 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).toContainEqual({ + method: "GET", + path: "/api/fs/read/*", + reason: "binary responses are not supported", + }) + expect(toolAt(result.tools, "v2.health.get")).not.toBeUndefined() + expect(toolAt(result.tools, "v2.session.get")).not.toBeUndefined() + expect(toolAt(result.tools, "v2.session.create")).not.toBeUndefined() + + const sessionGet = toolAt(result.tools, "v2.session.get") + expect(Tool.isDefinition(sessionGet)).toBe(true) + if (!Tool.isDefinition(sessionGet)) throw new Error("v2.session.get was not generated") + expect(inputTypeScript(sessionGet)).toBe("{ sessionID: string }") + expect(outputTypeScript(sessionGet)).toContain("id: string") + expect(outputTypeScript(sessionGet)).toContain("additions: number") + + const switchAgent = toolAt(result.tools, "v2.session.switchAgent") + expect(Tool.isDefinition(switchAgent)).toBe(true) + 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() + 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() + }) + + test("preserves operation path sanitization and collision handling", () => { + const response = { responses: { 200: { description: "Success" } } } + const result = OpenAPI.fromSpec({ + baseUrl, + spec: { + openapi: "3.1.0", + paths: { + "/first": { get: { ...response, operationId: "group.item" } }, + "/second": { get: { ...response, operationId: "group.item" } }, + "/third": { get: { ...response, operationId: "group..other" } }, + }, + }, + }) + + expect(Tool.isDefinition(toolAt(result.tools, "group.item"))).toBe(true) + expect(Tool.isDefinition(toolAt(result.tools, "group_item_2"))).toBe(true) + expect(Tool.isDefinition(toolAt(result.tools, "group.operation.other"))).toBe(true) + }) + + test("synthesizes flat operation IDs from methods and paths", () => { + const response = { responses: { 200: { description: "Success" } } } + const tools = OpenAPI.fromSpec({ + baseUrl, + spec: { + openapi: "3.1.0", + paths: { + "/users": { get: response, post: response }, + "/users/{id}": { get: response, patch: response, delete: response }, + "/organizations/{organizationId}/users/{id}": { get: response }, + }, + }, + }).tools + + for (const path of [ + "getUsers", + "postUsers", + "getUsersById", + "patchUsersById", + "deleteUsersById", + "getOrganizationsByOrganizationidUsersById", + ]) { + expect(Tool.isDefinition(toolAt(tools, path))).toBe(true) + } + }) + + test("lets operation parameters override matching path parameters", () => { + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: { + openapi: "3.1.0", + paths: { + "/test": { + parameters: [{ name: "limit", in: "query", schema: { type: "string" } }], + get: { + operationId: "test", + parameters: [{ name: "limit", in: "query", required: true, schema: { type: "number" } }], + responses: { 200: { description: "Success" } }, + }, + }, + }, + }, + }).tools, + "test", + ) + + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + expect(inputTypeScript(tool)).toBe("{ limit: number }") + }) + + test("normalizes OpenAPI 3.0 schemas with Effect", () => { + const result = OpenAPI.fromSpec({ + baseUrl, + spec: { + openapi: "3.0.3", + paths: { + "/search": { + get: { + operationId: "search", + parameters: [ + { + in: "query", + name: "value", + schema: { type: "string", nullable: true, minLength: 2 }, + }, + ], + responses: { 200: { description: "Success" } }, + }, + }, + }, + }, + }) + const search = toolAt(result.tools, "search") + + expect(Tool.isDefinition(search)).toBe(true) + if (!Tool.isDefinition(search)) throw new Error("search was not generated") + expect(inputTypeScript(search)).toBe("{ value?: string | null }") + const schema: unknown = search.input + const input = isRecord(schema) ? schema : {} + const properties = isRecord(input.properties) ? input.properties : {} + const value = isRecord(properties.value) ? properties.value : {} + expect(value.minLength).toBe(2) + }) + + test("preserves schema-local definitions alongside component definitions", () => { + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: { + openapi: "3.1.0", + paths: { + "/test": { + get: { + operationId: "test", + responses: { + 200: { + description: "Success", + content: { + "application/json": { + schema: { $ref: "#/$defs/Local", $defs: { Local: { type: "string" } } }, + }, + }, + }, + }, + }, + }, + }, + components: { schemas: { Global: { type: "number" } } }, + }, + }).tools, + "test", + ) + + if (!Tool.isDefinition(tool) || !isRecord(tool.output)) throw new Error("test output was not generated") + expect(tool.output.$defs).toMatchObject({ Local: { type: "string" }, Global: { type: "number" } }) + }) + + test("documents that the opencode fixture is unauthenticated", async () => { + const spec = await opencodeSpec() + const components = isRecord(spec.components) ? spec.components : {} + const result = OpenAPI.fromSpec({ spec, baseUrl }) + + expect(spec.security).toStrictEqual([]) + expect(isRecord(components.securitySchemes) ? Object.keys(components.securitySchemes) : []).toStrictEqual([]) + const health = toolAt(result.tools, "v2.health.get") + const healthInput = isRecord(health) ? health.input : undefined + expect(healthInput).toMatchObject({ type: "object", properties: {} }) + const input = isRecord(healthInput) ? healthInput : {} + expect(Object.keys(isRecord(input.properties) ? input.properties : {})).toStrictEqual([]) + }) + + test("exposes real opencode operations through CodeMode discovery", async () => { + const { layer } = recordingClient(() => json({})) + const runtime = CodeMode.make({ + tools: { opencode: OpenAPI.fromSpec({ spec: await opencodeSpec(), baseUrl }).tools }, + }) + const result = await Effect.runPromise( + runtime + .execute( + ` + return await tools.$codemode.search({ query: "global health", namespace: "opencode", limit: 1 }) + `, + ) + .pipe(Effect.provide(layer)), + ) + + expect(result).toMatchObject({ ok: true }) + if (!result.ok) return + expect(result.value).toMatchObject({ + items: [ + { + path: "tools.opencode.v2.health.get", + description: "Check whether the API server is ready to accept requests.", + }, + ], + }) + expect(JSON.stringify(result.value)).toContain("healthy: true") + }) + + test("invokes real opencode path parameters and JSON request bodies", async () => { + const { requests, layer } = recordingClient((request) => { + if (request.method === "GET") return json({ id: "ses_123" }) + return json({ id: "ses_456" }) + }) + const runtime = CodeMode.make({ + tools: { opencode: OpenAPI.fromSpec({ spec: await opencodeSpec(), baseUrl }).tools }, + }) + + const result = await Effect.runPromise( + runtime + .execute( + ` + const existing = await tools.opencode.v2.session.get({ sessionID: "ses_123" }) + const created = await tools.opencode.v2.session.create({ id: "ses_456" }) + return { existing, created } + `, + ) + .pipe(Effect.provide(layer)), + ) + + expect(result).toMatchObject({ ok: true }) + expect(requests).toHaveLength(2) + expect(requests[0]).toMatchObject({ method: "GET", body: undefined }) + expect(new URL(requests[0]!.url).pathname).toBe("/api/session/ses_123") + expect(requests[1]).toMatchObject({ + method: "POST", + url: "http://localhost:4096/api/session", + body: { id: "ses_456" }, + }) + }) + + test("serializes deep-object query parameters from the opencode fixture", async () => { + const client = recordingClient(() => json({ directory: "/tmp" })) + const location = toolAt(OpenAPI.fromSpec({ spec: await opencodeSpec(), baseUrl }).tools, "v2.location.get") + if (!Tool.isDefinition(location)) throw new Error("v2.location.get was not generated") + + await Effect.runPromise( + location.run({ location: { directory: "/tmp", workspace: "workspace-1" } }).pipe(Effect.provide(client.layer)), + ) + + const url = new URL(client.requests[0]!.url) + expect(url.searchParams.get("location[directory]")).toBe("/tmp") + expect(url.searchParams.get("location[workspace]")).toBe("workspace-1") + }) + + test("serializes supported simple and form parameter shapes", async () => { + const client = recordingClient(() => json({ ok: true })) + const result = OpenAPI.fromSpec({ + baseUrl, + spec: { + openapi: "3.1.0", + paths: { + "/items/{keys}": { + get: { + operationId: "items", + parameters: [ + { name: "keys", in: "path", required: true, schema: { type: "array", items: { type: "string" } } }, + { name: "tags", in: "query", style: "form", explode: false, schema: { type: "array" } }, + { name: "filter", in: "query", style: "form", explode: true, schema: { type: "object" } }, + { name: "nullable", in: "query", required: true, schema: { type: ["string", "null"] } }, + { name: "constructor", in: "query", schema: { type: "string" } }, + { name: "meta", in: "header", style: "simple", explode: true, schema: { type: "object" } }, + ], + responses: { 200: { description: "Success" } }, + }, + }, + }, + }, + }) + const tool = toolAt(result.tools, "items") + if (!Tool.isDefinition(tool)) throw new Error("items was not generated") + + await Effect.runPromise( + tool + .run({ + keys: ["a!", "b*"], + tags: ["x", "y"], + filter: { state: "open", page: 2 }, + nullable: null, + constructor_2: "safe", + meta: { a: "b", c: "d" }, + }) + .pipe(Effect.provide(client.layer)), + ) + + const url = new URL(client.requests[0]!.url) + expect(url.pathname).toBe("/items/a%21,b%2A") + expect(url.searchParams.get("tags")).toBe("x,y") + expect(url.searchParams.get("state")).toBe("open") + expect(url.searchParams.get("page")).toBe("2") + expect(url.searchParams.get("nullable")).toBe("null") + expect(url.searchParams.get("constructor")).toBe("safe") + expect(client.requests[0]!.headers.meta).toBe("a=b,c=d") + await expect(Effect.runPromise(tool.run({ keys: [undefined] }).pipe(Effect.provide(client.layer)))).rejects.toThrow( + "unsupported nested value", + ) + }) + + test("skips unsupported parameter encodings and malformed security", () => { + const result = OpenAPI.fromSpec({ + baseUrl, + spec: { + openapi: "3.1.0", + security: [{ bearer: [] }], + paths: { + "/cookie": { + get: { + operationId: "cookie", + parameters: [{ name: "session", in: "cookie", schema: { type: "string" } }], + responses: { 200: { description: "Success" } }, + }, + }, + "/reserved": { + get: { + operationId: "reserved", + parameters: [{ name: "query", in: "query", allowReserved: true, schema: { type: "string" } }], + responses: { 200: { description: "Success" } }, + }, + }, + "/invalid-style": { + get: { + operationId: "invalidStyle", + parameters: [{ name: "query", in: "query", style: 42, schema: { type: "string" } }], + responses: { 200: { description: "Success" } }, + }, + }, + "/security": { + get: { operationId: "security", security: null, responses: { 200: { description: "Success" } } }, + }, + }, + }, + }) + + expect(result.tools).toEqual({}) + expect(result.skipped.map((item) => item.reason)).toEqual([ + "cookie parameter 'session' is not supported", + "parameter 'query' uses unsupported allowReserved encoding", + "parameter 'query' has an invalid style", + "security declaration is not an array", + ]) + }) + + test("fails closed on prototype-named missing security schemes", () => { + const result = OpenAPI.fromSpec({ + baseUrl, + spec: singleOperation({ security: [JSON.parse('{"__proto__":[]}')] }), + }) + + expect(result.tools).toEqual({}) + expect(result.skipped[0]?.reason).toBe("security requirement references missing or malformed scheme: __proto__") + }) + + test("resolves bearer authentication without exposing it as input", async () => { + const contexts: Array[0]> = [] + const client = recordingClient(() => json({ ok: true })) + const spec = { + ...singleOperation({ operationId: undefined }), + security: [{ bearer: [] }], + components: { securitySchemes: { bearer: { type: "http", scheme: "bearer" } } }, + } satisfies Document + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec, + auth: { + resolve: (context) => { + contexts.push(context) + return Effect.succeed({ type: "bearer", token: "secret" }) + }, + }, + }).tools, + "getTest", + ) + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + + await Effect.runPromise(tool.run({}).pipe(Effect.provide(client.layer))) + + expect(inputTypeScript(tool)).toBe("{}") + expect(client.requests[0]!.headers.authorization).toBe("Bearer secret") + expect(contexts).toEqual([ + { + name: "bearer", + definition: { type: "http", scheme: "bearer" }, + scopes: [], + operation: { + operationId: undefined, + method: "GET", + path: "/test", + summary: undefined, + description: undefined, + }, + }, + ]) + }) + + test("applies authentication carriers without prototype or collision loss", async () => { + const client = recordingClient(() => json({ ok: true })) + const authenticated = ( + security: ReadonlyArray>>, + schemes: Record, + ) => + OpenAPI.fromSpec({ + baseUrl, + spec: { ...singleOperation({}), security, components: { securitySchemes: schemes } }, + auth: { resolve: () => Effect.succeed({ type: "apiKey", value: "secret" }) }, + }) + const prototype = toolAt( + authenticated([{ key: [] }], { key: { type: "apiKey", in: "query", name: "__proto__" } }).tools, + "test", + ) + if (!Tool.isDefinition(prototype)) throw new Error("prototype auth tool was not generated") + + await Effect.runPromise(prototype.run({}).pipe(Effect.provide(client.layer))) + expect(new URL(client.requests[0]!.url).searchParams.get("__proto__")).toBe("secret") + + const duplicate = toolAt( + authenticated([{ first: [], second: [] }], { + first: { type: "apiKey", in: "header", name: "x-key" }, + second: { type: "apiKey", in: "header", name: "x-key" }, + }).tools, + "test", + ) + if (!Tool.isDefinition(duplicate)) throw new Error("duplicate auth tool was not generated") + await expect(Effect.runPromise(duplicate.run({}).pipe(Effect.provide(client.layer)))).rejects.toThrow( + "multiple credentials", + ) + + const cookie = authenticated([{ key: [] }], { key: { type: "apiKey", in: "cookie", name: "session" } }) + expect(cookie.tools).toEqual({}) + expect(cookie.skipped[0]?.reason).toBe("cookie authentication 'key' is not supported") + + const alternative = OpenAPI.fromSpec({ + baseUrl, + spec: { + ...singleOperation({}), + security: [{ cookie: [] }, { bearer: [] }], + components: { + securitySchemes: { + cookie: { type: "apiKey", in: "cookie", name: "session" }, + bearer: { type: "http", scheme: "bearer" }, + }, + }, + }, + auth: { + resolve: ({ name }) => Effect.succeed(name === "bearer" ? { type: "bearer", token: "secret" } : undefined), + }, + }) + const alternativeTool = toolAt(alternative.tools, "test") + if (!Tool.isDefinition(alternativeTool)) throw new Error("supported auth alternative was not generated") + await Effect.runPromise(alternativeTool.run({}).pipe(Effect.provide(client.layer))) + expect(client.requests.at(-1)?.headers.authorization).toBe("Bearer secret") + }) + + test("honors server precedence and rejects ambiguous base URLs", async () => { + const client = recordingClient(() => json({ ok: true })) + const spec = { + ...singleOperation({ servers: [{ url: "https://operation.example/v1" }] }), + servers: [{ url: "https://document.example" }], + } satisfies Document + const tool = toolAt(OpenAPI.fromSpec({ spec }).tools, "test") + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + + await Effect.runPromise(tool.run({}).pipe(Effect.provide(client.layer))) + expect(client.requests[0]?.url).toBe("https://operation.example/v1/test") + + const invalid = OpenAPI.fromSpec({ spec, baseUrl: "https://example.com/api?tenant=one" }) + expect(invalid.tools).toEqual({}) + expect(invalid.skipped[0]?.reason).toContain("unsupported query string or fragment") + + const malformed = OpenAPI.fromSpec({ spec, baseUrl: "https:/example.com" }) + expect(malformed.tools).toEqual({}) + expect(malformed.skipped[0]?.reason).toContain("not an absolute HTTP(S) URL") + }) + + test("resolves chained response refs before detecting unsupported transports", () => { + const result = OpenAPI.fromSpec({ + baseUrl, + spec: { + ...singleOperation({ responses: { 200: { $ref: "#/components/responses/First" } } }), + components: { + responses: { + First: { $ref: "#/components/responses/Stream" }, + Stream: { content: { "text/event-stream": { schema: { type: "string" } } } }, + }, + }, + }, + }) + + expect(result.tools).toEqual({}) + expect(result.skipped[0]?.reason).toBe("SSE operations are not supported") + }) + + test("resolves response schemas before detecting binary output", () => { + const result = OpenAPI.fromSpec({ + baseUrl, + spec: { + ...singleOperation({ + responses: { + 200: { + content: { "text/plain": { schema: { $ref: "#/components/schemas/File" } } }, + }, + }, + }), + components: { schemas: { File: { type: "string", format: "binary" } } }, + }, + }) + + expect(result.tools).toEqual({}) + expect(result.skipped[0]?.reason).toBe("binary responses are not supported") + }) + + test("validates composite parameters before resolving auth", async () => { + const resolutions: Array = [] + const client = recordingClient(() => json({ ok: true })) + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: { + ...singleOperation({ + parameters: [{ name: "filter", in: "query", style: "form", explode: true, schema: { type: "object" } }], + }), + security: [{ bearer: [] }], + components: { securitySchemes: { bearer: { type: "http", scheme: "bearer" } } }, + }, + auth: { + resolve: ({ name }) => { + resolutions.push(name) + return Effect.succeed({ type: "bearer", token: "secret" }) + }, + }, + }).tools, + "test", + ) + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + + await expect( + Effect.runPromise(tool.run({ filter: { value: undefined } }).pipe(Effect.provide(client.layer))), + ).rejects.toThrow("unsupported nested value") + expect(resolutions).toEqual([]) + expect(client.requests).toEqual([]) + }) + + test("preserves JSON media types and rejects unencodable bodies", async () => { + const client = recordingClient(() => json({ ok: true })) + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: singleOperation( + { + requestBody: { + required: true, + content: { "application/merge-patch+json": { schema: { type: "object" } } }, + }, + }, + "post", + ), + }).tools, + "test", + ) + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + + await Effect.runPromise(tool.run({ body: { name: "updated" } }).pipe(Effect.provide(client.layer))) + expect(client.requests[0]!.headers["content-type"]).toBe("application/merge-patch+json") + const cyclic: Record = {} + cyclic.self = cyclic + await expect(Effect.runPromise(tool.run({ body: cyclic }).pipe(Effect.provide(client.layer)))).rejects.toThrow( + "Invalid JSON body", + ) + }) + + test("rejects oversized and malformed JSON responses", async () => { + const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec: singleOperation({}) }).tools, "test") + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + const oversized = recordingClient( + () => new Response(null, { headers: { "content-length": String(50 * 1024 * 1024 + 1) } }), + ) + const malformed = recordingClient(() => new Response("{", { headers: { "content-type": "application/json" } })) + const chunked = recordingClient(() => new Response(new Uint8Array(50 * 1024 * 1024 + 1))) + + await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(oversized.layer)))).rejects.toThrow( + "response exceeds 50 MiB", + ) + await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(malformed.layer)))).rejects.toThrow( + "returned malformed JSON", + ) + await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(chunked.layer)))).rejects.toThrow( + "response exceeds 50 MiB", + ) + }) + + test("keeps non-JSON responses raw and unions every success output", async () => { + const spec = singleOperation({ + responses: { + 200: { description: "Text", content: { "text/plain": { schema: { type: "string" } } } }, + 204: { description: "Empty" }, + }, + }) + const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec }).tools, "test") + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + const client = recordingClient(() => new Response("123", { headers: { "content-type": "text/plain" } })) + + expect(outputTypeScript(tool)).toBe("string | null") + await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(client.layer)))).resolves.toBe("123") + }) + + test("fails missing required parameters before auth and network", async () => { + const { requests, layer } = recordingClient(() => json({})) + const runtime = CodeMode.make({ + tools: { opencode: OpenAPI.fromSpec({ spec: await opencodeSpec(), baseUrl }).tools }, + }) + + const result = await Effect.runPromise( + runtime.execute("return await tools.opencode.v2.session.get({})").pipe(Effect.provide(layer)), + ) + + expect(result).toMatchObject({ ok: false }) + expect(JSON.stringify(result)).toContain("Missing required path parameter 'sessionID'") + expect(requests).toHaveLength(0) + }) + + test("prefixes cross-location collisions and reconstructs the HTTP request", async () => { + const spec = { + openapi: "3.1.0", + info: { title: "collision", version: "1.0.0" }, + paths: { + "/echo": { + post: { + operationId: "echo", + requestBody: { + required: true, + content: { "application/json": { schema: { type: "string" } } }, + }, + responses: { "204": { description: "Echoed" } }, + }, + }, + "/things/{id}": { + post: { + operationId: "things.update", + parameters: [ + { name: "id", in: "path", required: true, schema: { type: "string" } }, + { name: "id", in: "query", required: true, schema: { type: "string" } }, + { name: "path_id", in: "query", schema: { type: "string" } }, + { name: "id", in: "header", required: true, schema: { type: "string" } }, + ], + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + properties: { id: { type: "string" } }, + required: ["id"], + additionalProperties: false, + }, + }, + }, + }, + responses: { "204": { description: "Updated" } }, + }, + }, + }, + } satisfies Document + const { requests, layer } = recordingClient(() => new Response(null, { status: 204 })) + const tools = OpenAPI.fromSpec({ spec, baseUrl }).tools + const update = toolAt(tools, "things.update") + const echo = toolAt(tools, "echo") + + expect(Tool.isDefinition(update)).toBe(true) + if (!Tool.isDefinition(update)) throw new Error("things.update was not generated") + expect(inputTypeScript(update)).toBe( + "{ path_id: string; query_id: string; path_id_2?: string; header_id: string; body_id: string }", + ) + expect(Tool.isDefinition(echo)).toBe(true) + if (!Tool.isDefinition(echo)) throw new Error("echo was not generated") + expect(inputTypeScript(echo)).toBe("{ body: string }") + + const runtime = CodeMode.make({ tools }) + const result = await Effect.runPromise( + runtime + .execute( + ` + const updated = await tools.things.update({ path_id: "path", query_id: "query", path_id_2: "literal", header_id: "header", body_id: "body" }) + const echoed = await tools.echo({ body: "hello" }) + return { updated, echoed } + `, + ) + .pipe(Effect.provide(layer)), + ) + + expect(result).toMatchObject({ ok: true }) + expect(requests).toHaveLength(2) + expect(new URL(requests[0]!.url).pathname).toBe("/things/path") + expect(new URL(requests[0]!.url).searchParams.get("id")).toBe("query") + expect(new URL(requests[0]!.url).searchParams.get("path_id")).toBe("literal") + expect(requests[0]!.headers.id).toBe("header") + expect(requests[0]!.body).toStrictEqual({ id: "body" }) + expect(requests[1]!.body).toBe("hello") + }) + + test("keeps bodies nested when flattening would lose schema semantics", () => { + const body = (schema: Record, required = true) => ({ + required, + content: { "application/json": { schema } }, + }) + const spec = { + openapi: "3.1.0", + info: { title: "bodies", version: "1.0.0" }, + paths: Object.fromEntries( + [ + [ + "optional", + body( + { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + additionalProperties: false, + }, + false, + ), + ], + ["dictionary", body({ type: "object", additionalProperties: { type: "string" } })], + [ + "composed", + body({ + type: "object", + allOf: [{ type: "object", properties: { name: { type: "string" } }, required: ["name"] }], + additionalProperties: false, + }), + ], + [ + "nullable", + body({ + type: ["object", "null"], + properties: { name: { type: "string" } }, + additionalProperties: false, + }), + ], + ].map(([name, requestBody]) => [ + `/body/${name}`, + { + post: { + operationId: `body.${name}`, + requestBody, + responses: { "204": { description: "Accepted" } }, + }, + }, + ]), + ), + } satisfies Document + const tools = OpenAPI.fromSpec({ spec, baseUrl }).tools + + for (const name of ["optional", "dictionary", "composed", "nullable"]) { + const tool = toolAt(tools, `body.${name}`) + expect(Tool.isDefinition(tool)).toBe(true) + if (!Tool.isDefinition(tool)) throw new Error(`body.${name} was not generated`) + const input = isRecord(tool.input) ? tool.input : {} + expect(Object.keys(isRecord(input.properties) ? input.properties : {})).toStrictEqual(["body"]) + } + const optional = toolAt(tools, "body.optional") + if (!Tool.isDefinition(optional)) throw new Error("body.optional was not generated") + expect(inputTypeScript(optional)).toBe("{ body?: { name: string } }") + }) +}) diff --git a/packages/codemode/test/parity.test.ts b/packages/codemode/test/parity.test.ts new file mode 100644 index 00000000000..dfa85831837 --- /dev/null +++ b/packages/codemode/test/parity.test.ts @@ -0,0 +1,425 @@ +import { describe, expect, test } from "bun:test" +import { Effect } from "effect" +import { CodeMode } from "../src/index.js" +import { ToolRuntime } from "../src/tool-runtime.js" + +// Runs a CodeMode program with no host tools and returns the CodeMode.Result. These tests pin the +// JS-parity behaviors for the "99% of ordinary defensive JavaScript just works" goal: cases where +// 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 +// `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) => { + 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 +} + +describe("H2: string property access reads as undefined (not a throw)", () => { + test("unknown property on a string is undefined", async () => { + expect(await value(`const s = "hi"; return s.login === undefined`)).toBe(true) + expect(await value(`const s = "hi"; return s.login`)).toBeNull() + }) + + test("optional chaining + fallback on a string does not throw", async () => { + expect(await value(`const s = "hi"; return s?.login ?? "fallback"`)).toBe("fallback") + }) + + test("the real MCP pattern: result is a JSON string, defensive read falls through", async () => { + // me.result is a string; me.result?.login is undefined, so we fall back to the raw string. + expect(await value(`const me = { result: '{"login":"x"}' }; return me.result?.login ?? me.result`)).toBe( + '{"login":"x"}', + ) + }) + + 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)", () => { + test("unknown property on an array is undefined", async () => { + expect(await value(`return [1,2,3].foo === undefined`)).toBe(true) + expect(await value(`return [1,2,3].foo`)).toBeNull() + }) + + test("optional chaining on an array does not throw", async () => { + expect(await value(`return [1,2,3]?.foo ?? "fb"`)).toBe("fb") + }) + + test("unknown property reads stay undefined for methods CodeMode does not implement", async () => { + 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]) + expect(await value(`return [1,2,3][9] === undefined`)).toBe(true) + expect(await value(`return [1,2,3][9]`)).toBeNull() + }) +}) + +describe("H6: object spread of null/undefined is a no-op", () => { + test("spreading null is a no-op", async () => { + expect(await value(`const o = null; return { ...o, a: 1 }`)).toEqual({ a: 1 }) + }) + + test("spreading an absent argument merges cleanly", async () => { + expect(await value(`function f(opts){ return { ...opts, a: 1 } } return f(undefined)`)).toEqual({ a: 1 }) + }) + + test("spreading a real object still works", async () => { + expect(await value(`const o = { a: 1 }; return { ...o, b: 2 }`)).toEqual({ a: 1, b: 2 }) + }) + + test("spreading an array into an object still errors", async () => { + const err = await error(`return { ...[1,2], a: 1 }`) + expect(err.kind).toBe("InvalidDataValue") + }) +}) + +describe("H4: typeof on an undeclared identifier is 'undefined'", () => { + test("feature-detection guard does not throw", async () => { + expect(await value(`return typeof foo === "undefined" ? "safe" : "no"`)).toBe("safe") + }) + + test("typeof of a declared binding is unaffected", async () => { + expect(await value(`const x = 5; return typeof x`)).toBe("number") + expect(await value(`const s = "a"; return typeof s`)).toBe("string") + }) + + test("referencing an undeclared identifier outside typeof still throws", async () => { + const err = await error(`return foo + 1`) + expect(err.message).toContain("foo") + }) +}) + +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) + expect(await value(`const x = Number("abc"); return Number.isNaN(x) ? 0 : x`)).toBe(0) + expect(await value(`const o = {}; o.count = (o.count || 0) + 1; return o.count`)).toBe(1) + // average of an empty list, guarded - the classic divide-by-zero that used to throw pre-guard + 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 () => { + expect(await value(`return 5/0`)).toBeNull() + expect(await value(`return 0/0`)).toBeNull() + expect(await value(`return Math.max()`)).toBeNull() + // nested, too - normalization walks the returned structure + 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 () => { + 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 + 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] }) + }) +}) + +describe("Error values and instanceof", () => { + test("new Error carries name/message and is instanceof Error", async () => { + expect(await value(`const e = new Error("boom"); return [e instanceof Error, e.name, e.message]`)).toEqual([ + true, + "Error", + "boom", + ]) + }) + + test("Error without new behaves like new Error", async () => { + expect(await value(`const e = Error("plain"); return [e instanceof Error, e.name, e.message]`)).toEqual([ + true, + "Error", + "plain", + ]) + expect(await value(`const e = new Error(); return [e.name, e.message, e instanceof Error]`)).toEqual([ + "Error", + "", + true, + ]) + }) + + test("specific error types are instanceof themselves and Error, not each other", async () => { + expect( + await value( + `const e = new TypeError("t"); return [e instanceof TypeError, e instanceof Error, e instanceof RangeError]`, + ), + ).toEqual([true, true, false]) + expect(await value(`return new Error("e") instanceof TypeError`)).toBe(false) + }) + + test("thrown errors keep instanceof through try/catch", async () => { + expect(await value(`try { throw new Error("x") } catch (e) { return [e instanceof Error, e.message] }`)).toEqual([ + true, + "x", + ]) + }) + + test("interpreter runtime failures are caught as Error values", async () => { + expect(await value(`try { JSON.parse("nope") } catch (e) { return e instanceof Error }`)).toBe(true) + expect(await value(`try { undeclared() } catch (e) { return e instanceof Error }`)).toBe(true) + }) + + test("caught failures carry the constructor name the real-JS failure would have", async () => { + // JSON.parse throws SyntaxError: name and specific-instanceof both carry through, and the + // message keeps the engine's position detail. + expect( + await value(` + try { JSON.parse("{oops") } catch (e) { + return [e.name, e instanceof SyntaxError, e instanceof Error, e instanceof TypeError, e.message.includes("JSON")] + } + `), + ).toEqual(["SyntaxError", true, true, false, true]) + expect(await value(`try { undeclared() } catch (e) { return [e.name, e instanceof ReferenceError] }`)).toEqual([ + "ReferenceError", + true, + ]) + expect(await value(`try { const c = 1; c = 2 } catch (e) { return [e.name, e instanceof TypeError] }`)).toEqual([ + "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, + ]) + expect(await value(`try { new RegExp("(") } catch (e) { return [e.name, e instanceof SyntaxError] }`)).toEqual([ + "SyntaxError", + true, + ]) + }) + + test("diagnostics without a specific real-JS analogue are named plain Error", async () => { + expect(await value(`try { JSON.parse(5) } catch (e) { return [e.name, e instanceof Error] }`)).toEqual([ + "Error", + true, + ]) + }) + + test("Promise.allSettled rejection reasons are Error values", async () => { + expect( + await value(` + const settled = await Promise.allSettled([Promise.reject(new Error("b"))]) + return [settled[0].reason instanceof Error, settled[0].reason.message] + `), + ).toEqual([true, "b"]) + }) + + test("non-error thrown values are not instanceof Error", async () => { + expect(await value(`try { throw "raw" } catch (e) { return e instanceof Error }`)).toBe(false) + expect(await value(`try { throw { message: "shaped" } } catch (e) { return e instanceof Error }`)).toBe(false) + }) + + test("plain data is never instanceof Error", async () => { + expect(await value(`return [({}) instanceof Error, "s" instanceof Error, null instanceof Error]`)).toEqual([ + false, + false, + false, + ]) + }) + + test("error values still serialize as plain { name, message } data", async () => { + expect(await value(`return new Error("m")`)).toEqual({ name: "Error", message: "m" }) + expect(await value(`return JSON.stringify(new Error("m"))`)).toBe('{"name":"Error","message":"m"}') + expect(await value(`try { throw new Error("m") } catch (e) { return Object.keys(e) }`)).toEqual(["name", "message"]) + }) + + test("spreading an error loses the brand, like losing the prototype in JS", async () => { + expect(await value(`const e = new Error("m"); return ({ ...e }) instanceof Error`)).toBe(false) + expect(await value(`const e = new Error("m"); return { ...e }`)).toEqual({ name: "Error", message: "m" }) + }) + + test("typeof Error is function; an unknown instanceof right-hand side is a catchable error", async () => { + expect(await value(`return typeof Error`)).toBe("function") + expect(await value(`try { return 1 instanceof 5 } catch (e) { return "caught" }`)).toBe("caught") + const err = await error(`return 1 instanceof 5`) + expect(err.message).toContain("right-hand side of '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], + }) + }) + + test("splice inserts new elements at the cut", 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], + a: [1, "x", 3], + }) + }) + + 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"]) + expect( + await value(` + const out = [] + for (const [index, item] of ["a","b"].entries()) out.push(index + ":" + item) + return out + `), + ).toEqual(["0:a", "1:b"]) + expect(await value(`return [...[7].entries()]`)).toEqual([[0, 7]]) + }) +}) + +describe("string methods: localeCompare, normalize, trim aliases", () => { + 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") + }) +}) + +describe("compound assignment matches its binary operator", () => { + // `x op= y` must behave exactly like `x = x op y`, sharing the binary operator's coercion + // semantics (Dates string-coerce for `+` and use their time value for arithmetic; data + // objects/arrays coerce to their JS string form). + const pair = async (compound: string, expanded: string) => { + const [a, b] = await Promise.all([value(compound), value(expanded)]) + expect(a).toEqual(b) + return a + } + + test("sandbox 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 () => { + expect( + await pair(`let d = new Date(1000); d -= 400; return d`, `let d = new Date(1000); d = d - 400; return d`), + ).toBe(600) + expect(await pair(`let d = new Date(1000); d /= 4; return d`, `let d = new Date(1000); d = d / 4; return d`)).toBe( + 250, + ) + }) + + test("string += object/array matches x = x + obj", async () => { + expect(await pair(`let x = "a"; x += { b: 1 }; return x`, `let x = "a"; x = x + { b: 1 }; return x`)).toBe( + "a[object Object]", + ) + expect(await pair(`let x = "a"; x += [1, 2]; return x`, `let x = "a"; x = x + [1, 2]; return x`)).toBe("a1,2") + }) + + test("compound assignment through a member target coerces the same way", async () => { + expect( + await pair( + `const o = { s: "t" }; o.s += new Date(0); return o.s`, + `const o = { s: "t" }; o.s = o.s + new Date(0); return o.s`, + ), + ).toBe("t1970-01-01T00:00:00.000Z") + }) + + test("numeric and string compound operators sweep identically to their expansions", async () => { + const cases: Array<[string, number | string]> = [ + [`let x = 7; x += 3; return x`, 7 + 3], + [`let x = 7; x -= 3; return x`, 7 - 3], + [`let x = 7; x *= 3; return x`, 7 * 3], + [`let x = 7; x /= 2; return x`, 7 / 2], + [`let x = 7; x %= 3; return x`, 7 % 3], + [`let x = 7; x **= 2; return x`, 7 ** 2], + [`let x = 7; x &= 3; return x`, 7 & 3], + [`let x = 7; x |= 8; return x`, 7 | 8], + [`let x = 7; x ^= 2; return x`, 7 ^ 2], + [`let x = 7; x <<= 2; return x`, 7 << 2], + [`let x = -7; x >>= 1; return x`, -7 >> 1], + [`let x = -7; x >>>= 1; return x`, -7 >>> 1], + [`let x = "a"; x += "b"; return x`, "ab"], + ] + for (const [compound, expected] of cases) { + expect(await value(compound)).toBe(expected) + expect(await value(compound.replace(/x (\S+)= /, (_, op) => `x = x ${op} `))).toBe(expected) + } + }) +}) + +describe("H5: builtin coercion functions work as array callbacks", () => { + test("filter(Boolean) drops falsy values", async () => { + expect(await value(`return [0, 1, "", 2, null, 3].filter(Boolean)`)).toEqual([1, 2, 3]) + }) + + test("map(String) coerces each element", async () => { + 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") + }) +}) diff --git a/packages/codemode/test/promise.test.ts b/packages/codemode/test/promise.test.ts new file mode 100644 index 00000000000..545d463abfa --- /dev/null +++ b/packages/codemode/test/promise.test.ts @@ -0,0 +1,456 @@ +import { describe, expect, test } from "bun:test" +import { 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. + +type Trace = { + starts: Array + active: number + maxActive: number + completed: number + interrupted: number +} + +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) => + Tool.make({ + description: "Echo an id after a delay", + input: Schema.Struct({ id: Schema.Number, ms: Schema.optionalKey(Schema.Number) }), + output: Schema.Number, + run: ({ id, ms }) => + Effect.gen(function* () { + trace.starts.push(id) + trace.active += 1 + trace.maxActive = Math.max(trace.maxActive, trace.active) + yield* Effect.sleep(ms ?? 20) + trace.active -= 1 + trace.completed += 1 + return id + }).pipe( + Effect.onInterrupt(() => + Effect.sync(() => { + trace.active -= 1 + trace.interrupted += 1 + }), + ), + ), + }) + +const failingTool = Tool.make({ + description: "Always refuse", + input: Schema.Struct({}), + output: Schema.String, + run: () => Effect.fail(toolError("Lookup refused")), +}) + +const run = ( + code: string, + options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {}, +): Promise => { + const trace = options.trace ?? makeTrace() + return Effect.runPromise( + CodeMode.execute({ + tools: { host: { sleepy: sleepyTool(trace), fail: failingTool } }, + code, + ...(options.limits ? { limits: options.limits } : {}), + }), + ) +} + +const value = async (code: string, options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {}) => { + const result = await run(code, options) + if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) + return result.value +} + +const error = async (code: string, options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {}) => { + const result = await run(code, options) + if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`) + return result.error +} + +describe("first-class promise values", () => { + 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 rb = await b + const ra = await a + return [ra, rb] + `, + { trace }, + ) + expect(result).toEqual([1, 2]) + expect(trace.starts).toEqual([1, 2]) + // Both calls overlapped even though they were awaited sequentially. + expect(trace.maxActive).toBeGreaterThan(1) + }) + + 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 x = await p + const y = await p + return [x, y] + `) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toEqual([7, 7]) + expect(result.toolCalls).toStrictEqual([{ name: "host.sleepy" }]) + }) + + test("await of a non-promise value is a passthrough no-op", 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() + expect(await value(`return (await [1, 2]).length`)).toBe(2) + }) + + 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) + }) + + test("typeof a promise is 'object', and console.log renders it sensibly", async () => { + const result = await run(` + const p = Promise.resolve(1) + console.log(p) + return typeof p + `) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.value).toBe("object") + expect(result.logs).toStrictEqual(["[Promise (await it to get its value)]"]) + }) + + test("an awaited failure is catchable exactly like a synchronous throw", async () => { + expect( + await value(` + const p = tools.host.fail({}) + try { + await p + return "no" + } catch (e) { + return e.message + } + `), + ).toBe("Lookup refused") + }) + + test("a fire-and-forget call completes before the execution ends", async () => { + const trace = makeTrace() + const result = await value( + ` + tools.host.sleepy({ id: 1, ms: 30 }) + return "done" + `, + { trace }, + ) + expect(result).toBe("done") + expect(trace.completed).toBe(1) + expect(trace.interrupted).toBe(0) + }) + + test("a never-awaited failing call surfaces as an unhandled-rejection diagnostic", async () => { + const diagnostic = await error(` + 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(...)") + }) +}) + +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 }) }`) + expect(diagnostic.kind).toBe("InvalidDataValue") + expect(diagnostic.message).toContain("un-awaited Promise") + expect(diagnostic.message).toContain("await tools.ns.tool(...)") + }) + + 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 }) })`) + expect(diagnostic.kind).toBe("InvalidDataValue") + expect(diagnostic.message).toContain("un-awaited Promise") + }) + + test("JSON.stringify of a promise is a diagnostic, not '{}'", async () => { + const diagnostic = await error(`return JSON.stringify(Promise.resolve(1))`) + expect(diagnostic.kind).toBe("InvalidDataValue") + expect(diagnostic.message).toContain("un-awaited Promise") + }) + + test("operators reject promise operands", async () => { + const diagnostic = await error(`return Promise.resolve(1) + 1`) + expect(diagnostic.kind).toBe("InvalidDataValue") + }) +}) + +describe("Promise.all over arbitrary arrays", () => { + 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]) + `), + ).toEqual([1, "plain", 2, 42]) + }) + + test("accepts arrays built beforehand, passed as identifiers, and spread elements", async () => { + expect( + await value(` + const calls = [] + calls.push(tools.host.sleepy({ id: 1 })) + calls.push(7) + const more = [tools.host.sleepy({ id: 2 })] + const batch = [...calls, ...more, "x"] + return await Promise.all(batch) + `), + ).toEqual([1, 7, 2, "x"]) + }) + + test("runs items.map tool calls in parallel", async () => { + const trace = makeTrace() + const result = await value( + ` + const ids = [1, 2, 3, 4] + return await Promise.all(ids.map((id) => tools.host.sleepy({ id, ms: 40 }))) + `, + { trace }, + ) + expect(result).toEqual([1, 2, 3, 4]) + // maxActive counts truly-overlapping live executions, so > 1 proves real + // parallelism deterministically - no wall-clock assertion needed. + expect(trace.maxActive).toBeGreaterThan(1) + }) + + test("caps live tool-call concurrency at the fixed internal constant (8)", 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 }))) + return results.length + `, + { trace }, + ) + expect(result).toBe(20) + expect(trace.maxActive).toBeGreaterThan(1) + expect(trace.maxActive).toBeLessThanOrEqual(8) + }) + + test("resolves the empty array", async () => { + expect(await value(`return await Promise.all([])`)).toEqual([]) + }) + + test("rejects with the first failure, catchable in-program", async () => { + expect( + await value(` + try { + await Promise.all([tools.host.sleepy({ id: 1 }), tools.host.fail({})]) + return "no" + } catch (e) { + return e.message + } + `), + ).toBe("Lookup refused") + }) + + test("a non-collection argument is a clear error", async () => { + const diagnostic = await error(`return await Promise.all(42)`) + expect(diagnostic.message).toContain("Promise.all expects an array") + }) + + 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 })])`, + { limits: { maxToolCalls: 2 } }, + ) + expect(diagnostic.kind).toBe("ToolCallLimitExceeded") + }) +}) + +describe("Promise.allSettled", () => { + test("reports fulfilled and rejected outcomes with catch-normalized reasons", async () => { + expect( + await value(` + return await Promise.allSettled([ + tools.host.sleepy({ id: 5 }), + tools.host.fail({}), + "plain", + Promise.reject(new Error("boom")), + ]) + `), + ).toEqual([ + { status: "fulfilled", value: 5 }, + { status: "rejected", reason: { name: "Error", message: "Lookup refused" } }, + { status: "fulfilled", value: "plain" }, + { status: "rejected", reason: { name: "Error", message: "boom" } }, + ]) + }) + + test("never rejects for program-level failures", async () => { + const result = await run(` + const settled = await Promise.allSettled([tools.host.fail({}), tools.host.fail({})]) + return settled.filter((s) => s.status === "rejected").length + `) + expect(result.ok).toBe(true) + if (result.ok) expect(result.value).toBe(2) + }) +}) + +describe("Promise.race", () => { + test("first settlement wins and losers are interrupted", 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 }) + return await Promise.race([fast, slow]) + `, + { trace }, + ) + expect(result).toBe(1) + expect(trace.interrupted).toBe(1) + expect(trace.completed).toBe(1) + }) + + test("awaiting an interrupted loser afterwards is a catchable program failure", async () => { + expect( + await value(` + const fast = tools.host.sleepy({ id: 1, ms: 10 }) + const slow = tools.host.sleepy({ id: 2, ms: 5000 }) + const winner = await Promise.race([fast, slow]) + try { + await slow + return "no" + } catch (e) { + return { winner, caught: e.message } + } + `), + ).toEqual({ + winner: 1, + caught: "This tool call was interrupted because another value settled a Promise.race first.", + }) + }) + + 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 })]) + return "no" + } catch (e) { + return e.message + } + `), + ).toBe("Lookup refused") + }) + + 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(trace.interrupted).toBe(1) + }) + + 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") + }) +}) + +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) + }) + + test("reject produces a promise whose await throws the reason", async () => { + expect( + await value(` + try { + await Promise.reject("nope") + return "no" + } catch (e) { + return e + } + `), + ).toBe("nope") + }) +}) + +describe("timeout interruption of forked calls", () => { + test("the execution timeout interrupts in-flight forked fibers", async () => { + 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 }) + return await a + `, + { trace, limits: { timeoutMs: 100 } }, + ) + expect(result.ok).toBe(false) + if (result.ok) return + expect(result.error.kind).toBe("TimeoutExceeded") + // Both calls started; neither escaped the timeout - the awaited one AND the abandoned one. + expect(trace.starts).toEqual([1, 2]) + expect(trace.interrupted).toBe(2) + expect(trace.completed).toBe(0) + }) + + 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 })])`, + { trace, limits: { timeoutMs: 100 } }, + ) + expect(result.ok).toBe(false) + if (result.ok) return + expect(result.error.kind).toBe("TimeoutExceeded") + expect(trace.interrupted).toBe(2) + }) +}) + +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`) + 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") + }) +}) diff --git a/packages/codemode/test/signature.test.ts b/packages/codemode/test/signature.test.ts new file mode 100644 index 00000000000..232c6dcb221 --- /dev/null +++ b/packages/codemode/test/signature.test.ts @@ -0,0 +1,449 @@ +import { describe, expect, test } from "bun:test" +import { Effect, Schema } from "effect" +import { CodeMode, Tool } from "../src/index.js" +import { inputTypeScript, jsonSchemaToTypeScript, outputTypeScript } from "../src/tool-schema.js" + +// A raw JSON Schema tool in the shape an MCP adapter produces: render-only input schema +// whose property descriptions and constraints must surface as JSDoc in pretty signatures. +const listIssues = Tool.make({ + description: "List issues in a repository", + input: { + type: "object", + properties: { + owner: { type: "string", description: "Repository owner" }, + after: { type: "string", description: "Cursor from the previous response's pageInfo" }, + perPage: { type: "number", description: "Results per page", default: 30 }, + labels: { type: "array", items: { type: "string" }, description: "Filter by labels", minItems: 1, maxItems: 10 }, + state: { type: "string", enum: ["open", "closed"] }, + }, + required: ["owner"], + }, + run: () => Effect.succeed("[]"), +}) + +// An Effect Schema tool whose field annotations must flow through the emitted JSON Schema. +const lookupOrder = Tool.make({ + description: "Look up an order", + input: Schema.Struct({ + id: Schema.String.annotate({ description: "Order identifier" }), + verbose: Schema.optionalKey(Schema.Boolean), + }), + output: Schema.Struct({ + status: Schema.String.annotate({ description: "Current order status" }), + }), + run: () => Effect.succeed({ status: "open" }), +}) + +describe("pretty signature rendering", () => { + test("described fields get JSDoc comments; undescribed and untagged fields get none", () => { + expect(inputTypeScript(listIssues, true)).toBe( + [ + "{", + " /** Repository owner */", + " owner: string,", + " /** Cursor from the previous response's pageInfo */", + " after?: string,", + " /**", + " * Results per page", + " * @default 30", + " */", + " perPage?: number,", + " /**", + " * Filter by labels", + " * @minItems 1", + " * @maxItems 10", + " */", + " labels?: Array,", + ' state?: "open" | "closed",', + "}", + ].join("\n"), + ) + }) + + test("compact mode output is unchanged by the pretty machinery", () => { + expect(inputTypeScript(listIssues)).toBe( + '{ owner: string; after?: string; perPage?: number; labels?: Array; state?: "open" | "closed" }', + ) + expect(inputTypeScript(lookupOrder)).toBe("{ id: string; verbose?: boolean }") + expect(outputTypeScript(lookupOrder)).toBe("{ status: string }") + }) + + test("nested objects recurse with increasing indent and their own JSDoc", () => { + const pretty = jsonSchemaToTypeScript( + { + type: "object", + properties: { + filter: { + type: "object", + description: "Search filter", + properties: { state: { type: "string", description: "Issue state" } }, + }, + }, + }, + true, + ) + expect(pretty).toBe( + [ + "{", + " /** Search filter */", + " filter?: {", + " /** Issue state */", + " state?: string,", + " },", + "}", + ].join("\n"), + ) + }) + + test("Effect Schema annotations become JSDoc on input and output fields", () => { + expect(inputTypeScript(lookupOrder, true)).toBe( + ["{", " /** Order identifier */", " id: string,", " verbose?: boolean,", "}"].join("\n"), + ) + expect(outputTypeScript(lookupOrder, true)).toBe( + ["{", " /** Current order status */", " status: string,", "}"].join("\n"), + ) + }) + + test("constraints TypeScript cannot express surface as JSDoc tags", () => { + const pretty = jsonSchemaToTypeScript( + { + type: "object", + properties: { + legacy: { type: "string", deprecated: true }, + homepage: { type: "string", format: "uri" }, + tags: { type: "array", items: { type: "string" }, minItems: 2, maxItems: 5, default: ["a", "b"] }, + }, + }, + true, + ) + expect(pretty).toContain(" /** @deprecated */\n legacy?: string") + expect(pretty).toContain(" /** @format uri */\n homepage?: string") + expect(pretty).toContain( + [ + " /**", + ' * @default ["a","b"]', + " * @minItems 2", + " * @maxItems 5", + " */", + " tags?: Array", + ].join("\n"), + ) + }) + + test("skips an unserializable default rather than emitting a broken tag", () => { + const pretty = jsonSchemaToTypeScript( + { type: "object", properties: { size: { type: "number", default: 1n } } }, + true, + ) + expect(pretty).toBe(["{", " size?: number,", "}"].join("\n")) + }) + + test("neutralizes */ inside descriptions so nothing closes the comment early", () => { + const pretty = jsonSchemaToTypeScript( + { type: "object", properties: { note: { type: "string", description: "Ends */ early" } } }, + true, + ) + expect(pretty).toContain(" /** Ends * / early */") + expect(pretty).not.toContain("Ends */") + }) + + test("multiline descriptions become *-prefixed blocks with blank edges trimmed", () => { + const pretty = jsonSchemaToTypeScript( + { + type: "object", + properties: { query: { type: "string", description: "\nFirst line\n\nSecond line\n" } }, + }, + true, + ) + expect(pretty).toBe( + ["{", " /**", " * First line", " *", " * Second line", " */", " query?: string,", "}"].join("\n"), + ) + }) + + test("stays total on cyclic $refs and pathological nesting in both modes", () => { + const cyclic = { + $ref: "#/$defs/Node", + $defs: { Node: { type: "object", properties: { child: { $ref: "#/$defs/Node" }, name: { type: "string" } } } }, + } as const + expect(jsonSchemaToTypeScript(cyclic)).toBe("{ child?: unknown; name?: string }") + expect(jsonSchemaToTypeScript(cyclic, true)).toContain("child?: unknown") + + let deep: Record = { type: "string" } + for (let level = 0; level < 12; level += 1) deep = { type: "object", properties: { next: deep } } + for (const pretty of [false, true]) { + const rendered = jsonSchemaToTypeScript(deep, pretty) + expect(rendered).toContain("unknown") + expect(rendered).toContain("next?:") + } + }) + + test("intersects ref and union siblings instead of discarding them", () => { + expect( + jsonSchemaToTypeScript({ + $ref: "#/$defs/User", + properties: { active: { type: "boolean" } }, + required: ["active"], + $defs: { + User: { type: "object", properties: { id: { type: "string" } }, required: ["id"] }, + }, + }), + ).toBe("{ id: string } & { active: boolean }") + expect( + jsonSchemaToTypeScript({ + type: "object", + properties: { common: { type: "boolean" } }, + required: ["common"], + anyOf: [ + { type: "object", properties: { name: { type: "string" } }, required: ["name"] }, + { type: "object", properties: { count: { type: "number" } }, required: ["count"] }, + ], + }), + ).toBe("({ name: string } | { count: number }) & { common: boolean }") + expect(jsonSchemaToTypeScript({ $ref: "https://example.com/schema.json" })).toBe("unknown") + expect( + jsonSchemaToTypeScript({ + $ref: "#/$defs/User/properties/id", + $defs: { User: { type: "object" }, id: { type: "string" } }, + }), + ).toBe("unknown") + expect( + jsonSchemaToTypeScript({ + type: ["object", "null"], + properties: { name: { type: "string" } }, + }), + ).toBe("{ name?: string } | null") + }) +}) + +describe("non-identifier property names render as quoted keys", () => { + // MCP-style schemas routinely carry property names that are not bare TS identifiers + // (`foo-bar`, `@type`, dotted names); the rendered signature must quote them so the + // model sees a valid TypeScript object type. Bare identifiers stay unquoted. + const rawSchema = { + type: "object", + properties: { + "foo-bar": { type: "string" }, + "@type": { type: "string" }, + "x.y": { type: "number", description: "Dotted name" }, + "123": { type: "number" }, + plain: { type: "boolean" }, + }, + required: ["@type"], + } as const + + test("compact rendering quotes non-identifier keys and leaves identifiers bare", () => { + expect(jsonSchemaToTypeScript(rawSchema)).toBe( + '{ "123"?: number; "foo-bar"?: string; "@type": string; "x.y"?: number; plain?: boolean }', + ) + }) + + test("pretty rendering quotes non-identifier keys and keeps their JSDoc", () => { + expect(jsonSchemaToTypeScript(rawSchema, true)).toBe( + [ + "{", + ' "123"?: number,', + ' "foo-bar"?: string,', + ' "@type": string,', + " /** Dotted name */", + ' "x.y"?: number,', + " plain?: boolean,", + "}", + ].join("\n"), + ) + }) + + test("JSON Schema input and output signatures of a tool both quote", () => { + const tool = Tool.make({ + description: "Adapter tool with awkward field names", + input: rawSchema, + output: { + type: "object", + properties: { "content-type": { type: "string" } }, + required: ["content-type"], + } as const, + run: () => Effect.succeed({ "content-type": "text/plain" }), + }) + expect(inputTypeScript(tool)).toContain('"foo-bar"?: string') + expect(outputTypeScript(tool)).toBe('{ "content-type": string }') + expect(outputTypeScript(tool, true)).toBe(["{", ' "content-type": string,', "}"].join("\n")) + }) + + test("Effect Schema structs with non-identifier field names quote too", () => { + const tool = Tool.make({ + description: "Schema tool with awkward field names", + input: Schema.Struct({ "foo-bar": Schema.String, plain: Schema.optionalKey(Schema.Number) }), + run: () => Effect.succeed(null), + }) + expect(inputTypeScript(tool)).toBe('{ "foo-bar": string; plain?: number }') + expect(inputTypeScript(tool, true)).toBe(["{", ' "foo-bar": string,', " plain?: number,", "}"].join("\n")) + }) +}) + +describe("union schemas render every alternative", () => { + test("anyOf with a number branch keeps sibling alternatives", () => { + const schema = { + anyOf: [{ type: "string" }, { type: "number" }], + } as const + expect(jsonSchemaToTypeScript(schema)).toBe("string | number") + expect(jsonSchemaToTypeScript(schema, true)).toBe("string | number") + }) + + test("nullable numeric unions keep null", () => { + const schema = { + oneOf: [{ type: "number" }, { type: "null" }], + } as const + expect(jsonSchemaToTypeScript(schema)).toBe("number | null") + expect(jsonSchemaToTypeScript(schema, true)).toBe("number | null") + }) + + test("tool input and output signatures preserve numeric unions", () => { + const tool = Tool.make({ + description: "Tool with numeric unions", + input: { + type: "object", + properties: { + value: { anyOf: [{ type: "string" }, { type: "number" }] }, + }, + } as const, + output: { anyOf: [{ type: "number" }, { type: "boolean" }] } as const, + run: () => Effect.succeed(1), + }) + expect(inputTypeScript(tool)).toBe("{ value?: string | number }") + expect(outputTypeScript(tool)).toBe("number | boolean") + }) + + test("allOf renders intersections with parenthesized union members", () => { + const schema = { + allOf: [{ type: "object", properties: { id: { type: "string" } } }, { type: ["string", "null"] }], + } as const + expect(jsonSchemaToTypeScript(schema)).toBe("{ id?: string } & (string | null)") + }) + + test("allOf does not discard an unresolved constraint", () => { + expect(jsonSchemaToTypeScript({ allOf: [{ type: "string" }, { $ref: "https://example.com/external.json" }] })).toBe( + "unknown", + ) + expect( + jsonSchemaToTypeScript({ + allOf: [{ type: "string" }, { allOf: [{ $ref: "https://example.com/external.json" }] }], + }), + ).toBe("unknown") + expect( + jsonSchemaToTypeScript({ + type: "string", + allOf: [{ $ref: "#/$defs/Constraint" }], + $defs: { Constraint: { description: "TypeScript-neutral constraint" } }, + }), + ).toBe("string") + }) +}) + +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)} })`), + ) + 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 } + } + + test("a raw JSON Schema (MCP-style) tool's result signature carries field JSDoc and tags", async () => { + const { items } = await search("list issues repository") + const item = items.find(({ path }) => path === "tools.github.list_issues")! + expect(item.signature).toBe( + [ + "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,", + " /**", + " * Filter by labels", + " * @minItems 1", + " * @maxItems 10", + " */", + " labels?: Array,", + ' state?: "open" | "closed",', + "}): Promise", + ].join("\n"), + ) + }) + + test("an annotated Effect Schema tool's result signature carries field JSDoc (exact-path lookup too)", async () => { + for (const query of ["look up order", "tools.orders.lookup"]) { + const { items } = await search(query) + const item = items.find(({ path }) => path === "tools.orders.lookup")! + expect(item.signature).toBe( + [ + "tools.orders.lookup(input: {", + " /** Order identifier */", + " id: string,", + " verbose?: boolean,", + "}): Promise<{", + " /** Current order status */", + " status: string,", + "}>", + ].join("\n"), + ) + } + }) + + test("the inline catalog uses the same JSDoc signatures", async () => { + const instructions = runtime.instructions() + const github = (await search("list issues repository")).items.find( + ({ path }) => path === "tools.github.list_issues", + )! + const orders = (await search("look up order")).items.find(({ path }) => path === "tools.orders.lookup")! + expect(instructions).toContain(` - ${github.signature} // List issues in a repository`) + expect(instructions).toContain(` - ${orders.signature} // Look up an order`) + expect(instructions).toContain("/** Repository owner */") + }) +}) + +describe("non-identifier tool paths", () => { + const resolveLibrary = Tool.make({ + description: "Resolve a Context7 library ID", + input: { + type: "object", + properties: { + query: { type: "string" }, + libraryName: { type: "string" }, + }, + required: ["query", "libraryName"], + } as const, + run: () => Effect.succeed("/reactjs/react.dev"), + }) + const runtime = CodeMode.make({ tools: { context7: { "resolve-library-id": resolveLibrary } } }) + + test("inline catalog uses bracket notation for dashed tool names", () => { + const instructions = runtime.instructions() + + expect(instructions).toContain( + 'tools.context7["resolve-library-id"](input: {\n query: string,\n libraryName: string,\n}): Promise', + ) + expect(instructions).toContain("Do not infer or normalize tool names") + expect(instructions).toContain("bracket notation and quotes are part of the path") + expect(instructions).not.toContain("tools.context7.resolve-library-id") + expect(instructions).not.toContain("tools.context7.resolve_library_id") + }) + + 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" })`), + ) + expect(result.ok).toBe(true) + if (!result.ok) throw new Error("search failed") + + const value = result.value as { items: Array<{ path: string; signature: string }> } + expect(value.items[0]?.path).toBe('tools.context7["resolve-library-id"]') + expect(value.items[0]?.signature).toContain('tools.context7["resolve-library-id"](input: {') + }) +}) diff --git a/packages/codemode/test/stdlib.test.ts b/packages/codemode/test/stdlib.test.ts new file mode 100644 index 00000000000..f7831a06033 --- /dev/null +++ b/packages/codemode/test/stdlib.test.ts @@ -0,0 +1,715 @@ +import { describe, expect, test } from "bun:test" +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 +// 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 -> {}. +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 +} + +describe("Date", () => { + test("Date.now() returns a number", async () => { + expect(await value(`return typeof Date.now()`)).toBe("number") + }) + + test("epoch construction and ISO rendering", async () => { + expect(await value(`return new Date(0).toISOString()`)).toBe("1970-01-01T00:00:00.000Z") + }) + + test("string parsing round-trips", async () => { + expect(await value(`return new Date("2024-01-02T03:04:05.000Z").getTime()`)).toBe(1704164645000) + expect(await value(`return Date.parse("2024-01-02T03:04:05.000Z")`)).toBe(1704164645000) + }) + + test("date arithmetic and comparison use the time value", async () => { + expect(await value(`const a = new Date(1000); const b = new Date(3000); return b - a`)).toBe(2000) + expect(await value(`const a = new Date(1000); const b = new Date(3000); return a < b`)).toBe(true) + expect(await value(`return +new Date(42)`)).toBe(42) + }) + + test("UTC getters read calendar components", async () => { + expect( + await value( + `const d = new Date("2024-03-05T06:07:08.009Z"); return [d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds()]`, + ), + ).toEqual([2024, 2, 5, 6, 7, 8, 9]) + }) + + test("invalid dates yield NaN times, guardable in-sandbox", async () => { + expect(await value(`return Number.isNaN(new Date("garbage").getTime())`)).toBe(true) + expect(await value(`return new Date("garbage").toJSON()`)).toBeNull() + }) + + test("toISOString on an invalid date is a catchable error", async () => { + expect(await value(`try { new Date("garbage").toISOString(); return "no" } catch { return "caught" }`)).toBe( + "caught", + ) + }) + + test("template interpolation renders the ISO form", async () => { + expect(await value("return `at ${new Date(0)}`")).toBe("at 1970-01-01T00:00:00.000Z") + }) + + test("dates serialize to ISO strings at the boundary, direct and nested", async () => { + expect(await value(`return new Date(0)`)).toBe("1970-01-01T00:00:00.000Z") + expect(await value(`return { when: new Date(0), tags: [new Date(1000)] }`)).toEqual({ + when: "1970-01-01T00:00:00.000Z", + tags: ["1970-01-01T00:00:01.000Z"], + }) + expect(await value(`return JSON.stringify({ d: new Date(0) })`)).toBe('{"d":"1970-01-01T00:00:00.000Z"}') + }) + + test("coercions: Number is the time, String is ISO, Boolean is true", async () => { + expect(await value(`return Number(new Date(5))`)).toBe(5) + expect(await value(`return String(new Date(0))`)).toBe("1970-01-01T00:00:00.000Z") + expect(await value(`return Boolean(new Date(0))`)).toBe(true) + }) + + test("sorting dates with a numeric comparator", async () => { + expect( + await value(` + const dates = [new Date(3000), new Date(1000), new Date(2000)] + return dates.sort((a, b) => a - b).map((d) => d.getTime()) + `), + ).toEqual([1000, 2000, 3000]) + }) + + test("new Date(year, month, day) accepts component form", async () => { + expect(await value(`const d = new Date(2024, 0, 2); return [d.getFullYear(), d.getMonth(), d.getDate()]`)).toEqual([ + 2024, 0, 2, + ]) + }) + + test("typeof and unknown properties are forgiving", async () => { + expect(await value(`return typeof new Date(0)`)).toBe("object") + expect(await value(`return new Date(0).nope === undefined`)).toBe(true) + }) +}) + +describe("RegExp", () => { + test("literal test", async () => { + expect(await value(`return /ab+c/.test("xabbbc")`)).toBe(true) + expect(await value(`return /ab+c/.test("nope")`)).toBe(false) + }) + + test("exec exposes captures and index", async () => { + expect(await value(`const m = /a(b+)/.exec("xxabbc"); return { full: m[0], group: m[1], index: m.index }`)).toEqual( + { + full: "abb", + group: "bb", + index: 2, + }, + ) + expect(await value(`return /a/.exec("zzz")`)).toBeNull() + }) + + test("named groups read through", async () => { + expect( + await value(`const m = /(?[a-z]+)-(?\\d+)/.exec("id ab-42"); return m.groups.word + m.groups.num`), + ).toBe("ab42") + }) + + test("global exec advances lastIndex across calls", async () => { + expect( + await value(` + const r = /\\d+/g + const first = r.exec("a1b22c") + const second = r.exec("a1b22c") + return [first[0], second[0]] + `), + ).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"]) + expect(await value(`return "abc".match(/\\d/)`)).toBeNull() + }) + + test("matchAll materializes match arrays with captures", async () => { + 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(` + const seen = [] + const output = "a1b22".replace(/(\\d)(\\d)?/g, (match, first, second, offset, input) => { + seen.push([match, first, second === undefined, offset, input]) + return Number(match) * 2 + }) + return { output, seen } + `), + ).toEqual({ + output: "a2b44", + seen: [ + ["1", "1", true, 1, "a1b22"], + ["22", "2", false, 3, "a1b22"], + ], + }) + expect( + await value(` + return "red-blue".replace( + /(?[a-z]+)-(?[a-z]+)/, + (match, left, right, offset, input, groups) => groups.right + ":" + groups.left, + ) + `), + ).toBe("blue:red") + }) + + test("function replacers support string searches, zero-length matches, and result coercion", async () => { + expect(await value(`return "banana".replace("na", (match, offset, input) => "[" + offset + "]")`)).toBe("ba[2]na") + expect(await value(`return "ab".replaceAll("", (match, offset) => offset)`)).toBe("0a1b2") + expect(await value(`return "😀".replaceAll(/(?:)/gu, (match, offset) => "[" + offset + "]")`)).toBe("[0]😀[2]") + expect( + await value(`return "123".replace(/\\d/g, (match) => match === "1" ? 7 : match === "2" ? null : { n: 3 })`), + ).toBe("7null[object Object]") + }) + + test("function replacers can await effectful tool calls", async () => { + const decorate = Tool.make({ + description: "Decorate a string", + input: Schema.String, + output: Schema.String, + run: (input) => Effect.succeed(`[${input}]`), + }) + const result = await Effect.runPromise( + CodeMode.execute({ + tools: { host: { decorate } }, + code: `return "a1b22".replace(/\\d+/g, async (match) => await tools.host.decorate(match))`, + }), + ) + expect(result.ok && result.value).toBe("a[1]b[22]") + + const missingAwait = await Effect.runPromise( + CodeMode.execute({ + tools: { host: { decorate } }, + code: `return "a1".replace(/\\d/, (match) => tools.host.decorate(match))`, + }), + ) + expect(!missingAwait.ok && missingAwait.error.kind).toBe("InvalidDataValue") + expect(!missingAwait.ok && missingAwait.error.message).toContain("un-awaited Promise") + }) + + test("replaceAll without the g flag is a catchable error", async () => { + 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") + expect(await value(`return [/a/ instanceof RegExp, /a/.source]`)).toEqual([true, "a"]) + }) + + test("invalid patterns fail with actionable messages", async () => { + const fromString = await error(`return "abc".match("(")`) + expect(fromString.message).toContain('String.match received the string "("') + expect(fromString.message).toContain("escape them with a backslash") + + const fromConstructor = await error(`return new RegExp("(")`) + expect(fromConstructor.message).toContain('new RegExp(...) received "("') + expect(fromConstructor.message).toContain("escape them with a backslash") + + const fromFlags = await error(`return new RegExp("a", "xz")`) + expect(fromFlags.message).toContain('invalid flags "xz"') + expect(fromFlags.message).toContain("Valid flags are") + }) + + test("missing g-flag errors say how to fix the call", async () => { + expect((await error(`return "aa".replaceAll(/a/, "b")`)).message).toContain("write /a/g, or use String.replace") + expect((await error(`return "aa".matchAll(/a/)`)).message).toContain("write /a/g, or use String.match") + }) + + test("a non-pattern argument names the expected shapes", async () => { + const err = await error(`return "abc".match(42)`) + expect(err.message).toContain("expects a regular expression") + expect(err.message).toContain("not number") + }) + + test("source and flags properties read through", async () => { + expect(await value(`const r = /ab/gi; return { source: r.source, flags: r.flags, global: r.global }`)).toEqual({ + source: "ab", + flags: "gi", + global: true, + }) + }) + + test("regexes serialize to {} at the boundary, like JSON", async () => { + expect(await value(`return /a/`)).toEqual({}) + expect(await value(`return JSON.stringify({ r: /a/g })`)).toBe('{"r":{}}') + }) + + test("template interpolation renders the literal form", async () => { + expect(await value("return `${/ab/g}`")).toBe("/ab/g") + }) +}) + +describe("URL and URI helpers", () => { + test("encodes and decodes complete URIs and URI components", async () => { + expect( + await value(` + return [ + encodeURI("https://example.test/a b?q=a/b"), + encodeURIComponent("a b/c?"), + decodeURI("https://example.test/a%20b?q=a/b"), + decodeURIComponent("a%20b%2Fc%3F"), + ["a b", "c/d"].map(encodeURIComponent), + ] + `), + ).toEqual([ + "https://example.test/a%20b?q=a/b", + "a%20b%2Fc%3F", + "https://example.test/a b?q=a/b", + "a b/c?", + ["a%20b", "c%2Fd"], + ]) + expect( + await value(`try { decodeURIComponent("%zz"); return false } catch (error) { return error instanceof URIError }`), + ).toBe(true) + }) + + test("resolves and mutates URLs with linked search parameters", async () => { + expect( + await value(` + const url = new URL("../users?id=old#top", "https://user:pass@example.com:8443/api/v1/") + url.pathname = "/items/a b" + url.searchParams.set("id", "a b") + url.searchParams.append("tag", "x/y") + url.hash = "part 1" + return { + href: url.href, + origin: url.origin, + host: url.host, + pathname: url.pathname, + search: url.search, + id: url.searchParams.get("id"), + string: String(url), + json: url.toJSON(), + instances: [ + url instanceof URL, + url.searchParams instanceof URLSearchParams, + url.searchParams === url.searchParams, + ], + } + `), + ).toEqual({ + href: "https://user:pass@example.com:8443/items/a%20b?id=a+b&tag=x%2Fy#part%201", + origin: "https://example.com:8443", + host: "example.com:8443", + pathname: "/items/a%20b", + search: "?id=a+b&tag=x%2Fy", + id: "a b", + string: "https://user:pass@example.com:8443/items/a%20b?id=a+b&tag=x%2Fy#part%201", + json: "https://user:pass@example.com:8443/items/a%20b?id=a+b&tag=x%2Fy#part%201", + instances: [true, true, true], + }) + }) + + test("URLSearchParams supports records, pairs, mutation, callbacks, and materialization", async () => { + expect( + await value(` + const params = new URLSearchParams([["tag", "b"], ["tag", "a"], ["q", "a b"]]) + const seen = [] + params.forEach((value, key) => seen.push(key + "=" + value)) + params.delete("tag", "b") + params.append("tag", "c") + params.sort() + return { + text: params.toString(), + size: params.size, + tags: params.getAll("tag"), + has: params.has("tag", "c"), + entries: Array.from(params), + object: Object.fromEntries(params), + record: new URLSearchParams({ page: 2, filter: "open" }).toString(), + seen, + } + `), + ).toEqual({ + text: "q=a+b&tag=a&tag=c", + size: 3, + tags: ["a", "c"], + has: true, + entries: [ + ["q", "a b"], + ["tag", "a"], + ["tag", "c"], + ], + object: { q: "a b", tag: "c" }, + record: "page=2&filter=open", + seen: ["tag=b", "tag=a", "q=a b"], + }) + }) + + test("URL parsing failures are catchable and values use native JSON forms", async () => { + expect( + await value(` + const parsed = URL.parse("/users", "https://example.test/api/") + let invalidIsTypeError = false + try { new URL("not relative without a base") } catch (error) { invalidIsTypeError = error instanceof TypeError } + return { + canParse: URL.canParse("/users", "https://example.test/api/"), + cannotParse: URL.canParse("not relative without a base"), + parsed: parsed.href, + invalidIsTypeError, + boundary: [new URL("https://example.test/a"), new URLSearchParams("q=one")], + json: JSON.stringify({ url: new URL("https://example.test/a"), params: new URLSearchParams("q=one") }), + } + `), + ).toEqual({ + canParse: true, + cannotParse: false, + parsed: "https://example.test/users", + invalidIsTypeError: true, + boundary: ["https://example.test/a", {}], + json: '{"url":"https://example.test/a","params":{}}', + }) + }) + + test("distinguishes omitted URL arguments from explicit undefined", async () => { + expect( + await value(` + function throwsTypeError(run) { + try { run(); return false } catch (error) { return error instanceof TypeError } + } + const params = new URLSearchParams() + const required = [ + () => params.append(), + () => params.delete(), + () => params.get(), + () => params.getAll(), + () => params.has(), + () => params.set(), + () => params.forEach(), + ].map(throwsTypeError) + params.append(undefined, undefined) + return { + construct: throwsTypeError(() => new URL()), + canParse: throwsTypeError(() => URL.canParse()), + parse: throwsTypeError(() => URL.parse()), + explicitUndefined: new URL(undefined, "https://example.test/base/").href, + params: params.toString(), + required, + } + `), + ).toEqual({ + construct: true, + canParse: true, + parse: true, + explicitUndefined: "https://example.test/base/undefined", + params: "undefined=undefined", + required: [true, true, true, true, true, true, true], + }) + }) +}) + +describe("Map", () => { + test("get/set/has/size with chaining", async () => { + expect( + await value(` + const m = new Map() + m.set("a", 1).set("b", 2) + return { a: m.get("a"), b: m.get("b"), has: m.has("a"), miss: m.get("zz") === undefined, size: m.size } + `), + ).toEqual({ a: 1, b: 2, has: true, miss: true, size: 5 - 3 }) + }) + + test("object keys use identity", async () => { + expect( + await value(` + const key = { id: 1 } + const m = new Map() + m.set(key, "hit") + return [m.get(key), m.get({ id: 1 }) === undefined] + `), + ).toEqual(["hit", true]) + }) + + test("construction from entry pairs and another Map", async () => { + expect(await value(`const m = new Map([["a", 1], ["b", 2]]); return m.get("b")`)).toBe(2) + expect( + await value( + `const m = new Map([["a", 1]]); const n = new Map(m); n.set("b", 2); return [n.get("a"), n.get("b"), m.has("b")]`, + ), + ).toEqual([1, 2, false]) + expect((await error(`return new Map("nope")`)).message).toMatch(/\[key, value\] pairs/) + expect((await error(`return new Map(["flat"])`)).message).toMatch(/\[key, value\] pairs/) + }) + + test("keys/values/entries return arrays", async () => { + expect( + await value(` + const m = new Map([["a", 1], ["b", 2]]) + return { keys: m.keys(), values: m.values(), entries: m.entries() } + `), + ).toEqual({ + keys: ["a", "b"], + values: [1, 2], + entries: [ + ["a", 1], + ["b", 2], + ], + }) + }) + + test("Object.fromEntries(map) and Array.from(map)", async () => { + expect(await value(`return Object.fromEntries(new Map([["a", 1], ["b", 2]]))`)).toEqual({ a: 1, b: 2 }) + expect(await value(`return Array.from(new Map([["a", 1]]))`)).toEqual([["a", 1]]) + }) + + test("for...of iterates [key, value] pairs with destructuring", async () => { + expect( + await value(` + const m = new Map([["a", 1], ["b", 2]]) + let total = 0 + let names = "" + for (const [key, count] of m) { names += key; total += count } + return names + total + `), + ).toBe("ab3") + }) + + test("spread produces entry pairs", async () => { + expect(await value(`return [...new Map([["a", 1]])]`)).toEqual([["a", 1]]) + }) + + test("forEach passes (value, key)", async () => { + expect( + await value(` + const m = new Map([["a", 1], ["b", 2]]) + const seen = [] + m.forEach((count, key) => seen.push(key + count)) + return seen + `), + ).toEqual(["a1", "b2"]) + }) + + test("delete and clear", async () => { + expect( + await value(` + const m = new Map([["a", 1], ["b", 2]]) + const removed = m.delete("a") + const missed = m.delete("zz") + const sizeAfterDelete = m.size + m.clear() + return [removed, missed, sizeAfterDelete, m.size] + `), + ).toEqual([true, false, 1, 0]) + }) + + test("counting idiom: grouped tallies", async () => { + expect( + await value(` + const words = ["a", "b", "a", "c", "a"] + const counts = new Map() + for (const word of words) counts.set(word, (counts.get(word) ?? 0) + 1) + return Object.fromEntries(counts) + `), + ).toEqual({ a: 3, b: 1, c: 1 }) + }) + + test("maps serialize to {} at the boundary, like JSON", async () => { + expect(await value(`return new Map([["a", 1]])`)).toEqual({}) + expect(await value(`return JSON.stringify(new Map([["a", 1]]))`)).toBe("{}") + }) + + test("console.log renders map contents for debugging", async () => { + const result = await run(`console.log(new Map([["a", 1]])); return null`) + expect(result.ok).toBe(true) + expect(result.logs?.[0]).toBe(`Map(1) [["a",1]]`) + }) +}) + +describe("Set", () => { + test("add/has/delete/size with chaining", async () => { + expect( + await value(` + const s = new Set() + s.add(1).add(2).add(1) + const removed = s.delete(2) + return [s.size, s.has(1), s.has(2), removed] + `), + ).toEqual([1, true, false, true]) + }) + + test("dedupe idiom: [...new Set(items)]", async () => { + expect(await value(`return [...new Set([1, 2, 2, 3, 1])]`)).toEqual([1, 2, 3]) + }) + + test("construction from strings and other Sets", async () => { + expect(await value(`return [...new Set("aba")]`)).toEqual(["a", "b"]) + expect(await value(`return Array.from(new Set(new Set([1, 2])))`)).toEqual([1, 2]) + }) + + test("SameValueZero: NaN is findable", async () => { + expect(await value(`const s = new Set([NaN]); return s.has(NaN)`)).toBe(true) + }) + + test("for...of iterates values", async () => { + expect( + await value(` + let total = 0 + for (const n of new Set([1, 2, 3])) total += n + return total + `), + ).toBe(6) + }) + + test("sets serialize to {} at the boundary, like JSON", async () => { + expect(await value(`return { s: new Set([1]) }`)).toEqual({ s: {} }) + }) +}) + +describe("stdlib integration", () => { + 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") + expect(await value(`return typeof Math`)).toBe("object") + expect(await value(`return typeof tools`)).toBe("object") + }) + + test("negation works on any value", async () => { + expect(await value(`return !new Map()`)).toBe(false) + expect(await value(`const fn = () => 1; return !fn`)).toBe(false) + }) + + test("object spread of sandbox 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 () => { + expect( + await value(` + const m = new Map([["start", new Date(1000)]]) + return m.get("start").getTime() + `), + ).toBe(1000) + }) + + test("instanceof recognizes the stdlib value types", async () => { + expect( + await value( + `return [new Date(0) instanceof Date, /a/ instanceof RegExp, new Map() instanceof Map, new Set() instanceof Set]`, + ), + ).toEqual([true, true, true, true]) + expect( + await value(`return [[1] instanceof Array, [1] instanceof Object, ({}) instanceof Object, 5 instanceof Object]`), + ).toEqual([true, true, true, false]) + expect(await value(`return [new Map() instanceof Set, "s" instanceof Date]`)).toEqual([false, false]) + expect( + await value(`const p = Promise.resolve(1); const isPromise = p instanceof Promise; await p; return isPromise`), + ).toBe(true) + }) + + test("realistic pipeline: parse, extract with regex, dedupe, count by day", async () => { + expect( + await value(` + const raw = '[{"at":"2024-01-01T05:00:00Z","tag":"a b"},{"at":"2024-01-01T09:00:00Z","tag":"b c"},{"at":"2024-01-02T01:00:00Z","tag":"a"}]' + const rows = JSON.parse(raw) + const tags = new Set() + const byDay = new Map() + for (const row of rows) { + for (const m of row.tag.matchAll(/[a-z]+/g)) tags.add(m[0]) + const day = new Date(row.at).toISOString().slice(0, 10) + byDay.set(day, (byDay.get(day) ?? 0) + 1) + } + return { tags: [...tags].sort((a, b) => (a < b ? -1 : 1)), byDay: Object.fromEntries(byDay) } + `), + ).toEqual({ tags: ["a", "b", "c"], byDay: { "2024-01-01": 2, "2024-01-02": 1 } }) + }) +}) + +describe("sandbox values at intra-sandbox 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( + "d:0", + ) + }) + + 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 () => { + expect( + await value(` + const src = { m: new Map([["a", 1]]) } + const copy = { ...src } + copy.m.set("b", 2) + return [copy.m.get("a"), src.m.get("b")] + `), + ).toEqual([1, 2]) + 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 () => { + expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5) + }) + + 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 () => { + 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([]) + expect(await value(`return Object.assign({}, new Map([["a", 1]]))`)).toEqual({}) + expect(await value(`return Object.hasOwn(new Date(0), "time")`)).toBe(false) + }) + + test("the host boundary still serializes JSON forms: results, JSON.stringify, and tool arguments", async () => { + expect(await value(`return { d: new Date(0), m: new Map([["a", 1]]) }`)).toEqual({ + d: "1970-01-01T00:00:00.000Z", + m: {}, + }) + expect(await value(`return JSON.stringify({ d: new Date(0) })`)).toBe('{"d":"1970-01-01T00:00:00.000Z"}') + + const observed: Array = [] + const capture = Tool.make({ + description: "Capture the exact input the host receives", + input: { type: "object" }, + run: (input) => + Effect.sync(() => { + observed.push(input) + return "ok" + }), + }) + const result = await Effect.runPromise( + CodeMode.execute({ + tools: { host: { capture } }, + code: `return await tools.host.capture({ when: new Date(0), tags: new Map([["a", 1]]) })`, + }), + ) + expect(result.ok).toBe(true) + expect(observed).toStrictEqual([{ when: "1970-01-01T00:00:00.000Z", tags: {} }]) + }) +}) diff --git a/packages/codemode/tsconfig.json b/packages/codemode/tsconfig.json new file mode 100644 index 00000000000..0c8cf18fc2b --- /dev/null +++ b/packages/codemode/tsconfig.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/bun/tsconfig.json", + "compilerOptions": { + "noUncheckedIndexedAccess": false, + "types": ["bun"], + "lib": ["ESNext", "DOM", "DOM.Iterable"] // kilocode_change - align upstream's new package with Kilo's tsgo environment + } +} diff --git a/packages/core/bunfig.toml b/packages/core/bunfig.toml new file mode 100644 index 00000000000..786a3774447 --- /dev/null +++ b/packages/core/bunfig.toml @@ -0,0 +1,2 @@ +[test] +preload = ["./test/preload.ts"] diff --git a/packages/opencode/drizzle.config.ts b/packages/core/drizzle.config.ts similarity index 78% rename from packages/opencode/drizzle.config.ts rename to packages/core/drizzle.config.ts index 428afabcfb2..20322ba42f9 100644 --- a/packages/opencode/drizzle.config.ts +++ b/packages/core/drizzle.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "drizzle-kit" export default defineConfig({ dialect: "sqlite", - schema: "./src/**/*.sql.ts", + schema: ["./src/**/*.sql.ts", "./src/**/sql.ts"], out: "./migration", dbCredentials: { url: "/home/thdxr/.local/share/kilo/kilo.db", diff --git a/packages/core/package.json b/packages/core/package.json index 8a56507dd3d..03ed5fec474 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,32 +1,69 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.3.54", + "version": "7.4.23", "name": "@opencode-ai/core", "type": "module", "license": "MIT", "private": true, "scripts": { - "test": "bun test", - "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml", - "typecheck": "tsgo --noEmit" + "db": "bun drizzle-kit", + "migration": "bun run script/migration.ts", + "fix-node-pty": "bun run script/fix-node-pty.ts", + "test": "bun test --only-failures", + "typecheck": "tsgo --noEmit", + "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml" }, "bin": { "opencode": "./bin/opencode" }, "exports": { + "./effect/layer-node": "./src/effect/layer-node.ts", + "./effect/app-node": "./src/effect/app-node.ts", + "./session/runner": "./src/session/runner/index.ts", + "./system-context": "./src/system-context/index.ts", "./*": "./src/*.ts" }, - "imports": {}, + "imports": { + "#sqlite": { + "bun": "./src/database/sqlite.bun.ts", + "node": "./src/database/sqlite.node.ts", + "default": "./src/database/sqlite.bun.ts" + }, + "#pty": { + "bun": "./src/pty/pty.bun.ts", + "node": "./src/pty/pty.node.ts", + "default": "./src/pty/pty.bun.ts" + }, + "#fff": { + "bun": "./src/filesystem/fff.bun.ts", + "node": "./src/filesystem/fff.node.ts", + "default": "./src/filesystem/fff.bun.ts" + } + }, "devDependencies": { "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@types/cross-spawn": "catalog:", "@types/npm-package-arg": "6.1.4", "@types/npmcli__arborist": "6.3.3", - "@types/semver": "catalog:" + "@types/semver": "catalog:", + "@types/node": "catalog:", + "@types/turndown": "5.0.5", + "@types/which": "3.0.4", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1", + "@opencode-ai/http-recorder": "workspace:*", + "drizzle-kit": "catalog:" }, "dependencies": { "@kilocode/kilo-gateway": "workspace:*", + "@kilocode/kilo-indexing": "workspace:*", "@kilocode/sandbox": "workspace:*", "@effect/opentelemetry": "catalog:", "@effect/platform-node": "catalog:", @@ -47,33 +84,52 @@ "xdg-basedir": "5.1.0", "zod": "catalog:", "@ai-sdk/alibaba": "1.0.17", - "@ai-sdk/amazon-bedrock": "4.0.107", - "@ai-sdk/anthropic": "3.0.71", - "@ai-sdk/azure": "3.0.49", + "@ai-sdk/amazon-bedrock": "4.0.112", + "@ai-sdk/anthropic": "3.0.82", + "@ai-sdk/azure": "3.0.93", "@ai-sdk/cerebras": "2.0.54", "@ai-sdk/cohere": "3.0.27", "@ai-sdk/deepinfra": "2.0.41", - "@ai-sdk/gateway": "3.0.104", + "@ai-sdk/gateway": "3.0.157", "@ai-sdk/google": "3.0.73", "@ai-sdk/google-vertex": "4.0.128", "@ai-sdk/groq": "3.0.31", - "@ai-sdk/mistral": "3.0.27", - "@ai-sdk/openai": "3.0.53", + "@ai-sdk/mistral": "3.0.51", + "@ai-sdk/openai": "3.0.88", "@ai-sdk/openai-compatible": "2.0.48", "@ai-sdk/perplexity": "3.0.26", - "@ai-sdk/provider": "3.0.8", - "@ai-sdk/provider-utils": "4.0.23", + "@ai-sdk/provider": "3.0.14", + "@ai-sdk/provider-utils": "4.0.40", "@ai-sdk/togetherai": "2.0.41", "@ai-sdk/vercel": "2.0.39", - "@ai-sdk/xai": "3.0.92", - "@aws-sdk/credential-providers": "3.993.0", + "@ai-sdk/xai": "3.0.102", + "@aws-sdk/credential-providers": "3.1057.0", "@openrouter/ai-sdk-provider": "2.9.0", "ai-gateway-provider": "3.1.2", - "gitlab-ai-provider": "6.8.0", + "gitlab-ai-provider": "6.12.1", "google-auth-library": "10.5.0", "immer": "11.1.4", - "venice-ai-sdk-provider": "2.0.2", - "jsonc-parser": "3.3.1" + "venice-ai-sdk-provider": "2.1.1", + "jsonc-parser": "3.3.1", + "@effect/sql-sqlite-bun": "catalog:", + "@lydell/node-pty": "catalog:", + "@opencode-ai/effect-drizzle-sqlite": "workspace:*", + "@opencode-ai/effect-sqlite-node": "workspace:*", + "@opencode-ai/llm": "workspace:*", + "@parcel/watcher": "2.5.1", + "bun-pty": "0.4.8", + "drizzle-orm": "catalog:", + "fuzzysort": "3.1.0", + "gray-matter": "4.0.3", + "htmlparser2": "8.0.2", + "ignore": "7.0.5", + "turndown": "7.2.0", + "which": "6.0.1", + "@ff-labs/fff-bun": "0.9.4", + "@silvia-odwyer/photon-node": "0.3.4", + "@opencode-ai/schema": "workspace:*", + "@kilocode/plugin": "workspace:*", + "diff": "catalog:" }, "overrides": { "drizzle-orm": "catalog:" diff --git a/packages/core/schema.json b/packages/core/schema.json new file mode 100644 index 00000000000..f5811f4a87e --- /dev/null +++ b/packages/core/schema.json @@ -0,0 +1,2196 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "08cfecbf-f95d-404b-a76a-2afc564baa8b", + "prevIds": [ + "f14a9b18-8207-487e-a3d3-227e629ba9ad" + ], + "ddl": [ + { + "name": "workspace", + "entityType": "tables" + }, + { + "name": "data_migration", + "entityType": "tables" + }, + { + "name": "account_state", + "entityType": "tables" + }, + { + "name": "account", + "entityType": "tables" + }, + { + "name": "control_account", + "entityType": "tables" + }, + { + "name": "credential", + "entityType": "tables" + }, + { + "name": "event_sequence", + "entityType": "tables" + }, + { + "name": "event", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "project_directory", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "session_context_epoch", + "entityType": "tables" + }, + { + "name": "session_input", + "entityType": "tables" + }, + { + "name": "session_message", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "''", + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "branch", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "extra", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_completed", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_account_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_org_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "credential" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "integration_id", + "entityType": "columns", + "table": "credential" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "label", + "entityType": "columns", + "table": "credential" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "value", + "entityType": "columns", + "table": "credential" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "connector_id", + "entityType": "columns", + "table": "credential" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "method_id", + "entityType": "columns", + "table": "credential" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "credential" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "credential" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "credential" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "action", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "resource", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "strategy", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url_override", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "baseline", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "snapshot", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "baseline_seq", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "prompt", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "delivery", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "admitted_seq", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "promoted_seq", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "path", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "metadata", + "entityType": "columns", + "table": "session" + }, + { + "type": "real", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "cost", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_input", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_output", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_reasoning", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_read", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_write", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_workspace_project_id_project_id_fk", + "entityType": "fks", + "table": "workspace" + }, + { + "columns": [ + "active_account_id" + ], + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "nameExplicit": false, + "name": "fk_account_state_active_account_id_account_id_fk", + "entityType": "fks", + "table": "account_state" + }, + { + "columns": [ + "aggregate_id" + ], + "tableTo": "event_sequence", + "columnsTo": [ + "aggregate_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_project_directory_project_id_project_id_fk", + "entityType": "fks", + "table": "project_directory" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": [ + "message_id" + ], + "tableTo": "message", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_context_epoch_session_id_session_id_fk", + "entityType": "fks", + "table": "session_context_epoch" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_input_session_id_session_id_fk", + "entityType": "fks", + "table": "session_input" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_message_session_id_session_id_fk", + "entityType": "fks", + "table": "session_message" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": [ + "email", + "url" + ], + "nameExplicit": false, + "name": "control_account_pk", + "entityType": "pks", + "table": "control_account" + }, + { + "columns": [ + "project_id", + "directory" + ], + "nameExplicit": false, + "name": "project_directory_pk", + "entityType": "pks", + "table": "project_directory" + }, + { + "columns": [ + "session_id", + "position" + ], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "workspace_pk", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": [ + "name" + ], + "nameExplicit": false, + "name": "data_migration_pk", + "table": "data_migration", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_state_pk", + "table": "account_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "credential_pk", + "table": "credential", + "entityType": "pks" + }, + { + "columns": [ + "aggregate_id" + ], + "nameExplicit": false, + "name": "event_sequence_pk", + "table": "event_sequence", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "event_pk", + "table": "event", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_context_epoch_pk", + "table": "session_context_epoch", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_input_pk", + "table": "session_input", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_message_pk", + "table": "session_message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "event_aggregate_seq_idx", + "entityType": "indexes", + "table": "event" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "event_aggregate_type_seq_idx", + "entityType": "indexes", + "table": "event" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + }, + { + "value": "action", + "isExpression": false + }, + { + "value": "resource", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "permission_project_action_resource_idx", + "entityType": "indexes", + "table": "permission" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_time_created_id_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_id_id_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + }, + { + "value": "message_id", + "isExpression": false + }, + { + "value": "json_extract(\"data\", '$.type')", + "isExpression": true + }, + { + "value": "CASE WHEN json_extract(\"data\", '$.type') = 'text' THEN coalesce(json_extract(\"data\", '$.text'), '') WHEN json_extract(\"data\", '$.type') = 'file' THEN trim(coalesce(json_extract(\"data\", '$.filename'), '') || ' ' || CASE WHEN coalesce(json_extract(\"data\", '$.url'), '') NOT LIKE 'data:%' THEN coalesce(json_extract(\"data\", '$.url'), '') ELSE '' END || ' ' || coalesce(json_extract(\"data\", '$.source.path'), '') || ' ' || coalesce(json_extract(\"data\", '$.source.name'), '') || ' ' || CASE WHEN coalesce(json_extract(\"data\", '$.source.uri'), '') NOT LIKE 'data:%' THEN coalesce(json_extract(\"data\", '$.source.uri'), '') ELSE '' END || ' ' || coalesce(json_extract(\"data\", '$.source.clientName'), '')) ELSE coalesce(json_extract(\"data\", '$.state.error'), '') END", + "isExpression": true + } + ], + "isUnique": false, + "where": "json_valid(\"part\".\"data\") AND ((json_extract(\"part\".\"data\", '$.type') = 'text' AND coalesce(json_extract(\"part\".\"data\", '$.synthetic'), 0) = 0 AND coalesce(json_extract(\"part\".\"data\", '$.ignored'), 0) = 0) OR json_extract(\"part\".\"data\", '$.type') = 'file' OR (json_extract(\"part\".\"data\", '$.type') = 'tool' AND json_extract(\"part\".\"data\", '$.state.status') = 'error'))", + "origin": "manual", + "name": "recall_part_search_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "promoted_seq", + "isExpression": false + }, + { + "value": "delivery", + "isExpression": false + }, + { + "value": "admitted_seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_input_session_pending_delivery_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "admitted_seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_input_session_admitted_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "promoted_seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_input_session_promoted_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_message_session_seq_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_type_seq_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_time_created_id_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_time_created_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_workspace_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/packages/opencode/script/fix-node-pty.ts b/packages/core/script/fix-node-pty.ts similarity index 100% rename from packages/opencode/script/fix-node-pty.ts rename to packages/core/script/fix-node-pty.ts diff --git a/packages/core/script/migration.ts b/packages/core/script/migration.ts new file mode 100644 index 00000000000..4f383f5f8f6 --- /dev/null +++ b/packages/core/script/migration.ts @@ -0,0 +1,196 @@ +#!/usr/bin/env bun + +import { $ } from "bun" +import fs from "fs/promises" +import os from "os" +import path from "path" +import { pathToFileURL } from "url" +import { parseArgs } from "util" + +const root = path.resolve(import.meta.dirname, "../../..") +const snapshot = path.join(root, "packages/core/schema.json") +const tsDir = path.join(root, "packages/core/src/database/migration") +const registry = path.join(root, "packages/core/src/database/migration.gen.ts") +const schema = path.join(root, "packages/core/src/database/schema.gen.ts") +const args = parseArgs({ + args: process.argv.slice(2), + options: { + check: { type: "boolean" }, + name: { type: "string" }, + }, +}) + +if (args.values.check) { + await check() + process.exit(0) +} + +await generate() + +async function generate() { + const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-core-migration-")) + const incremental = path.join(temporary, "incremental") + const full = path.join(temporary, "full") + try { + await fs.mkdir(incremental) + await fs.mkdir(path.join(incremental, "baseline")) + await fs.copyFile(snapshot, path.join(incremental, "baseline/snapshot.json")) + await drizzle(temporary, incremental, args.values.name) + + const generated = await generatedMigrations(incremental) + if (generated.length > 1) throw new Error(`Expected one generated migration, found ${generated.length}.`) + const name = generated[0] + if (name) { + const target = path.join(tsDir, `${name}.ts`) + if (await Bun.file(target).exists()) throw new Error(`Database migration already exists: ${name}`) + await Bun.write( + target, + await formatTypescript( + renderMigration(name, await Bun.file(path.join(incremental, name, "migration.sql")).text()), + ), + ) + await fs.copyFile(path.join(incremental, name, "snapshot.json"), snapshot) + } + + await fs.mkdir(full) + await drizzle(temporary, full, "schema") + await Bun.write(schema, await formatTypescript(renderSchema(await generatedSql(full)))) + await Bun.write(registry, await formatTypescript(renderRegistry(await typescriptMigrations()))) + } finally { + await fs.rm(temporary, { recursive: true, force: true }) + } +} + +async function check() { + const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-core-migration-check-")) + const incremental = path.join(temporary, "incremental") + const full = path.join(temporary, "full") + try { + await fs.mkdir(incremental) + await fs.mkdir(path.join(incremental, "baseline")) + await fs.copyFile(snapshot, path.join(incremental, "baseline/snapshot.json")) + await drizzle(temporary, incremental) + if ((await generatedMigrations(incremental)).length > 0) { + throw new Error( + "Core schema has ungenerated database migrations. Run `bun script/migration.ts` from packages/core.", + ) + } + + await fs.mkdir(full) + await drizzle(temporary, full, "schema") + if ((await Bun.file(schema).text()) !== (await formatTypescript(renderSchema(await generatedSql(full))))) { + throw new Error("Current database schema is stale. Run `bun script/migration.ts` from packages/core.") + } + + const migrations = await typescriptMigrations() + if ((await Bun.file(registry).text()) !== (await formatTypescript(renderRegistry(migrations)))) { + throw new Error("Database migration registry is stale. Run `bun script/migration.ts` from packages/core.") + } + } finally { + await fs.rm(temporary, { recursive: true, force: true }) + } +} + +async function drizzle(temporary: string, output: string, name?: string) { + const config = path.join(temporary, `${path.basename(output)}.config.ts`) + await Bun.write( + config, + `import config from ${JSON.stringify(pathToFileURL(path.join(root, "packages/core/drizzle.config.ts")).href)} + +export default { ...config, out: ${JSON.stringify(output)} } +`, + ) + await $`bun drizzle-kit generate --config ${config} ${name ? ["--name", name] : []}`.cwd( + path.join(root, "packages/core"), + ) +} + +async function generatedMigrations(directory: string) { + return (await Array.fromAsync(new Bun.Glob("*/migration.sql").scan({ cwd: directory }))) + .map((file) => file.split("/")[0]) + .filter((name): name is string => name !== undefined) + .sort() +} + +async function generatedSql(directory: string) { + const generated = await generatedMigrations(directory) + if (generated.length !== 1) throw new Error(`Expected one full schema migration, found ${generated.length}.`) + return Bun.file(path.join(directory, generated[0]!, "migration.sql")).text() +} + +async function typescriptMigrations() { + return (await Array.fromAsync(new Bun.Glob("*.ts").scan({ cwd: tsDir }))) + .map((file) => path.basename(file, ".ts")) + .sort() +} + +function renderMigration(name: string, sql: string) { + return `import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: ${JSON.stringify(name)}, + up(tx) { + return Effect.gen(function* () { +${renderStatements(sql)} + }) + }, +} satisfies DatabaseMigration.Migration +` +} + +function renderSchema(sql: string) { + return `import { Effect } from "effect" +import type { DatabaseMigration } from "./migration" + +export default { + up(tx) { + return Effect.gen(function* () { +${renderStatements(sql)} + }) + }, +} satisfies Omit +` +} + +function renderStatements(sql: string) { + return sql + .split("--> statement-breakpoint") + .map((statement) => statement.trim()) + .filter((statement) => statement.length > 0) + .map(renderRun) + .join("\n") +} + +function renderRun(statement: string) { + const lines = statement.replaceAll("\t", " ").split("\n") + if (lines.length === 1) return ` yield* tx.run(\`${escapeTemplate(lines[0])}\`)` + return ` yield* tx.run(\`\n${lines.map((line) => ` ${escapeTemplate(line)}`).join("\n")}\n \`)` +} + +function escapeTemplate(line: string) { + return line.replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("${", "\\${") +} + +async function formatTypescript(input: string) { + const prettier = await import("prettier") + const typescript = await import("prettier/plugins/typescript") + const estree = await import("prettier/plugins/estree") + return prettier.format(input, { + parser: "typescript", + plugins: [typescript.default, estree.default], + semi: false, + printWidth: 120, + }) +} + +function renderRegistry(names: string[]) { + return `import type { DatabaseMigration } from "./migration" + +export const migrations = ( + await Promise.all([ +${names.map((name) => ` import("./migration/${name}"),`).join("\n")} + ]) +).map((module) => module.default) satisfies DatabaseMigration.Migration[] +` +} diff --git a/packages/core/src/account.ts b/packages/core/src/account.ts index 2d729086439..d364d6f3448 100644 --- a/packages/core/src/account.ts +++ b/packages/core/src/account.ts @@ -1,331 +1,101 @@ -import path from "path" -import { Effect, Layer, Option, Schema, Context, SynchronizedRef } from "effect" -import { Identifier } from "./util/identifier" -import { NonNegativeInt, withStatics } from "./schema" -import { Global } from "./global" -import { AppFileSystem } from "./filesystem" -import { EventV2 } from "./event" +export * as AccountV2 from "./account" -export const ID = Schema.String.pipe( - Schema.brand("AccountV2.ID"), - withStatics((schema) => ({ create: () => schema.make("acc_" + Identifier.ascending()) })), -) -export type ID = typeof ID.Type +import { Schema } from "effect" +import type * as HttpClientError from "effect/unstable/http/HttpClientError" -export const ServiceID = Schema.String.pipe(Schema.brand("ServiceID")) -export type ServiceID = typeof ServiceID.Type +export const ID = Schema.String.pipe(Schema.brand("AccountID")) +export type ID = Schema.Schema.Type -export class OAuthCredential extends Schema.Class("AccountV2.OAuthCredential")({ - type: Schema.Literal("oauth"), - refresh: Schema.String, - access: Schema.String, - expires: NonNegativeInt, - accountId: Schema.optional(Schema.String), // kilocode_change - preserve Kilo organization during v1 migration -}) {} +export const OrgID = Schema.String.pipe(Schema.brand("OrgID")) +export type OrgID = Schema.Schema.Type -export class ApiKeyCredential extends Schema.Class("AccountV2.ApiKeyCredential")({ - type: Schema.Literal("api"), - key: Schema.String, - metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), -}) {} +export const AccessToken = Schema.String.pipe(Schema.brand("AccessToken")) +export type AccessToken = Schema.Schema.Type + +export const RefreshToken = Schema.String.pipe(Schema.brand("RefreshToken")) +export type RefreshToken = Schema.Schema.Type + +export const DeviceCode = Schema.String.pipe(Schema.brand("DeviceCode")) +export type DeviceCode = Schema.Schema.Type -export const Credential = Schema.Union([OAuthCredential, ApiKeyCredential]) - .pipe(Schema.toTaggedUnion("type")) - .annotate({ - identifier: "AccountV2.Credential", - }) -export type Credential = Schema.Schema.Type +export const UserCode = Schema.String.pipe(Schema.brand("UserCode")) +export type UserCode = Schema.Schema.Type -export class Info extends Schema.Class("AccountV2.Info")({ +export class Info extends Schema.Class("Account")({ id: ID, - serviceID: ServiceID, - description: Schema.String, - credential: Credential, + email: Schema.String, + url: Schema.String, + active_org_id: Schema.NullOr(OrgID), }) {} -export class FileWriteError extends Schema.TaggedErrorClass()("AccountV2.FileWriteError", { - operation: Schema.Union([Schema.Literal("migrate"), Schema.Literal("write")]), - cause: Schema.Defect, +export class Org extends Schema.Class("Org")({ + id: OrgID, + name: Schema.String, }) {} -export type Error = FileWriteError - -export const Event = { - Added: EventV2.define({ - type: "account.added", - schema: { - account: Info, - }, - }), - Removed: EventV2.define({ - type: "account.removed", - schema: { - account: Info, - }, - }), - Switched: EventV2.define({ - type: "account.switched", - schema: { - serviceID: ServiceID, - from: Schema.optional(ID), - to: Schema.optional(ID), - }, - }), -} - -interface Writable { - version: 2 - accounts: Record - active: Record -} +export class AccountRepoError extends Schema.TaggedErrorClass()("AccountRepoError", { + message: Schema.String, + cause: Schema.optional(Schema.Defect()), +}) {} -const decodeV1 = Schema.decodeUnknownOption(Schema.Record(Schema.String, Credential)) +export class AccountServiceError extends Schema.TaggedErrorClass()("AccountServiceError", { + message: Schema.String, + cause: Schema.optional(Schema.Defect()), +}) {} -function migrate(old: Record): Writable { - const accounts: Record = {} - const active: Record = {} - for (const [serviceID, value] of Object.entries(old)) { - const decoded = Option.getOrElse(decodeV1({ [serviceID]: value }), () => ({})) - const parsed = (decoded as Record)[serviceID] - if (!parsed) continue - const id = Identifier.ascending() - const account = ID.make(id) - const brandedServiceID = ServiceID.make(serviceID) - accounts[id] = new Info({ - id: account, - serviceID: brandedServiceID, - description: "default", - credential: parsed, +export class AccountTransportError extends Schema.TaggedErrorClass()("AccountTransportError", { + method: Schema.String, + url: Schema.String, + description: Schema.optional(Schema.String), + cause: Schema.optional(Schema.Defect()), +}) { + static fromHttpClientError(error: HttpClientError.TransportError): AccountTransportError { + return new AccountTransportError({ + method: error.request.method, + url: error.request.url, + description: error.description, + cause: error.cause, }) - active[brandedServiceID] = account } - return { version: 2, accounts, active } -} -export interface Interface { - readonly get: (id: ID) => Effect.Effect - readonly all: () => Effect.Effect - readonly create: (input: { - serviceID: ServiceID - credential: Credential - description?: string - }) => Effect.Effect - readonly update: (id: ID, updates: Partial>) => Effect.Effect - readonly remove: (id: ID) => Effect.Effect - readonly activate: (id: ID) => Effect.Effect - readonly active: (serviceID: ServiceID) => Effect.Effect - readonly forService: (serviceID: ServiceID) => Effect.Effect + override get message(): string { + return [ + `Could not reach ${this.method} ${this.url}.`, + `This failed before the server returned an HTTP response.`, + this.description, + `Check your network, proxy, or VPN configuration and try again.`, + ] + .filter(Boolean) + .join("\n") + } } -export class Service extends Context.Service()("@opencode/v2/Account") {} - -export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const fsys = yield* AppFileSystem.Service - const global = yield* Global.Service - const events = yield* EventV2.Service - const file = path.join(global.data, "account.json") - const legacyFile = path.join(global.data, "auth.json") - const prior = path.join(global.data, "auth-v2.json") // kilocode_change - - const writeMigrated = Effect.fnUntraced(function* (raw: Record) { - const migrated = migrate(raw) - yield* fsys - .writeJson(file, migrated, 0o600) - .pipe(Effect.mapError((cause) => new FileWriteError({ operation: "migrate", cause }))) - return migrated - }) - - const parseAuthContent = () => { - try { - return JSON.parse(process.env.KILO_AUTH_CONTENT ?? "") - } catch {} - } - - const load: () => Effect.Effect = Effect.fnUntraced(function* () { - if (process.env.KILO_AUTH_CONTENT) { - const raw = parseAuthContent() - if (raw && typeof raw === "object") { - if ("version" in raw && raw.version === 2) return raw as Writable - return yield* writeMigrated(raw as Record) - } - return { version: 2, accounts: {}, active: {} } - } - - const legacy = yield* fsys.readJson(legacyFile).pipe(Effect.orElseSucceed(() => null)) - if (legacy && typeof legacy === "object") return yield* writeMigrated(legacy as Record) - - const raw = yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => null)) +export type AccountError = AccountRepoError | AccountServiceError | AccountTransportError - if (raw && typeof raw === "object") { - if ("version" in raw && raw.version === 2) return raw as Writable - return yield* writeMigrated(raw as Record) - } - - // kilocode_change start - migrate the previous Kilo multi-account store after the current store - const previous = yield* fsys.readJson(prior).pipe(Effect.orElseSucceed(() => null)) - if (previous && typeof previous === "object" && "version" in previous && previous.version === 2) { - yield* fsys - .writeJson(file, previous, 0o600) - .pipe(Effect.mapError((cause) => new FileWriteError({ operation: "migrate", cause }))) - return previous as Writable - } - // kilocode_change end - - return { version: 2, accounts: {}, active: {} } - }) - - const write = (data: Writable) => - fsys - .writeJson(file, data, 0o600) - .pipe(Effect.mapError((cause) => new FileWriteError({ operation: "write", cause }))) - - const state = SynchronizedRef.makeUnsafe( - yield* load().pipe(Effect.orElseSucceed((): Writable => ({ version: 2, accounts: {}, active: {} }))), - ) - - const activate = Effect.fn("AccountV2.activate")(function* (id: ID) { - const data = yield* SynchronizedRef.get(state) - const account = data.accounts[id] - if (!account) return - const activated = yield* SynchronizedRef.modifyEffect( - state, - Effect.fnUntraced(function* (data) { - const nextAccount = data.accounts[id] - if (!nextAccount) return [undefined, data] as const - - const next = { ...data, active: { ...data.active, [nextAccount.serviceID]: id } } - yield* write(next) - return [{ serviceID: nextAccount.serviceID, from: data.active[nextAccount.serviceID], to: id }, next] as const - }), - ) - if (activated) yield* events.publish(Event.Switched, activated) - }) - - const result: Interface = { - get: Effect.fn("AccountV2.get")(function* (id) { - return (yield* SynchronizedRef.get(state)).accounts[id] - }), - - all: Effect.fn("AccountV2.all")(function* () { - return Object.values((yield* SynchronizedRef.get(state)).accounts) - }), - - active: Effect.fn("AccountV2.active")(function* (serviceID) { - const data = yield* SynchronizedRef.get(state) - return ( - data.accounts[data.active[serviceID]] ?? Object.values(data.accounts).find((a) => a.serviceID === serviceID) - ) - }), - - forService: Effect.fn("AccountV2.list")(function* (serviceID) { - return Object.values((yield* SynchronizedRef.get(state)).accounts).filter((a) => a.serviceID === serviceID) - }), - - create: Effect.fn("AccountV2.add")(function* (input) { - const id = ID.make(Identifier.ascending()) - const account = new Info({ - id, - serviceID: input.serviceID, - description: input.description ?? "default", - credential: input.credential, - }) - const added = yield* SynchronizedRef.modifyEffect( - state, - Effect.fnUntraced(function* (data) { - const next = { - ...data, - accounts: { ...data.accounts, [account.id]: account }, - active: { ...data.active, [account.serviceID]: account.id }, - } - - yield* write(next) - return [ - { - account, - switched: { serviceID: account.serviceID, from: data.active[account.serviceID], to: account.id }, - }, - next, - ] as const - }), - ) - yield* events.publish(Event.Added, { account: added.account }) - yield* events.publish(Event.Switched, added.switched) - return added.account - }), - - update: Effect.fn("AccountV2.update")(function* (id, updates) { - const existing = (yield* SynchronizedRef.get(state)).accounts[id] - if (!existing) return - yield* SynchronizedRef.modifyEffect( - state, - Effect.fnUntraced(function* (data) { - if (!data.accounts[id]) return [undefined, data] as const - - const next = { - ...data, - accounts: { - ...data.accounts, - [id]: new Info({ - id, - serviceID: existing.serviceID, - description: updates.description ?? existing.description, - credential: updates.credential ?? existing.credential, - }), - }, - } +export class Login extends Schema.Class("Login")({ + code: DeviceCode, + user: UserCode, + url: Schema.String, + server: Schema.String, + expiry: Schema.Duration, + interval: Schema.Duration, +}) {} - yield* write(next) - return [undefined, next] as const - }), - ) - }), +export class PollSuccess extends Schema.TaggedClass()("PollSuccess", { + email: Schema.String, +}) {} - remove: Effect.fn("AccountV2.remove")(function* (id) { - const removed = yield* SynchronizedRef.modifyEffect( - state, - Effect.fnUntraced(function* (data) { - const accounts = { ...data.accounts } - const active = { ...data.active } - const removed = accounts[id] - if (!removed) return [undefined, data] as const - const wasActive = active[removed.serviceID] === id - delete accounts[id] - const replacement = Object.values(accounts).find((account) => account.serviceID === removed.serviceID) - if (wasActive) { - if (replacement) active[removed.serviceID] = replacement.id - else delete active[removed.serviceID] - } +export class PollPending extends Schema.TaggedClass()("PollPending", {}) {} - const next = { ...data, accounts, active } - yield* write(next) - return [ - { - account: removed, - switched: wasActive ? { serviceID: removed.serviceID, from: id, to: replacement?.id } : undefined, - }, - next, - ] as const - }), - ) - if (removed) { - yield* events.publish(Event.Removed, { account: removed.account }) - if (removed.switched) yield* events.publish(Event.Switched, removed.switched) - } - }), +export class PollSlow extends Schema.TaggedClass()("PollSlow", {}) {} - activate, - } +export class PollExpired extends Schema.TaggedClass()("PollExpired", {}) {} - return Service.of(result) - }), -) +export class PollDenied extends Schema.TaggedClass()("PollDenied", {}) {} -export const defaultLayer = layer.pipe( - Layer.provide(AppFileSystem.defaultLayer), - Layer.provide(Global.defaultLayer), - Layer.provide(EventV2.defaultLayer), -) +export class PollError extends Schema.TaggedClass()("PollError", { + cause: Schema.Defect(), +}) {} -export * as AccountV2 from "./account" +export const PollResult = Schema.Union([PollSuccess, PollPending, PollSlow, PollExpired, PollDenied, PollError]) +export type PollResult = Schema.Schema.Type diff --git a/packages/core/src/account/sql.ts b/packages/core/src/account/sql.ts new file mode 100644 index 00000000000..4f45651d78e --- /dev/null +++ b/packages/core/src/account/sql.ts @@ -0,0 +1,39 @@ +import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core" + +import { AccountV2 } from "../account" +import { Timestamps } from "../database/schema.sql" + +export const AccountTable = sqliteTable("account", { + id: text().$type().primaryKey(), + email: text().notNull(), + url: text().notNull(), + access_token: text().$type().notNull(), + refresh_token: text().$type().notNull(), + token_expiry: integer(), + ...Timestamps, +}) + +export const AccountStateTable = sqliteTable("account_state", { + id: integer().primaryKey(), + active_account_id: text() + .$type() + .references(() => AccountTable.id, { onDelete: "set null" }), + active_org_id: text().$type(), +}) + +// LEGACY +export const ControlAccountTable = sqliteTable( + "control_account", + { + email: text().notNull(), + url: text().notNull(), + access_token: text().$type().notNull(), + refresh_token: text().$type().notNull(), + token_expiry: integer(), + active: integer({ mode: "boolean" }) + .notNull() + .$default(() => false), + ...Timestamps, + }, + (table) => [primaryKey({ columns: [table.email, table.url] })], +) diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts index ec7dfa2adeb..da86cf809b9 100644 --- a/packages/core/src/agent.ts +++ b/packages/core/src/agent.ts @@ -1,83 +1,60 @@ export * as AgentV2 from "./agent" -import { Array, Context, Effect, Layer, Schema, Scope } from "effect" -import { castDraft, enableMapSet, type Draft } from "immer" -import { ModelV2 } from "./model" -import { PermissionV2 } from "./permission" -import { ProviderV2 } from "./provider" -import { PositiveInt } from "./schema" +import { makeLocationNode } from "./effect/app-node" +import { Array, Context, Effect, Layer, Types } from "effect" +import { Agent } from "@opencode-ai/schema/agent" import { State } from "./state" -export const ID = Schema.String.pipe(Schema.brand("AgentV2.ID")) +export const ID = Agent.ID export type ID = typeof ID.Type +export const defaultID = ID.make("build") -export const Color = Schema.Union([ - Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), - Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), -]) +export const Color = Agent.Color -export class Info extends Schema.Class("AgentV2.Info")({ - id: ID, - model: ModelV2.Ref.pipe(Schema.optional), - options: ProviderV2.Options, - system: Schema.String.pipe(Schema.optional), - description: Schema.String.pipe(Schema.optional), - mode: Schema.Literals(["subagent", "primary", "all"]), - hidden: Schema.Boolean, - color: Color.pipe(Schema.optional), - steps: PositiveInt.pipe(Schema.optional), - permissions: PermissionV2.Ruleset, -}) { - static empty(id: ID) { - return new Info({ - id, - options: { - headers: {}, - body: {}, - aisdk: { - provider: {}, - request: {}, - }, - }, - mode: "all", - hidden: false, - permissions: [], - }) - } +export const Info = Agent.Info +export type Info = Agent.Info + +export interface Selection { + readonly id: ID + readonly info: Info | undefined } type Data = { - agents: Map + agents: Map> + default?: ID } -export type Editor = { +export type Draft = { list: () => readonly Info[] get: (id: ID) => Info | undefined - update: (id: ID, fn: (agent: Draft) => void) => void + default: (id: ID | undefined) => void + update: (id: ID, fn: (agent: Types.DeepMutable) => void) => void remove: (id: ID) => void } -export interface Interface { - readonly transform: State.Interface["transform"] - readonly update: (update: State.Transform) => Effect.Effect +export interface Interface extends State.Transformable { readonly get: (id: ID) => Effect.Effect + readonly default: () => Effect.Effect + readonly resolve: (id?: ID | string) => Effect.Effect + readonly select: (id?: ID | string) => Effect.Effect readonly all: () => Effect.Effect } export class Service extends Context.Service()("@opencode/v2/Agent") {} -enableMapSet() - -export const layer = Layer.effect( +const layer = Layer.effect( Service, Effect.gen(function* () { - const state = State.create({ + const state = State.create({ initial: () => ({ agents: new Map() }), - editor: (draft) => ({ + draft: (draft) => ({ list: () => Array.fromIterable(draft.agents.values()) as Info[], get: (id) => draft.agents.get(id), + default: (id) => { + draft.default = id + }, update: (id, fn) => { - const current = draft.agents.get(id) ?? castDraft(Info.empty(id)) + const current = draft.agents.get(id) ?? (Info.empty(id) as Types.DeepMutable) if (!draft.agents.has(id)) draft.agents.set(id, current) fn(current) current.id = id @@ -87,16 +64,41 @@ export const layer = Layer.effect( }, }), }) + const selectable = (agent: Info | undefined) => + agent && agent.mode !== "subagent" && !agent.hidden ? agent : undefined + const selectedDefault = () => { + const data = state.get() + const configured = data.default ? selectable(data.agents.get(data.default)) : undefined + if (configured) return configured + const build = selectable(data.agents.get(ID.make("build"))) + if (build) return build + for (const agent of data.agents.values()) { + const fallback = selectable(agent) + if (fallback) return fallback + } + } return Service.of({ transform: state.transform, - update: Effect.fn("AgentV2.update")(function* (update) { - const transform = yield* state.transform() - yield* transform(update) - }), + reload: state.reload, get: Effect.fn("AgentV2.get")(function* (id) { return state.get().agents.get(id) }), + default: Effect.fn("AgentV2.default")(function* () { + return selectedDefault() + }), + resolve: Effect.fn("AgentV2.resolve")(function* (id) { + if (id !== undefined) return state.get().agents.get(ID.make(id)) + return selectedDefault() + }), + select: Effect.fn("AgentV2.select")(function* (id) { + if (id !== undefined) { + const selected = ID.make(id) + return { id: selected, info: state.get().agents.get(selected) } + } + const info = selectedDefault() + return { id: info?.id ?? defaultID, info } + }), all: Effect.fn("AgentV2.all")(function* () { return Array.fromIterable(state.get().agents.values()) }), @@ -104,4 +106,6 @@ export const layer = Layer.effect( }), ) -export const defaultLayer = layer +export const locationLayer = layer + +export const node = makeLocationNode({ service: Service, layer, deps: [] }) diff --git a/packages/core/src/aisdk.ts b/packages/core/src/aisdk.ts index 5fa2294309c..b604dac664b 100644 --- a/packages/core/src/aisdk.ts +++ b/packages/core/src/aisdk.ts @@ -1,13 +1,28 @@ export * as AISDK from "./aisdk" +import { makeLocationNode } from "./effect/app-node" import type { LanguageModelV3 } from "@ai-sdk/provider" -import { Cause, Context, Effect, Layer, Schema } from "effect" +import { Cause, Context, Effect, Layer, Schema, Scope } from "effect" import { ModelV2 } from "./model" -import { PluginV2 } from "./plugin" import { ProviderV2 } from "./provider" +import { State } from "./state" type SDK = any +export interface SDKEvent { + readonly model: ModelV2.Info + readonly package: string + readonly options: Record + sdk?: SDK +} + +export interface LanguageEvent { + readonly model: ModelV2.Info + readonly sdk: SDK + readonly options: Record + language?: LanguageModelV3 +} + function wrapSSE(res: Response, ms: number, ctl: AbortController) { if (typeof ms !== "number" || ms <= 0) return res if (!res.body) return res @@ -57,8 +72,12 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) { } function prepareOptions(model: ModelV2.Info, pkg: string) { - const options: Record = { name: model.providerID, ...model.options.aisdk.provider } - if (model.endpoint.type === "aisdk" && model.endpoint.url) options.baseURL = model.endpoint.url + const options: Record = { + name: model.providerID, + ...(model.api.type === "aisdk" ? (model.api.settings ?? {}) : {}), + ...model.request.body, + } + if (model.api.type === "aisdk" && model.api.url) options.baseURL = model.api.url const customFetch = options.fetch const chunkTimeout = options.chunkTimeout @@ -77,7 +96,11 @@ function prepareOptions(model: ModelV2.Info, pkg: string) { if (abortSignals.length === 1) opts.signal = abortSignals[0] if (abortSignals.length > 1) opts.signal = AbortSignal.any(abortSignals) - if ((pkg === "@ai-sdk/openai" || pkg === "@ai-sdk/azure") && opts.body && opts.method === "POST") { + if ( + (pkg === "@ai-sdk/openai" || pkg === "@ai-sdk/azure" || pkg === "@ai-sdk/amazon-bedrock/mantle") && + opts.body && + opts.method === "POST" + ) { const body = JSON.parse(opts.body as string) if (body.store !== true && Array.isArray(body.input)) { for (const item of body.input) { @@ -100,7 +123,7 @@ function prepareOptions(model: ModelV2.Info, pkg: string) { export class InitError extends Schema.TaggedErrorClass()("AISDK.InitError", { providerID: ProviderV2.ID, - cause: Schema.Defect, + cause: Schema.Defect(), }) {} function initError(providerID: ProviderV2.ID) { @@ -108,65 +131,105 @@ function initError(providerID: ProviderV2.ID) { } export interface Interface { + readonly hook: { + readonly sdk: ( + callback: (event: SDKEvent) => Effect.Effect | void, + ) => Effect.Effect + readonly language: ( + callback: (event: LanguageEvent) => Effect.Effect | void, + ) => Effect.Effect + } + readonly runSDK: (event: SDKEvent) => Effect.Effect + readonly runLanguage: (event: LanguageEvent) => Effect.Effect readonly language: (model: ModelV2.Info) => Effect.Effect } export class Service extends Context.Service()("@opencode/v2/AISDK") {} -export const layer = Layer.effect( +export const locationLayer = Layer.effect( Service, Effect.gen(function* () { - const plugin = yield* PluginV2.Service + let sdkHooks: ((event: SDKEvent) => Effect.Effect | void)[] = [] + let languageHooks: ((event: LanguageEvent) => Effect.Effect | void)[] = [] const languages = new Map() const sdks = new Map() - return Service.of({ + const register = ( + hooks: () => ((event: Event) => Effect.Effect | void)[], + update: (hooks: ((event: Event) => Effect.Effect | void)[]) => void, + ) => + Effect.fn("AISDK.hook")(function* (callback: (event: Event) => Effect.Effect | void) { + const scope = yield* Scope.Scope + let active = true + update([...hooks(), callback]) + const dispose = Effect.sync(() => { + if (!active) return + active = false + update(hooks().filter((item) => item !== callback)) + }) + yield* Scope.addFinalizer(scope, dispose) + return { dispose } + }) + + const run = Effect.fnUntraced(function* ( + hooks: readonly ((event: Event) => Effect.Effect | void)[], + event: Event, + ) { + for (const hook of hooks) { + const result = hook(event) + if (Effect.isEffect(result)) yield* result + } + return event + }) + + const service = Service.of({ + hook: { + sdk: register( + () => sdkHooks, + (next) => (sdkHooks = next), + ), + language: register( + () => languageHooks, + (next) => (languageHooks = next), + ), + }, + runSDK: (event) => run(sdkHooks, event), + runLanguage: (event) => run(languageHooks, event), language: Effect.fn("AISDK.language")(function* (model) { - const key = `${model.providerID}/${model.id}/${model.options.variant ?? "default"}` + const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}` const existing = languages.get(key) if (existing) return existing - if (model.endpoint.type !== "aisdk") + if (model.api.type !== "aisdk") return yield* new InitError({ providerID: model.providerID, - cause: new Error(`Unsupported endpoint ${model.endpoint.type}`), + cause: new Error(`Unsupported api ${model.api.type}`), }) - const options = prepareOptions(model, model.endpoint.package) + const options = prepareOptions(model, model.api.package) const sdkKey = JSON.stringify({ providerID: model.providerID, - endpoint: model.endpoint, + api: model.api, options, }) const sdk = sdks.get(sdkKey) ?? - (yield* plugin - .trigger("aisdk.sdk", { model, package: model.endpoint.package, options }, {}) - .pipe(initError(model.providerID))).sdk + (yield* service.runSDK({ model, package: model.api.package, options }).pipe(initError(model.providerID))).sdk if (!sdk) return yield* new InitError({ providerID: model.providerID, cause: new Error("No AISDK provider plugin returned an SDK"), }) sdks.set(sdkKey, sdk) - const result = yield* plugin - .trigger( - "aisdk.language", - { - model, - sdk, - options, - }, - {}, - ) - .pipe(initError(model.providerID)) - const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.apiID)).pipe( + const result = yield* service.runLanguage({ model, sdk, options }).pipe(initError(model.providerID)) + const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.id)).pipe( initError(model.providerID), ) languages.set(key, language) return language }), }) + return service }), ) -export const defaultLayer = layer.pipe(Layer.provide(PluginV2.defaultLayer)) +export const node = makeLocationNode({ service: Service, layer: locationLayer, deps: [] }) diff --git a/packages/core/src/background-job.ts b/packages/core/src/background-job.ts new file mode 100644 index 00000000000..cdffd212bc4 --- /dev/null +++ b/packages/core/src/background-job.ts @@ -0,0 +1,365 @@ +export * as BackgroundJob from "./background-job" + +import { Cause, Clock, Context, Deferred, Effect, Exit, Layer, Scope, SynchronizedRef } from "effect" +import { Identifier } from "./id/id" +import { makeGlobalNode } from "./effect/app-node" + +export type Status = "running" | "completed" | "error" | "cancelled" + +export type Info = { + id: string + type: string + title?: string + status: Status + started_at: number + completed_at?: number + output?: string + error?: string + metadata?: Record +} + +type Active = { + info: Info + done: Deferred.Deferred + scope: Scope.Closeable + token: object + pending: number + next: number + output?: { sequence: number; text: string } + tail: Deferred.Deferred + promoted: Deferred.Deferred + onPromote?: Effect.Effect +} + +type State = { + jobs: SynchronizedRef.SynchronizedRef> + scope: Scope.Scope +} + +type FinishResult = { + info?: Info + done?: Deferred.Deferred + scope?: Scope.Closeable +} + +type PromoteResult = { + info?: Info + promoted?: Deferred.Deferred + onPromote?: Effect.Effect +} + +type StartResult = { info: Info } | { info: Info; scope: Scope.Closeable; token: object } + +type ExtendResult = + | { extended: false } + | { + extended: true + previous: Deferred.Deferred + scope: Scope.Closeable + tail: Deferred.Deferred + token: object + sequence: number + } + +export type StartInput = { + id?: string + type: string + title?: string + metadata?: Record + onPromote?: Effect.Effect + run: Effect.Effect +} + +export type ExtendInput = { + id: string + run: Effect.Effect +} + +export type WaitInput = { + id: string + timeout?: number +} + +export type WaitResult = { + info?: Info + timedOut: boolean +} + +export interface Interface { + readonly list: () => Effect.Effect + readonly get: (id: string) => Effect.Effect + readonly start: (input: StartInput) => Effect.Effect + readonly extend: (input: ExtendInput) => Effect.Effect + readonly wait: (input: WaitInput) => Effect.Effect + readonly waitForPromotion: (id: string) => Effect.Effect + readonly promote: (id: string) => Effect.Effect + readonly cancel: (id: string) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/BackgroundJob") {} + +function snapshot(job: Active): Info { + return { + ...job.info, + ...(job.info.metadata ? { metadata: { ...job.info.metadata } } : {}), + } +} + +function errorText(error: unknown) { + if (error instanceof Error) return error.message + return String(error) +} + +/** + * Makes one scoped, process-local registry. Entries are intentionally not + * durable: process restart or owner-scope closure loses status and interrupts + * live work. Persisted observation, restart recovery, and remote workers need a + * separate durable ownership slice rather than pretending this registry has + * those semantics. + */ +export const make = Effect.gen(function* () { + const state: State = { + jobs: yield* SynchronizedRef.make(new Map()), + scope: yield* Scope.Scope, + } + + const settle = Effect.fn("BackgroundJob.settle")(function* ( + id: string, + token: object, + sequence: number, + exit: Exit.Exit, + ) { + const completed_at = yield* Clock.currentTimeMillis + const result = yield* SynchronizedRef.modify(state.jobs, (jobs): readonly [FinishResult, Map] => { + const job = jobs.get(id) + if (!job) return [{}, jobs] + if (job.token !== token) return [{}, jobs] + if (job.info.status !== "running") return [{ info: snapshot(job) }, jobs] + const pending = job.pending - 1 + const output = + Exit.isSuccess(exit) && (!job.output || sequence > job.output.sequence) + ? { sequence, text: exit.value } + : job.output + if (Exit.isSuccess(exit) && pending > 0) { + return [{}, new Map(jobs).set(id, { ...job, pending, output })] + } + const status: Exclude = Exit.isSuccess(exit) + ? "completed" + : Cause.hasInterruptsOnly(exit.cause) + ? "cancelled" + : "error" + const next = { + ...job, + onPromote: undefined, + pending: 0, + output, + info: { + ...job.info, + status, + completed_at, + ...(output ? { output: output.text } : {}), + ...(Exit.isFailure(exit) ? { error: errorText(Cause.squash(exit.cause)) } : {}), + }, + } + return [{ info: snapshot(next), done: job.done, scope: job.scope }, new Map(jobs).set(id, next)] + }) + if (result.info && result.done) yield* Deferred.succeed(result.done, result.info).pipe(Effect.ignore) + if (result.scope) { + yield* Scope.close(result.scope, Exit.void).pipe(Effect.forkIn(state.scope, { startImmediately: true })) + } + return result.info + }) + + const fork = Effect.fn("BackgroundJob.fork")(function* ( + scope: Scope.Scope, + id: string, + token: object, + sequence: number, + run: Effect.Effect, + ) { + return yield* run.pipe( + Effect.matchCauseEffect({ + onSuccess: (output) => settle(id, token, sequence, Exit.succeed(output)), + onFailure: (cause) => settle(id, token, sequence, Exit.failCause(cause)), + }), + Effect.asVoid, + Effect.forkIn(scope, { startImmediately: true }), + ) + }) + + const list: Interface["list"] = Effect.fn("BackgroundJob.list")(function* () { + return Array.from((yield* SynchronizedRef.get(state.jobs)).values()) + .map(snapshot) + .toSorted((a, b) => a.started_at - b.started_at) + }) + + const get: Interface["get"] = Effect.fn("BackgroundJob.get")(function* (id) { + const job = (yield* SynchronizedRef.get(state.jobs)).get(id) + if (!job) return + return snapshot(job) + }) + + const start: Interface["start"] = Effect.fn("BackgroundJob.start")(function* (input) { + return yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const id = input.id ?? Identifier.ascending("job") + const started_at = yield* Clock.currentTimeMillis + const done = yield* Deferred.make() + const promoted = yield* Deferred.make() + const tail = yield* Deferred.make() + const result = yield* SynchronizedRef.modifyEffect( + state.jobs, + Effect.fnUntraced(function* (jobs) { + const existing = jobs.get(id) + if (existing?.info.status === "running") { + return [{ info: snapshot(existing) }, jobs] as readonly [StartResult, Map] + } + const scope = yield* Scope.fork(state.scope, "parallel") + const token = {} + const job = { + info: { + id, + type: input.type, + title: input.title, + status: "running" as const, + started_at, + metadata: input.metadata, + }, + done, + scope, + token, + pending: 1, + next: 1, + tail, + promoted, + onPromote: input.onPromote, + } + return [{ info: snapshot(job), scope, token }, new Map(jobs).set(id, job)] as readonly [ + StartResult, + Map, + ] + }), + ) + if ("scope" in result) + yield* fork( + result.scope, + id, + result.token, + 0, + restore(input.run).pipe(Effect.ensuring(Deferred.succeed(tail, undefined))), + ) + return result.info + }), + ) + }) + + const extend: Interface["extend"] = Effect.fn("BackgroundJob.extend")(function* (input) { + return yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const tail = yield* Deferred.make() + const result = yield* SynchronizedRef.modify( + state.jobs, + (jobs): readonly [ExtendResult, Map] => { + const job = jobs.get(input.id) + if (!job || job.info.status !== "running") return [{ extended: false }, jobs] + return [ + { extended: true, previous: job.tail, scope: job.scope, tail, token: job.token, sequence: job.next }, + new Map(jobs).set(input.id, { + ...job, + pending: job.pending + 1, + next: job.next + 1, + tail, + }), + ] + }, + ) + if (!result.extended) return false + yield* fork( + result.scope, + input.id, + result.token, + result.sequence, + Deferred.await(result.previous).pipe( + Effect.andThen(restore(input.run)), + Effect.ensuring(Deferred.succeed(result.tail, undefined)), + ), + ) + return true + }), + ) + }) + + const wait: Interface["wait"] = Effect.fn("BackgroundJob.wait")(function* (input) { + const job = (yield* SynchronizedRef.get(state.jobs)).get(input.id) + if (!job) return { timedOut: false } + if (job.info.status !== "running") return { info: snapshot(job), timedOut: false } + if (input.timeout === undefined) return { info: yield* Deferred.await(job.done), timedOut: false } + if (input.timeout <= 0) return { info: snapshot(job), timedOut: true } + const info = yield* Deferred.await(job.done).pipe(Effect.timeoutOption(input.timeout)) + if (info._tag === "Some") return { info: info.value, timedOut: false } + return { info: snapshot(job), timedOut: true } + }) + + const waitForPromotion: Interface["waitForPromotion"] = Effect.fn("BackgroundJob.waitForPromotion")(function* (id) { + const job = (yield* SynchronizedRef.get(state.jobs)).get(id) + if (!job || job.info.status !== "running") return yield* Effect.never + if (job.info.metadata?.background === true) return snapshot(job) + return yield* Deferred.await(job.promoted) + }) + + const promote: Interface["promote"] = Effect.fn("BackgroundJob.promote")(function* (id) { + const result = yield* SynchronizedRef.modifyEffect( + state.jobs, + Effect.fnUntraced(function* (jobs) { + const job = jobs.get(id) + if (!job || job.info.status !== "running") return [{}, jobs] as readonly [PromoteResult, Map] + if (job.info.metadata?.background === true) + return [{ info: snapshot(job) }, jobs] as readonly [PromoteResult, Map] + const next = { + ...job, + onPromote: undefined, + info: { + ...job.info, + metadata: { ...job.info.metadata, background: true }, + }, + } + return [ + { info: snapshot(next), onPromote: job.onPromote, promoted: job.promoted }, + new Map(jobs).set(id, next), + ] as readonly [PromoteResult, Map] + }), + ) + if (result.info && result.promoted) yield* Deferred.succeed(result.promoted, result.info).pipe(Effect.ignore) + if (result.onPromote) yield* result.onPromote.pipe(Effect.ignore) + return result.info + }) + + const cancel: Interface["cancel"] = Effect.fn("BackgroundJob.cancel")(function* (id) { + const completed_at = yield* Clock.currentTimeMillis + const result = yield* SynchronizedRef.modify(state.jobs, (jobs): readonly [FinishResult, Map] => { + const job = jobs.get(id) + if (!job) return [{}, jobs] + if (job.info.status !== "running") return [{ info: snapshot(job) }, jobs] + const next = { + ...job, + onPromote: undefined, + pending: 0, + info: { + ...job.info, + status: "cancelled" as const, + completed_at, + }, + } + return [{ info: snapshot(next), done: job.done, scope: job.scope }, new Map(jobs).set(id, next)] + }) + if (result.info && result.done) yield* Deferred.succeed(result.done, result.info).pipe(Effect.ignore) + if (result.scope) yield* Scope.close(result.scope, Exit.void) + return result.info + }) + + return Service.of({ list, get, start, extend, wait, waitForPromotion, promote, cancel }) +}) + +const layer = Layer.effect(Service, make) + +export const node = makeGlobalNode({ service: Service, layer, deps: [] }) diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index a12de5d476b..1945024b54e 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -1,60 +1,41 @@ export * as Catalog from "./catalog" -import { Context, Effect, Layer, Option, Order, pipe, Schema, Array, Scope, Stream } from "effect" -import { castDraft, enableMapSet, type Draft } from "immer" +import { makeLocationNode } from "./effect/app-node" +import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect" +import { Catalog } from "@opencode-ai/schema/catalog" import { ModelV2 } from "./model" -import { PluginV2 } from "./plugin" import { ProviderV2 } from "./provider" -import { Location } from "./location" import { EventV2 } from "./event" import { Policy } from "./policy" import { State } from "./state" +import { Integration } from "./integration" export type ProviderRecord = { - provider: ProviderV2.Info - models: Map + provider: ProviderV2.MutableInfo + models: Map } export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID } -export class ProviderNotFoundError extends Schema.TaggedErrorClass()( - "CatalogV2.ProviderNotFound", - { - providerID: ProviderV2.ID, - }, -) {} - -export class ModelNotFoundError extends Schema.TaggedErrorClass()("CatalogV2.ModelNotFound", { - providerID: ProviderV2.ID, - modelID: ModelV2.ID, -}) {} - export const PolicyActions = Schema.Literals(["provider.use"]) -export const Event = { - ModelUpdated: EventV2.define({ - type: "catalog.model.updated", - schema: { - model: ModelV2.Info, - }, - }), -} +export const Event = Catalog.Event type Data = { providers: Map defaultModel?: DefaultModel } -export type Editor = { +export type Draft = { provider: { list: () => readonly ProviderRecord[] get: (providerID: ProviderV2.ID) => ProviderRecord | undefined - update: (providerID: ProviderV2.ID, fn: (provider: Draft) => void) => void + update: (providerID: ProviderV2.ID, fn: (provider: ProviderV2.MutableInfo) => void) => void remove: (providerID: ProviderV2.ID) => void } model: { get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => ModelV2.Info | undefined - update: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: Draft) => void) => void + update: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: ModelV2.MutableInfo) => void) => void remove: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void default: { get: () => DefaultModel | undefined @@ -63,101 +44,82 @@ export type Editor = { } } -export interface Interface { - readonly transform: State.Interface["transform"] +export interface Interface extends State.Transformable { readonly provider: { - readonly get: (providerID: ProviderV2.ID) => Effect.Effect + readonly get: (providerID: ProviderV2.ID) => Effect.Effect readonly all: () => Effect.Effect readonly available: () => Effect.Effect } readonly model: { - readonly get: ( - providerID: ProviderV2.ID, - modelID: ModelV2.ID, - ) => Effect.Effect + readonly get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => Effect.Effect readonly all: () => Effect.Effect readonly available: () => Effect.Effect - readonly default: () => Effect.Effect> - readonly small: (providerID: ProviderV2.ID) => Effect.Effect> + readonly default: () => Effect.Effect + readonly small: (providerID: ProviderV2.ID) => Effect.Effect } } export class Service extends Context.Service()("@opencode/v2/Catalog") {} -enableMapSet() - -export const layer = Layer.effect( +const layer = Layer.effect( Service, Effect.gen(function* () { - yield* Location.Service - const plugin = yield* PluginV2.Service const events = yield* EventV2.Service const policy = yield* Policy.Service - const scope = yield* Scope.Scope - - const resolve = (model: ModelV2.Info) => { - const provider = state.get().providers.get(model.providerID)!.provider - const endpoint = - model.endpoint.type === "unknown" - ? provider.endpoint - : model.endpoint.type === "aisdk" && provider.endpoint.type === "aisdk" && !model.endpoint.url - ? { ...model.endpoint, url: provider.endpoint.url } - : model.endpoint - const options = { - headers: { - ...provider.options.headers, - ...model.options.headers, - }, - body: { - ...provider.options.body, - ...model.options.body, - }, - aisdk: { - provider: { - ...provider.options.aisdk.provider, - ...model.options.aisdk.provider, - }, - request: model.options.aisdk.request, - }, - variant: model.options.variant, + const integrations = yield* Integration.Service + + const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => { + if (provider.disabled) return false + if (typeof provider.request.body.apiKey === "string") return true + if (integration?.connections.length) return true + return provider.integrationID === undefined && !integration + } + + const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => { + const api = + model.api.type === "native" && !model.api.url && Object.keys(model.api.settings).length === 0 + ? { ...provider.api, id: model.api.id } + : model.api.type === "aisdk" && provider.api.type === "aisdk" && !model.api.url + ? { ...model.api, url: provider.api.url, settings: { ...provider.api.settings, ...model.api.settings } } + : model.api.type === "aisdk" && provider.api.type === "aisdk" + ? { ...model.api, settings: { ...provider.api.settings, ...model.api.settings } } + : model.api + const request = { + headers: { ...provider.request.headers, ...model.request.headers }, + body: { ...provider.request.body, ...model.request.body }, + variant: model.request.variant, } - return new ModelV2.Info({ + return ModelV2.Info.make({ ...model, - endpoint, - options, + api, + request, }) } - function* getRecord(providerID: ProviderV2.ID) { - const match = state.get().providers.get(providerID) - if (!match) return yield* new ProviderNotFoundError({ providerID }) - return match + const normalizeApi = (item: ProviderV2.MutableInfo | ModelV2.MutableInfo) => { + if (typeof item.request.body.baseURL !== "string") return + item.api.url = item.request.body.baseURL + delete item.request.body.baseURL } - const normalizeEndpoint = (item: Draft | Draft) => { - if (item.endpoint.type !== "aisdk" || typeof item.options.aisdk.provider.baseURL !== "string") return - item.endpoint.url = item.options.aisdk.provider.baseURL - delete item.options.aisdk.provider.baseURL - } - - const state = State.create({ + const state = State.create({ initial: () => ({ providers: new Map() }), - editor: (draft) => { - const result: Editor = { + draft: (draft) => { + const result: Draft = { provider: { list: () => Array.fromIterable(draft.providers.values()) as ProviderRecord[], get: (providerID) => draft.providers.get(providerID), update: (providerID, fn) => { let current = draft.providers.get(providerID) if (!current) { - current = castDraft({ - provider: ProviderV2.Info.empty(providerID), - models: new Map(), - }) + current = { + provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo, + models: new Map(), + } draft.providers.set(providerID, current) } fn(current.provider) - normalizeEndpoint(current.provider) + normalizeApi(current.provider) }, remove: (providerID) => { draft.providers.delete(providerID) @@ -166,14 +128,21 @@ export const layer = Layer.effect( model: { get: (providerID, modelID) => draft.providers.get(providerID)?.models.get(modelID), update: (providerID, modelID, fn) => { - result.provider.update(providerID, () => {}) - const record = draft.providers.get(providerID)! - const model = record.models.get(modelID) ?? castDraft(ModelV2.Info.empty(providerID, modelID)) + let record = draft.providers.get(providerID) + if (!record) { + record = { + provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo, + models: new Map(), + } + draft.providers.set(providerID, record) + } + const model = + record.models.get(modelID) ?? (ModelV2.Info.empty(providerID, modelID) as ModelV2.MutableInfo) if (!record.models.has(modelID)) record.models.set(modelID, model) fn(model) model.id = modelID model.providerID = providerID - normalizeEndpoint(model) + normalizeApi(model) }, remove: (providerID, modelID) => { draft.providers.get(providerID)?.models.delete(modelID) @@ -188,30 +157,24 @@ export const layer = Layer.effect( } return result }, - finalize: Effect.fn("CatalogV2.finalize")(function* (catalog, reason) { - if (reason !== "plugin.added") yield* plugin.trigger("catalog.transform", catalog, {}).pipe(Effect.asVoid) - for (const record of [...catalog.provider.list()]) { - if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") { - catalog.provider.remove(record.provider.id) + finalize: Effect.fn("CatalogV2.finalize")(function* (catalog) { + if (policy.hasStatements()) { + for (const record of [...catalog.provider.list()]) { + if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") { + catalog.provider.remove(record.provider.id) + } } } + yield* events.publish(Event.Updated, {}) }), }) - - yield* events.subscribe(PluginV2.Event.Added).pipe( - Stream.runForEach((event) => - state.update((catalog) => plugin.triggerFor(event.data.id, "catalog.transform", catalog, {}), "plugin.added"), - ), - Effect.forkIn(scope, { startImmediately: true }), - ) - const result: Interface = { transform: state.transform, + reload: state.reload, provider: { get: Effect.fn("CatalogV2.provider.get")(function* (providerID) { - const record = yield* getRecord(providerID) - return record.provider + return state.get().providers.get(providerID)?.provider }), all: Effect.fn("CatalogV2.provider.all")(function* () { @@ -219,57 +182,68 @@ export const layer = Layer.effect( }), available: Effect.fn("CatalogV2.provider.available")(function* () { - return Array.fromIterable(state.get().providers.values()) - .map((record) => record.provider) - .filter((provider) => provider.enabled) + const active = new Map((yield* integrations.list()).map((integration) => [integration.id, integration])) + return (yield* result.provider.all()).filter((provider) => + available(provider, active.get(provider.integrationID ?? Integration.ID.make(provider.id))), + ) }), }, model: { get: Effect.fn("CatalogV2.model.get")(function* (providerID, modelID) { - const record = yield* getRecord(providerID) + const record = state.get().providers.get(providerID) + if (!record) return const model = record.models.get(modelID) - if (!model) return yield* new ModelNotFoundError({ providerID, modelID }) - return resolve(model) + return model && projectModel(model, record.provider) }), all: Effect.fn("CatalogV2.model.all")(function* () { return pipe( Array.fromIterable(state.get().providers.values()), - Array.flatMap((record) => Array.fromIterable(record.models.values())), - Array.map(resolve), - Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)), + Array.flatMap((record) => { + return Array.fromIterable(record.models.values()).map((model) => projectModel(model, record.provider)) + }), + Array.sortWith((item) => item.time.released, Order.flip(Order.Number)), ) }), available: Effect.fn("CatalogV2.model.available")(function* () { - return (yield* result.model.all()).filter((model) => { - const record = state.get().providers.get(model.providerID) - return record?.provider.enabled !== false && model.enabled - }) + const providers = new Set((yield* result.provider.available()).map((provider) => provider.id)) + return (yield* result.model.all()).filter((model) => providers.has(model.providerID) && model.enabled) }), default: Effect.fn("CatalogV2.model.default")(function* () { const defaultModel = state.get().defaultModel if (defaultModel) { - const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID).pipe(Effect.option) - if (Option.isSome(model) && model.value.enabled) return model + const provider = yield* result.provider.get(defaultModel.providerID) + if (provider && (yield* result.provider.available()).some((item) => item.id === provider.id)) { + const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID) + if (model?.enabled) return model + } } - return pipe( - yield* result.model.available(), - Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)), - Array.head, + return Option.getOrUndefined( + pipe( + yield* result.model.available(), + Array.sortWith((item) => item.time.released, Order.flip(Order.Number)), + Array.head, + ), ) }), small: Effect.fn("CatalogV2.model.small")(function* (providerID) { const record = state.get().providers.get(providerID) - if (!record) return Option.none() + if (!record) return + const provider = record.provider + + // TODO: Remove these provider-specific assumptions once model syncing reliably reports available deployments. + if (providerID === ProviderV2.ID.azure || providerID === ProviderV2.ID.make("azure-cognitive-services")) { + return + } if (providerID === ProviderV2.ID.opencode) { const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano")) - if (gpt5Nano?.enabled && gpt5Nano.status === "active") return Option.some(resolve(gpt5Nano)) + if (gpt5Nano?.enabled && gpt5Nano.status === "active") return projectModel(gpt5Nano, provider) } const candidates = pipe( @@ -285,7 +259,7 @@ export const layer = Layer.effect( Array.map((model) => ({ model, cost: model.cost[0] ? model.cost[0].input + model.cost[0].output : 999, - age: (Date.now() - model.time.released.epochMilliseconds) / (1000 * 60 * 60 * 24 * 30), + age: (Date.now() - model.time.released) / (1000 * 60 * 60 * 24 * 30), small: SMALL_MODEL_RE.test(`${model.id} ${model.family ?? ""} ${model.name}`.toLowerCase()), })), Array.filter((item) => item.cost > 0 && item.age <= 18), @@ -297,15 +271,17 @@ export const layer = Layer.effect( return pipe( items, Array.sortWith((item) => (item.cost / maxCost) * 0.8 + (item.age / maxAge) * 0.2, Order.Number), - Array.map((item) => resolve(item.model)), + Array.map((item) => projectModel(item.model, provider)), Array.head, ) } - return pipe( - candidates, - Array.filter((item) => item.small), - (items) => (items.length > 0 ? pick(items) : pick(candidates)), + return Option.getOrUndefined( + pipe( + candidates, + Array.filter((item) => item.small), + (items) => (items.length > 0 ? pick(items) : pick(candidates)), + ), ) }), }, @@ -317,4 +293,9 @@ export const layer = Layer.effect( const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/ -export const defaultLayer = layer.pipe(Layer.provide(EventV2.defaultLayer), Layer.provide(PluginV2.defaultLayer)) +export const locationLayer = layer.pipe( + Layer.provideMerge(Integration.locationLayer), + Layer.provideMerge(Policy.locationLayer), +) + +export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Policy.node, Integration.node] }) diff --git a/packages/core/src/command.ts b/packages/core/src/command.ts new file mode 100644 index 00000000000..cb082a9f47e --- /dev/null +++ b/packages/core/src/command.ts @@ -0,0 +1,64 @@ +export * as CommandV2 from "./command" + +import { makeLocationNode } from "./effect/app-node" +import { Context, Effect, Layer, Types } from "effect" +import { Command } from "@opencode-ai/schema/command" +import { State } from "./state" + +export const Info = Command.Info +export type Info = Command.Info + +export type Data = { + commands: Map> +} + +export type Draft = { + list: () => readonly Info[] + get: (name: string) => Info | undefined + update: (name: string, update: (command: Types.DeepMutable) => void) => void + remove: (name: string) => void +} + +export interface Interface extends State.Transformable { + readonly get: (name: string) => Effect.Effect + readonly list: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/Command") {} + +const layer = Layer.effect( + Service, + Effect.sync(() => { + const state = State.create({ + initial: () => ({ commands: new Map() }), + draft: (draft) => ({ + list: () => Array.from(draft.commands.values()) as Info[], + get: (name) => draft.commands.get(name), + update: (name, update) => { + const current = draft.commands.get(name) ?? ({ name, template: "" } as Types.DeepMutable) + if (!draft.commands.has(name)) draft.commands.set(name, current) + update(current) + current.name = name + }, + remove: (name) => { + draft.commands.delete(name) + }, + }), + }) + + return Service.of({ + reload: state.reload, + transform: state.transform, + get: Effect.fn("CommandV2.get")(function* (name) { + return state.get().commands.get(name) + }), + list: Effect.fn("CommandV2.list")(function* () { + return Array.from(state.get().commands.values()) + }), + }) + }), +) + +export const locationLayer = layer + +export const node = makeLocationNode({ service: Service, layer, deps: [] }) diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index c9e1396739e..6c9b06d3e59 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -1,17 +1,20 @@ export * as Config from "./config" +import { makeLocationNode } from "./effect/app-node" import path from "path" import { type ParseError, parse } from "jsonc-parser" import { Context, Effect, Layer, Option, Schema } from "effect" -import { AppFileSystem } from "./filesystem" +import { Permission } from "@opencode-ai/schema/permission" +import { FSUtil } from "./fs-util" +import { Flag } from "./flag/flag" // kilocode_change import { Global } from "./global" import { Location } from "./location" -import { PermissionV2 } from "./permission" import { Policy } from "./policy" import { AbsolutePath } from "./schema" import { ConfigAgent } from "./config/agent" import { ConfigAttachments } from "./config/attachments" import { ConfigCompaction } from "./config/compaction" +import { ConfigCommand } from "./config/command" import { ConfigExperimental } from "./config/experimental" import { ConfigFormatter } from "./config/formatter" import { ConfigLSP } from "./config/lsp" @@ -21,6 +24,8 @@ import { ConfigProvider } from "./config/provider" import { ConfigReference } from "./config/reference" import { ConfigToolOutput } from "./config/tool-output" import { ConfigWatcher } from "./config/watcher" +import { ConfigV1 } from "./v1/config/config" +import { ConfigMigrateV1 } from "./v1/config/migrate" export class Info extends Schema.Class("Config.Info")({ $schema: Schema.optional(Schema.String).annotate({ @@ -32,6 +37,9 @@ export class Info extends Schema.Class("Config.Info")({ model: Schema.String.pipe(Schema.optional).annotate({ description: "Default model to use when no session or agent model is selected", }), + default_agent: Schema.String.pipe(Schema.optional).annotate({ + description: "Default primary agent to use when no session agent is selected", + }), autoupdate: Schema.Union([Schema.Boolean, Schema.Literal("notify")]) .pipe(Schema.optional) .annotate({ @@ -50,7 +58,7 @@ export class Info extends Schema.Class("Config.Info")({ username: Schema.String.pipe(Schema.optional).annotate({ description: "Username displayed in conversations and used for telemetry identity", }), - permissions: PermissionV2.Ruleset.pipe(Schema.optional).annotate({ + permissions: Permission.Ruleset.pipe(Schema.optional).annotate({ description: "Ordered tool permission rules applied to agent tool use", }), agents: Schema.Record(Schema.String, ConfigAgent.Info).pipe(Schema.optional).annotate({ @@ -83,6 +91,9 @@ export class Info extends Schema.Class("Config.Info")({ skills: Schema.String.pipe(Schema.Array, Schema.optional).annotate({ description: "Additional paths or URLs to discover skills from", }), + commands: Schema.Record(Schema.String, ConfigCommand.Info).pipe(Schema.optional).annotate({ + description: "Named slash command definitions", + }), instructions: Schema.String.pipe(Schema.Array, Schema.optional).annotate({ description: "Additional paths or URLs supplying ambient instructions", }), @@ -96,42 +107,43 @@ export class Info extends Schema.Class("Config.Info")({ providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional), }) {} -export const FileSource = Schema.Struct({ - type: Schema.Literal("file"), - path: Schema.String, -}).annotate({ identifier: "Config.FileSource" }) -export type FileSource = typeof FileSource.Type +export class Document extends Schema.Class("Config.Document")({ + type: Schema.Literal("document"), + path: Schema.String.pipe(Schema.optional), + info: Info, +}) {} -export const MemorySource = Schema.Struct({ - type: Schema.Literal("memory"), -}).annotate({ identifier: "Config.MemorySource" }) -export type MemorySource = typeof MemorySource.Type +export class Directory extends Schema.Class("Config.Directory")({ + type: Schema.Literal("directory"), + path: AbsolutePath, +}) {} -export const Source = Schema.Union([FileSource, MemorySource]).pipe(Schema.toTaggedUnion("type")) -export type Source = typeof Source.Type +export type Entry = Document | Directory -export class Loaded extends Schema.Class("Config.Loaded")({ - source: Source, - info: Info, -}) {} +export function latest(entries: readonly Entry[], key: K): Info[K] | undefined { + return entries + .filter((entry): entry is Document => entry.type === "document") + .findLast((entry) => entry.info[key] !== undefined)?.info[key] +} export interface Interface { - /** Returns supplemental config directories from lowest to highest priority. */ - readonly directories: () => Effect.Effect - /** Loads location config files from lowest to highest priority. */ - readonly get: () => Effect.Effect + /** Returns location config documents and supplemental directories from lowest to highest priority. */ + readonly entries: () => Effect.Effect } export class Service extends Context.Service()("@opencode/v2/Config") {} -export const layer = Layer.effect( +const layer = Layer.effect( Service, Effect.gen(function* () { - const fs = yield* AppFileSystem.Service + const fs = yield* FSUtil.Service const global = yield* Global.Service const location = yield* Location.Service const policy = yield* Policy.Service - const names = ["config.json", "opencode.json", "opencode.jsonc"] + const names = ["config.json", "kilo.json", "kilo.jsonc", "opencode.json", "opencode.jsonc"] // kilocode_change + const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const + const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions) + const decodeV1Info = Schema.decodeUnknownOption(ConfigV1.Info, decodeOptions) const loadFile = Effect.fnUntraced(function* (filepath: string) { const text = yield* fs.readFileStringSafe(filepath) @@ -141,63 +153,78 @@ export const layer = Layer.effect( const input: unknown = parse(text, errors, { allowTrailingComma: true }) if (errors.length) return - // Accept legacy fields while v2 is migrated incrementally; recognized - // fields still have to satisfy the v2 schema. const info = Option.getOrUndefined( - Schema.decodeUnknownOption(Info)(input, { errors: "all", onExcessProperty: "ignore" }), + ConfigMigrateV1.isV1(input) + ? decodeV1Info(input).pipe(Option.map(ConfigMigrateV1.migrate), Option.flatMap(decodeInfo)) + : decodeInfo(input), ) if (!info) return - return new Loaded({ source: { type: "file", path: filepath }, info }) + return new Document({ type: "document", path: filepath, info }) }) const loadDirectory = Effect.fnUntraced(function* (directory: AbsolutePath) { - return yield* Effect.forEach(names, (file) => loadFile(path.join(directory, file))).pipe( - Effect.map((configs) => configs.filter((config): config is Loaded => config !== undefined)), - ) + return [ + ...(yield* Effect.forEach(names, (file) => loadFile(path.join(directory, file))).pipe( + Effect.map((configs) => configs.filter((config): config is Document => config !== undefined)), + )), + new Directory({ type: "directory", path: directory }), + ] }) const globalDirectory = AbsolutePath.make(global.config) const locationIsGlobal = path.resolve(location.directory) === path.resolve(global.config) // Read configuration once when this location opens. Later calls reuse these // values until the location is reopened. - const directories = locationIsGlobal - ? [globalDirectory] - : [ - globalDirectory, - ...(yield* fs - .up({ targets: [".opencode"], start: location.directory, stop: location.project.directory }) - .pipe(Effect.orDie)) - .toReversed() - .map((directory) => AbsolutePath.make(directory)), - ] + const discovered = locationIsGlobal || Flag.KILO_DISABLE_PROJECT_CONFIG // kilocode_change + ? [] + : yield* fs + .up({ + targets: [".kilo", ".kilocode", ...names.toReversed()], // kilocode_change + start: location.directory, + stop: location.project.directory, + }) + .pipe(Effect.orDie) + const directories = [ + globalDirectory, + ...discovered + .filter((item) => [".kilo", ".kilocode"].includes(path.basename(item))) // kilocode_change + .toReversed() + .map((directory) => AbsolutePath.make(directory)), + ] // A config closer to the opened directory should win over one higher up. // Search starts nearby, so reverse the results before applying them. - const directPaths = locationIsGlobal - ? [] - : (yield* fs - .up({ targets: names.toReversed(), start: location.directory, stop: location.project.directory }) - .pipe(Effect.orDie)).toReversed() + // kilocode_change start + const directPaths = discovered.filter((item) => ![".kilo", ".kilocode"].includes(path.basename(item))).toReversed() + // kilocode_change end const direct = yield* Effect.forEach(directPaths, loadFile).pipe( Effect.orDie, - Effect.map((configs) => configs.filter((config): config is Loaded => config !== undefined)), + Effect.map((configs) => configs.filter((config): config is Document => config !== undefined)), ) const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie) // Apply general settings first and more specific settings last: - // global config, project files, then `.opencode` files. + // global config, project files, then Kilo config-directory files. // kilocode_change const configs = [...(supplementary[0] ?? []), ...direct, ...supplementary.slice(1).flat()] // Rules use the opposite order so a user-global rule can override a // repository rule. Statement order inside each file stays unchanged. - yield* policy.load(configs.toReversed().flatMap((config) => config.info.experimental?.policies ?? [])) + yield* policy.load( + configs + .filter((config): config is Document => config.type === "document") + .toReversed() + .flatMap((config) => config.info.experimental?.policies ?? []), + ) return Service.of({ - directories: Effect.fn("Config.directories")(function* () { - return directories - }), - get: Effect.fn("Config.get")(function* () { + entries: Effect.fn("Config.entries")(function* () { return configs }), }) }), ) -export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Global.defaultLayer)) +export const locationLayer = layer.pipe(Layer.provideMerge(Policy.locationLayer)) + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [FSUtil.node, Global.node, Location.node, Policy.node], +}) diff --git a/packages/core/src/config/agent.ts b/packages/core/src/config/agent.ts index 40d2bc94b58..63df995f859 100644 --- a/packages/core/src/config/agent.ts +++ b/packages/core/src/config/agent.ts @@ -1,7 +1,7 @@ export * as ConfigAgent from "./agent" import { Schema } from "effect" -import { PermissionV2 } from "../permission" +import { Permission } from "@opencode-ai/schema/permission" import { ConfigProvider } from "./provider" import { PositiveInt } from "../schema" @@ -13,7 +13,7 @@ export const Color = Schema.Union([ export class Info extends Schema.Class("ConfigV2.Agent")({ model: Schema.String.pipe(Schema.optional), variant: Schema.String.pipe(Schema.optional), - options: ConfigProvider.Options.pipe(Schema.optional), + request: ConfigProvider.Request.pipe(Schema.optional), system: Schema.String.pipe(Schema.optional), description: Schema.String.pipe(Schema.optional), mode: Schema.Literals(["subagent", "primary", "all"]).pipe(Schema.optional), @@ -21,5 +21,5 @@ export class Info extends Schema.Class("ConfigV2.Agent")({ color: Color.pipe(Schema.optional), steps: PositiveInt.pipe(Schema.optional), disabled: Schema.Boolean.pipe(Schema.optional), - permissions: PermissionV2.Ruleset.pipe(Schema.optional), + permissions: Permission.Ruleset.pipe(Schema.optional), }) {} diff --git a/packages/core/src/config/command.ts b/packages/core/src/config/command.ts new file mode 100644 index 00000000000..01bdb2120d1 --- /dev/null +++ b/packages/core/src/config/command.ts @@ -0,0 +1,12 @@ +export * as ConfigCommand from "./command" + +import { Schema } from "effect" + +export class Info extends Schema.Class("ConfigV2.Command")({ + template: Schema.String.pipe(Schema.optional), // kilocode_change - allow partial command overrides + description: Schema.String.pipe(Schema.optional), + agent: Schema.String.pipe(Schema.optional), + model: Schema.String.pipe(Schema.optional), + variant: Schema.String.pipe(Schema.optional), + subtask: Schema.Boolean.pipe(Schema.optional), +}) {} diff --git a/packages/core/src/config/compaction.ts b/packages/core/src/config/compaction.ts index eef67ee26a0..3c5960c8355 100644 --- a/packages/core/src/config/compaction.ts +++ b/packages/core/src/config/compaction.ts @@ -4,7 +4,6 @@ import { Schema } from "effect" import { NonNegativeInt } from "../schema" export class Keep extends Schema.Class("ConfigV2.Compaction.Keep")({ - turns: NonNegativeInt.pipe(Schema.optional), tokens: NonNegativeInt.pipe(Schema.optional), }) {} diff --git a/packages/core/src/config/markdown.ts b/packages/core/src/config/markdown.ts new file mode 100644 index 00000000000..e1d74e649eb --- /dev/null +++ b/packages/core/src/config/markdown.ts @@ -0,0 +1,36 @@ +export * as ConfigMarkdown from "./markdown" + +import matter from "gray-matter" +export function parse(content: string) { + try { + return matter(content) + } catch { + return matter(sanitize(content)) + } +} + +export function parseOption(content: string) { + try { + return parse(content) + } catch { + return undefined + } +} + +// Other coding agents accept unquoted colons in frontmatter values. Retry +// those values as YAML block scalars so existing config files keep working. +export function sanitize(content: string) { + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/) + if (!match) return content + const frontmatter = match[1] + const result = frontmatter.split(/\r?\n/).flatMap((line) => { + if (line.trim().startsWith("#") || line.trim() === "" || /^\s+/.test(line)) return [line] + const entry = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/) + if (!entry) return [line] + const value = entry[2].trim() + if (value === "" || value === ">" || value === "|" || value.startsWith('"') || value.startsWith("'")) return [line] + if (!value.includes(":")) return [line] + return [`${entry[1]}: |-`, ` ${value}`] + }) + return content.replace(frontmatter, () => result.join("\n")) +} diff --git a/packages/core/src/config/mcp.ts b/packages/core/src/config/mcp.ts index fce853815b4..f3a5ac9b256 100644 --- a/packages/core/src/config/mcp.ts +++ b/packages/core/src/config/mcp.ts @@ -3,12 +3,24 @@ export * as ConfigMCP from "./mcp" import { Schema } from "effect" import { PositiveInt } from "../schema" +export class Timeout extends Schema.Class("ConfigV2.MCP.Timeout")({ + startup: PositiveInt.pipe(Schema.optional).annotate({ + description: "Maximum time in milliseconds to establish and initialize the MCP server.", + }), + request: PositiveInt.pipe(Schema.optional).annotate({ + description: "Maximum time in milliseconds to wait for each MCP request after initialization.", + }), +}) {} + export class Local extends Schema.Class("ConfigV2.MCP.Local")({ type: Schema.Literal("local"), command: Schema.String.pipe(Schema.Array), + cwd: Schema.String.pipe(Schema.optional).annotate({ + description: "Working directory for the MCP server process. Relative paths resolve from the workspace directory.", + }), environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), disabled: Schema.Boolean.pipe(Schema.optional), - timeout: PositiveInt.pipe(Schema.optional), + timeout: Timeout.pipe(Schema.optional), }) {} export class OAuth extends Schema.Class("ConfigV2.MCP.OAuth")({ @@ -25,12 +37,12 @@ export class Remote extends Schema.Class("ConfigV2.MCP.Remote")({ headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), oauth: Schema.Union([OAuth, Schema.Literal(false)]).pipe(Schema.optional), disabled: Schema.Boolean.pipe(Schema.optional), - timeout: PositiveInt.pipe(Schema.optional), + timeout: Timeout.pipe(Schema.optional), }) {} export const Server = Schema.Union([Local, Remote]).pipe(Schema.toTaggedUnion("type")) export class Info extends Schema.Class("ConfigV2.MCP")({ - timeout: PositiveInt.pipe(Schema.optional), + timeout: Timeout.pipe(Schema.optional), servers: Schema.Record(Schema.String, Server).pipe(Schema.optional), }) {} diff --git a/packages/core/src/config/plugin/agent.ts b/packages/core/src/config/plugin/agent.ts index c05b0a578f0..0635aa6291c 100644 --- a/packages/core/src/config/plugin/agent.ts +++ b/packages/core/src/config/plugin/agent.ts @@ -1,65 +1,179 @@ export * as ConfigAgentPlugin from "./agent" -import { Effect } from "effect" +import { define } from "../../plugin/internal" +import path from "path" +import { Effect, Option, Schema } from "effect" import { AgentV2 } from "../../agent" import { Config } from "../../config" +import { ConfigAgent } from "../agent" +import { ConfigMarkdown } from "../markdown" +import { FSUtil } from "../../fs-util" import { ModelV2 } from "../../model" +import { ConfigAgentV1 } from "../../v1/config/agent" +import { ConfigMigrateV1 } from "../../v1/config/migrate" +import { Global } from "../../global" import { PermissionV2 } from "../../permission" -import { PluginV2 } from "../../plugin" +import type { LocationMutation } from "../../location-mutation" +import type { ReadTool } from "../../tool/read" +import type { EditTool } from "../../tool/edit" -export const Plugin = PluginV2.define({ - id: PluginV2.ID.make("config-agent"), - effect: Effect.gen(function* () { - const agent = yield* AgentV2.Service - const config = yield* Config.Service - const files = yield* config.get() - - yield* agent.update((editor) => { - const permissions = new Map() +const legacySources = [ + { pattern: "{agent,agents}/**/*.md", primary: false }, + { pattern: "{mode,modes}/*.md", primary: true }, +] as const +const decodeAgent = Schema.decodeUnknownOption(ConfigAgent.Info) +const decodeLegacyAgent = Schema.decodeUnknownOption(ConfigAgentV1.Info) +const decodeConfig = Schema.decodeUnknownOption(Config.Info) +type PathAction = + | LocationMutation.ExternalDirectoryAuthorization["action"] + | typeof ReadTool.name + | typeof EditTool.name +const pathActions = ["external_directory", "read", "edit"] as const satisfies readonly PathAction[] +const agentKeys = new Set([ + "model", + "variant", + "request", + "system", + "description", + "mode", + "hidden", + "color", + "steps", + "disabled", + "permissions", +]) - for (const file of files) { - for (const [id, item] of Object.entries(file.info.agents ?? {})) { - const agentID = AgentV2.ID.make(id) - if (item.disabled) { - editor.remove(agentID) - permissions.delete(agentID) - continue - } +export const Plugin = define({ + id: "config-agent", + effect: Effect.fn(function* (ctx) { + const config = yield* Config.Service + const fs = yield* FSUtil.Service + const global = yield* Global.Service + yield* ctx.agent.transform( + Effect.fn(function* (draft) { + const documents = yield* Effect.forEach(yield* config.entries(), (entry) => { + if (entry.type === "document") return Effect.succeed([entry]) + return Effect.gen(function* () { + const files = yield* discover(fs, entry.path) + return yield* Effect.forEach(files, (file) => + fs.readFileStringSafe(file.filepath).pipe( + Effect.map((content) => content && decode(file, content)), + Effect.catch(() => Effect.succeed(undefined)), + ), + ).pipe( + Effect.map((documents) => + documents.filter((document): document is Config.Document => document !== undefined), + ), + ) + }) + }).pipe(Effect.map((documents) => documents.flat())) + const permissions = expandPermissions( + documents.flatMap((document) => document.info.permissions ?? []), + global.home, + ) + const configuredDefault = Config.latest(documents, "default_agent") + if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault)) + for (const current of draft.list()) { + draft.update(current.id, (agent) => agent.permissions.push(...permissions)) + } - editor.update(agentID, (agent) => { - if (item.model !== undefined) { - const model = ModelV2.parse(item.model) - agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant } - } - if (item.variant !== undefined && agent.model !== undefined) { - agent.model.variant = ModelV2.VariantID.make(item.variant) - } - if (item.options !== undefined) { - Object.assign(agent.options.headers, item.options.headers ?? {}) - Object.assign(agent.options.body, item.options.body ?? {}) - Object.assign(agent.options.aisdk.provider, item.options.aisdk?.provider ?? {}) - Object.assign(agent.options.aisdk.request, item.options.aisdk?.request ?? {}) + for (const document of documents) { + for (const [id, item] of Object.entries(document.info.agents ?? {})) { + const agentID = AgentV2.ID.make(id) + if (item.disabled) { + draft.remove(agentID) + continue } - if (item.system !== undefined) agent.system = item.system - if (item.description !== undefined) agent.description = item.description - if (item.mode !== undefined) agent.mode = item.mode - if (item.hidden !== undefined) agent.hidden = item.hidden - if (item.color !== undefined) agent.color = item.color - if (item.steps !== undefined) agent.steps = item.steps - }) - if (item.permissions !== undefined) { - permissions.set(agentID, [...(permissions.get(agentID) ?? []), ...item.permissions]) + const exists = draft.get(agentID) !== undefined + draft.update(agentID, (agent) => { + if (!exists) agent.permissions.push(...permissions) + if (item.model !== undefined) { + const model = ModelV2.parse(item.model) + agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant } + } + if (item.variant !== undefined && agent.model !== undefined) { + agent.model.variant = ModelV2.VariantID.make(item.variant) + } + if (item.request !== undefined) { + Object.assign(agent.request.headers, item.request.headers ?? {}) + Object.assign(agent.request.body, item.request.body ?? {}) + } + if (item.system !== undefined) agent.system = item.system + if (item.description !== undefined) agent.description = item.description + if (item.mode !== undefined) agent.mode = item.mode + if (item.hidden !== undefined) agent.hidden = item.hidden + if (item.color !== undefined) agent.color = item.color + if (item.steps !== undefined) agent.steps = item.steps + if (item.permissions !== undefined) { + agent.permissions.push(...expandPermissions(item.permissions, global.home)) + } + }) } } - } - - const global = files.flatMap((file) => file.info.permissions ?? []) - for (const current of editor.list()) { - editor.update(current.id, (agent) => { - agent.permissions.push(...global, ...(permissions.get(current.id) ?? [])) - }) - } - }) + }), + ) }), }) + +function expandPermissions(rules: PermissionV2.Ruleset, home: string): PermissionV2.Ruleset { + // Expand only resources tools resolve as filesystem paths. Bash resources are raw shell text: + // rewriting `$HOME/private/**` would miss `$HOME/private/key`, and safe expansion needs shell-aware parsing. + return rules.map((rule) => + isPathAction(rule.action) ? { ...rule, resource: expandHome(rule.resource, home) } : rule, + ) +} + +function isPathAction(action: string): action is PathAction { + return pathActions.some((item) => item === action) +} + +function expandHome(resource: string, home: string) { + if (resource.startsWith("~/")) return home + resource.slice(1) + if (resource === "~") return home + if (resource === "$HOME") return home + if (resource.startsWith("$HOME/")) return home + resource.slice(5) + if (resource.startsWith("$HOME\\")) return home + resource.slice(5) + return resource +} + +function discover(fs: FSUtil.Interface, directory: string) { + return Effect.forEach(legacySources, (source) => + fs + .glob(source.pattern, { cwd: directory, absolute: true, dot: true, symlink: true }) + .pipe( + Effect.map((files) => files.toSorted().map((filepath) => ({ directory, filepath, primary: source.primary }))), + ), + ).pipe( + Effect.map((files) => files.flat()), + Effect.catch(() => Effect.succeed([])), + ) +} + +function decode(file: { directory: string; filepath: string; primary: boolean }, content: string) { + const markdown = ConfigMarkdown.parseOption(content) + if (!markdown) return + const name = path + .relative(file.directory, file.filepath) + .replaceAll("\\", "/") + .replace(/^(agent|agents|mode|modes)\//, "") + .replace(/\.md$/, "") + const body = markdown.content.trim() + const legacy = Object.keys(markdown.data).some((key) => !agentKeys.has(key)) + const agent = Option.getOrUndefined( + legacy + ? Option.map( + decodeLegacyAgent({ name, ...markdown.data, prompt: body }, { errors: "all", propertyOrder: "original" }), + ConfigMigrateV1.migrateAgent, + ) + : decodeAgent({ ...markdown.data, system: body }, { errors: "all", propertyOrder: "original" }), + ) + if (!agent) return + const info = Option.getOrUndefined( + decodeConfig({ + agents: { [name]: file.primary ? { ...agent, mode: "primary" } : agent }, + }), + ) + if (!info) return + return new Config.Document({ type: "document", path: file.filepath, info }) +} diff --git a/packages/core/src/config/plugin/command.ts b/packages/core/src/config/plugin/command.ts new file mode 100644 index 00000000000..93846386afd --- /dev/null +++ b/packages/core/src/config/plugin/command.ts @@ -0,0 +1,92 @@ +export * as ConfigCommandPlugin from "./command" + +import { define } from "../../plugin/internal" +import path from "path" +import { Effect, Option, Schema } from "effect" +import { Config } from "../../config" +import { FSUtil } from "../../fs-util" +import { ModelV2 } from "../../model" +import { ConfigCommand } from "../command" +import { ConfigMarkdown } from "../markdown" + +const decodeCommand = Schema.decodeUnknownOption(ConfigCommand.Info) + +export const Plugin = define({ + id: "config-command", + effect: Effect.fn(function* (ctx) { + const config = yield* Config.Service + const fs = yield* FSUtil.Service + yield* ctx.command.transform( + Effect.fn(function* (draft) { + const documents = yield* Effect.forEach(yield* config.entries(), (entry) => { + if (entry.type === "document") return Effect.succeed([{ commands: entry.info.commands }]) + return loadDirectory(fs, entry.path).pipe( + Effect.map((commands) => [ + { commands: Object.fromEntries(commands.map((command) => [command.name, command.info])) }, + ]), + ) + }).pipe(Effect.map((documents) => documents.flat())) + + // kilocode_change start - apply partial workflow overrides after every command template is registered + const items = documents.flatMap((document) => Object.entries(document.commands ?? {})) + for (const [name, command] of items) { + if (command.template === undefined) continue + const template = command.template + draft.update(name, (item) => { + item.template = template + }) + } + for (const [name, command] of items) { + if (command.template === undefined && !draft.get(name)) continue + draft.update(name, (item) => { + if (command.description !== undefined) item.description = command.description + if (command.agent !== undefined) item.agent = command.agent + if (command.model !== undefined) { + const model = ModelV2.parse(command.model) + item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant } + } + if (command.variant !== undefined) item.variant = ModelV2.VariantID.make(command.variant) + if (command.variant !== undefined && item.model !== undefined) { + item.model.variant = ModelV2.VariantID.make(command.variant) + } + if (command.subtask !== undefined) item.subtask = command.subtask + }) + } + // kilocode_change end + }), + ) + }), +}) + +function loadDirectory(fs: FSUtil.Interface, directory: string) { + return Effect.gen(function* () { + const files = yield* fs + .glob("{command,commands}/**/*.md", { cwd: directory, absolute: true, dot: true, symlink: true }) + .pipe(Effect.catch(() => Effect.succeed([] as string[]))) + return yield* Effect.forEach(files.toSorted(), (filepath) => + fs.readFileStringSafe(filepath).pipe( + Effect.map((content) => (content === undefined ? undefined : decode(directory, filepath, content))), + Effect.catch(() => Effect.succeed(undefined)), + ), + ).pipe( + Effect.map((commands) => + commands.filter((command): command is { name: string; info: ConfigCommand.Info } => command !== undefined), + ), + ) + }) +} + +function decode(directory: string, filepath: string, content: string) { + const markdown = ConfigMarkdown.parseOption(content) + if (!markdown) return + const info = Option.getOrUndefined(decodeCommand({ ...markdown.data, template: markdown.content.trim() })) + if (!info) return + return { + name: path + .relative(directory, filepath) + .replaceAll("\\", "/") + .replace(/^(command|commands)\//, "") + .replace(/\.md$/, ""), + info, + } +} diff --git a/packages/core/src/config/plugin/external.ts b/packages/core/src/config/plugin/external.ts new file mode 100644 index 00000000000..045334e8673 --- /dev/null +++ b/packages/core/src/config/plugin/external.ts @@ -0,0 +1,91 @@ +export * as ConfigExternalPlugin from "./external" + +import type { Plugin as EffectPlugin } from "@kilocode/plugin/v2/effect" +import type { Plugin as PromisePlugin } from "@kilocode/plugin/v2/promise" +import { Effect, Schema } from "effect" +import path from "path" +import { fileURLToPath, pathToFileURL } from "url" +import { Config } from "../../config" +import { FSUtil } from "../../fs-util" +import { Location } from "../../location" +import { Npm } from "../../npm" +import { define } from "../../plugin/internal" +import { PluginPromise } from "../../plugin/promise" + +const PluginModule = Schema.Struct({ + default: Schema.Union([ + Schema.Struct({ + id: Schema.String, + effect: Schema.declare( + (input): input is EffectPlugin["effect"] => typeof input === "function", + ), + }), + Schema.Struct({ + id: Schema.String, + setup: Schema.declare( + (input): input is PromisePlugin["setup"] => typeof input === "function", + ), + }), + ]), +}) + +export const Plugin = define({ + id: "config-plugin", + effect: Effect.fn(function* (ctx) { + const config = yield* Config.Service + const fs = yield* FSUtil.Service + const location = yield* Location.Service + const npm = yield* Npm.Service + yield* Effect.gen(function* () { + const configured: { package: string; options?: Record }[] = [] + + for (const entry of yield* config.entries()) { + if (entry.type === "document") { + const directory = entry.path ? path.dirname(entry.path) : location.directory + for (const item of entry.info.plugins ?? []) { + const ref = typeof item === "string" ? { package: item } : item + const packageName = (() => { + if (ref.package.startsWith("file://")) return fileURLToPath(ref.package) + if (ref.package.startsWith("./") || ref.package.startsWith("../")) { + return path.resolve(directory, ref.package) + } + return ref.package + })() + configured.push({ package: packageName, options: ref.options }) + } + } + + if (entry.type === "directory") { + const files = yield* fs + .glob("{plugin,plugins}/*.{ts,js}", { + cwd: entry.path, + absolute: true, + include: "file", + dot: true, + symlink: true, + }) + .pipe(Effect.orElseSucceed(() => [])) + files.sort() + for (const file of files) configured.push({ package: file }) + } + } + + for (const ref of configured) { + yield* Effect.gen(function* () { + const entrypoint = path.isAbsolute(ref.package) + ? pathToFileURL(ref.package).href + : (yield* npm.add(ref.package)).entrypoint + if (!entrypoint) return + + const mod = yield* Effect.promise(() => import(entrypoint)) + const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default + const plugin = "effect" in value ? value : PluginPromise.fromPromise(value) + yield* ctx.plugin.add({ + id: plugin.id, + effect: (host) => plugin.effect({ ...host, options: ref.options ?? {} }), + }) + }).pipe(Effect.ignoreCause) + } + }).pipe(Effect.forkScoped({ startImmediately: true })) + }), +}) diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts index fca2e53302e..6f6e0528da7 100644 --- a/packages/core/src/config/plugin/provider.ts +++ b/packages/core/src/config/plugin/provider.ts @@ -1,95 +1,113 @@ export * as ConfigProviderPlugin from "./provider" +import { define } from "../../plugin/internal" import { Effect } from "effect" -import { Catalog } from "../../catalog" import { Config } from "../../config" import { ModelV2 } from "../../model" -import { PluginV2 } from "../../plugin" import { ProviderV2 } from "../../provider" -export const Plugin = PluginV2.define({ - id: PluginV2.ID.make("config-provider"), - effect: Effect.gen(function* () { - const catalog = yield* Catalog.Service +export const Plugin = define({ + id: "config-provider", + effect: Effect.fn(function* (ctx) { const config = yield* Config.Service - const transform = yield* catalog.transform() - const files = yield* config.get() - - yield* transform((catalog) => { - for (const file of files) { - for (const [id, item] of Object.entries(file.info.providers ?? {})) { - const providerID = ProviderV2.ID.make(id) - catalog.provider.update(providerID, (provider) => { - if (item.name !== undefined) provider.name = item.name - if (item.env !== undefined) provider.env = [...item.env] - provider.enabled = { via: "custom", data: {} } - if (item.endpoint !== undefined) provider.endpoint = { ...item.endpoint } - if (item.options !== undefined) { - Object.assign(provider.options.headers, item.options.headers ?? {}) - Object.assign(provider.options.body, item.options.body ?? {}) - Object.assign(provider.options.aisdk.provider, item.options.aisdk?.provider ?? {}) - Object.assign(provider.options.aisdk.request, item.options.aisdk?.request ?? {}) + yield* ctx.integration.transform( + Effect.fn(function* (integrations) { + const files = (yield* config.entries()).filter((entry): entry is Config.Document => entry.type === "document") + const configuredIntegrations = new Set( + files.flatMap((file) => + Object.entries(file.info.providers ?? {}).flatMap(([id, provider]) => + provider.env === undefined ? [] : [id], + ), + ), + ) + for (const file of files) { + for (const [id, item] of Object.entries(file.info.providers ?? {})) { + const integrationID = id + if (!configuredIntegrations.has(id) && !integrations.get(integrationID)) continue + integrations.update(integrationID, (integration) => { + integration.name = item.name ?? integration.name + }) + if (item.env !== undefined) { + integrations.method.update({ + integrationID, + method: { type: "env", names: [...item.env] }, + }) } - }) + } + } + }), + ) - for (const [id, config] of Object.entries(item.models ?? {})) { - catalog.model.update(providerID, ModelV2.ID.make(id), (model) => { - if (config.api_id !== undefined) model.apiID = config.api_id - if (config.family !== undefined) model.family = config.family - if (config.name !== undefined) model.name = config.name - if (config.endpoint !== undefined) model.endpoint = { ...config.endpoint } - if (config.capabilities !== undefined) { - model.capabilities = { - tools: config.capabilities.tools, - input: [...config.capabilities.input], - output: [...config.capabilities.output], - } - } - if (config.options !== undefined) { - Object.assign(model.options.headers, config.options.headers ?? {}) - Object.assign(model.options.body, config.options.body ?? {}) - Object.assign(model.options.aisdk.provider, config.options.aisdk?.provider ?? {}) - Object.assign(model.options.aisdk.request, config.options.aisdk?.request ?? {}) - if (config.options.variant !== undefined) model.options.variant = config.options.variant + yield* ctx.catalog.transform( + Effect.fn(function* (catalog) { + const entries = yield* config.entries() + const files = entries.filter((entry): entry is Config.Document => entry.type === "document") + const configuredDefault = Config.latest(entries, "model") + if (configuredDefault !== undefined) { + const model = ModelV2.parse(configuredDefault) + catalog.model.default.set(model.providerID, model.modelID) + } + for (const file of files) { + for (const [id, item] of Object.entries(file.info.providers ?? {})) { + const providerID = id + catalog.provider.update(providerID, (provider) => { + if (item.name !== undefined) provider.name = item.name + if (item.api !== undefined) provider.api = { ...item.api } + if (item.request !== undefined) { + Object.assign(provider.request.headers, item.request.headers) + Object.assign(provider.request.body, item.request.body) } - if (config.variants !== undefined) { - for (const variant of config.variants) { - let existing = model.variants.find((item) => item.id === variant.id) - if (!existing) { - existing = { - id: variant.id, - headers: {}, - body: {}, - aisdk: { - provider: {}, - request: {}, - }, + }) + for (const [id, config] of Object.entries(item.models ?? {})) { + catalog.model.update(providerID, id, (model) => { + if (config.family !== undefined) model.family = config.family + if (config.name !== undefined) model.name = config.name + if (config.api !== undefined) model.api = { ...model.api, ...config.api } + if (config.capabilities !== undefined) { + model.capabilities = { + tools: config.capabilities.tools, + input: [...config.capabilities.input], + output: [...config.capabilities.output], + } + } + if (config.request !== undefined) { + Object.assign(model.request.headers, config.request.headers) + Object.assign(model.request.body, config.request.body) + if (config.request.variant !== undefined) model.request.variant = config.request.variant + } + if (config.variants !== undefined) { + for (const variant of config.variants) { + let existing = model.variants.find((item) => item.id === variant.id) + if (!existing) { + existing = { + id: variant.id, + headers: {}, + body: {}, + } + model.variants.push(existing) } - model.variants.push(existing) + Object.assign(existing.headers, variant.headers) + Object.assign(existing.body, variant.body) } - Object.assign(existing.headers, variant.headers ?? {}) - Object.assign(existing.body, variant.body ?? {}) - Object.assign(existing.aisdk.provider, variant.aisdk?.provider ?? {}) - Object.assign(existing.aisdk.request, variant.aisdk?.request ?? {}) } - } - if (config.cost !== undefined) { - model.cost = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({ - tier: cost.tier && { ...cost.tier }, - input: cost.input, - output: cost.output, - cache: { - read: cost.cache?.read ?? 0, - write: cost.cache?.write ?? 0, - }, - })) - } - if (config.disabled !== undefined) model.enabled = !config.disabled - if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit } - }) + if (config.cost !== undefined) { + model.cost = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({ + tier: cost.tier && { ...cost.tier }, + input: cost.input, + output: cost.output, + cache: { + read: cost.cache?.read ?? 0, + write: cost.cache?.write ?? 0, + }, + })) + } + if (config.disabled !== undefined) model.enabled = !config.disabled + if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit } + }) + } } } - } - }) + }), + ) }), }) diff --git a/packages/core/src/config/plugin/reference.ts b/packages/core/src/config/plugin/reference.ts new file mode 100644 index 00000000000..a3170992574 --- /dev/null +++ b/packages/core/src/config/plugin/reference.ts @@ -0,0 +1,73 @@ +export * as ConfigReferencePlugin from "./reference" + +import { define } from "../../plugin/internal" +import path from "path" +import { Effect } from "effect" +import { Config } from "../../config" +import { ConfigReference } from "../reference" +import { Reference } from "../../reference" +import { AbsolutePath } from "../../schema" +import { Global } from "../../global" +import { Location } from "../../location" + +export const Plugin = define({ + id: "core/config-reference", + effect: Effect.fn(function* (ctx) { + const config = yield* Config.Service + const location = yield* Location.Service + const global = yield* Global.Service + yield* ctx.reference.transform( + Effect.fn(function* (draft) { + const entries = new Map() + for (const doc of (yield* config.entries()).filter( + (entry): entry is Config.Document => entry.type === "document", + )) { + // kilocode_change start + const root = path.parse(location.project.directory).root + const directory = location.project.directory === root ? location.directory : location.project.directory + // kilocode_change end + for (const [name, entry] of Object.entries(doc.info.references ?? {})) { + if (!validAlias(name)) continue + const description = typeof entry === "string" ? undefined : entry.description + const hidden = typeof entry === "string" ? undefined : entry.hidden + entries.set( + name, + local(entry) + ? Reference.LocalSource.make({ + type: "local", + path: AbsolutePath.make( + localPath(directory, global.home, typeof entry === "string" ? entry : entry.path), + ), + ...(description === undefined ? {} : { description }), + ...(hidden === undefined ? {} : { hidden }), + }) + : Reference.GitSource.make({ + type: "git", + repository: typeof entry === "string" ? entry : entry.repository, + ...(entry.branch === undefined ? {} : { branch: entry.branch }), + ...(description === undefined ? {} : { description }), + ...(hidden === undefined ? {} : { hidden }), + }), + ) + } + } + for (const [name, source] of entries) draft.add(name, source) + }), + ) + }), +}) + +function validAlias(name: string) { + return name.length > 0 && !/[\/\s`,]/.test(name) +} + +function local(entry: ConfigReference.Entry): entry is string | ConfigReference.Local { + return typeof entry === "string" + ? entry.startsWith(".") || entry.startsWith("/") || entry.startsWith("~") + : "path" in entry +} + +function localPath(directory: string, home: string, value: string) { + if (value.startsWith("~/")) return path.join(home, value.slice(2)) + return path.isAbsolute(value) ? value : path.resolve(directory, value) +} diff --git a/packages/core/src/config/plugin/skill.ts b/packages/core/src/config/plugin/skill.ts new file mode 100644 index 00000000000..765992a765b --- /dev/null +++ b/packages/core/src/config/plugin/skill.ts @@ -0,0 +1,50 @@ +export * as ConfigSkillPlugin from "./skill" + +import { define } from "../../plugin/internal" +import path from "path" +import { Effect } from "effect" +import { Config } from "../../config" +import { AbsolutePath } from "../../schema" +import { SkillV2 } from "../../skill" +import { Global } from "../../global" +import { Location } from "../../location" + +export const Plugin = define({ + id: "config-skill", + effect: Effect.fn(function* (ctx) { + const config = yield* Config.Service + const global = yield* Global.Service + const location = yield* Location.Service + yield* ctx.skill.transform( + Effect.fn(function* (draft) { + const entries = yield* config.entries() + const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : [])) + const items = entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : [])) + for (const directory of directories) { + draft.source( + SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }), + ) + draft.source( + SkillV2.DirectorySource.make({ + type: "directory", + path: AbsolutePath.make(path.join(directory, "skills")), + }), + ) + } + for (const item of items) { + if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) { + draft.source(SkillV2.UrlSource.make({ type: "url", url: item })) + continue + } + const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item + draft.source( + SkillV2.DirectorySource.make({ + type: "directory", + path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)), + }), + ) + } + }), + ) + }), +}) diff --git a/packages/core/src/config/provider.ts b/packages/core/src/config/provider.ts index fbb0e1c3ef2..1b547570783 100644 --- a/packages/core/src/config/provider.ts +++ b/packages/core/src/config/provider.ts @@ -4,13 +4,9 @@ import { Schema } from "effect" import { ProviderV2 } from "../provider" import { ModelV2 } from "../model" -export class Options extends Schema.Class("ConfigV2.Provider.Options")({ +export class Request extends Schema.Class("ConfigV2.Provider.Request")({ headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), body: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - aisdk: Schema.Struct({ - provider: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - request: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - }).pipe(Schema.optional), }) {} class Cache extends Schema.Class("ConfigV2.Model.Cost.Cache")({ @@ -34,19 +30,32 @@ class Limit extends Schema.Class("ConfigV2.Model.Limit")({ output: Schema.Int.pipe(Schema.optional), }) {} +const ModelApi = Schema.Union([ + Schema.Struct({ + id: ModelV2.ID.pipe(Schema.optional), + ...ProviderV2.AISDK.fields, + }), + Schema.Struct({ + id: ModelV2.ID.pipe(Schema.optional), + ...ProviderV2.Native.fields, + }), + Schema.Struct({ + id: ModelV2.ID, + }), +]) + class Model extends Schema.Class("ConfigV2.Model")({ - api_id: ModelV2.ID.pipe(Schema.optional), family: ModelV2.Family.pipe(Schema.optional), name: Schema.String.pipe(Schema.optional), - endpoint: ProviderV2.Endpoint.pipe(Schema.optional), + api: ModelApi.pipe(Schema.optional), capabilities: ModelV2.Capabilities.pipe(Schema.optional), - options: Schema.Struct({ - ...Options.fields, + request: Schema.Struct({ + ...Request.fields, variant: Schema.String.pipe(Schema.optional), }).pipe(Schema.optional), variants: Schema.Struct({ id: ModelV2.VariantID, - ...Options.fields, + ...Request.fields, }).pipe(Schema.Array, Schema.optional), cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional), disabled: Schema.Boolean.pipe(Schema.optional), @@ -56,7 +65,7 @@ class Model extends Schema.Class("ConfigV2.Model")({ export class Info extends Schema.Class("ConfigV2.Provider")({ name: Schema.String.pipe(Schema.optional), env: Schema.String.pipe(Schema.Array, Schema.optional), - endpoint: ProviderV2.Endpoint.pipe(Schema.optional), - options: Options.pipe(Schema.optional), + api: ProviderV2.Api.pipe(Schema.optional), + request: Request.pipe(Schema.optional), models: Schema.Record(Schema.String, Model).pipe(Schema.optional), }) {} diff --git a/packages/core/src/config/reference.ts b/packages/core/src/config/reference.ts index dc9042e6f76..4518eb4f87d 100644 --- a/packages/core/src/config/reference.ts +++ b/packages/core/src/config/reference.ts @@ -5,13 +5,18 @@ import { Schema } from "effect" export class Git extends Schema.Class("ConfigV2.Reference.Git")({ repository: Schema.String, branch: Schema.String.pipe(Schema.optional), + description: Schema.String.pipe(Schema.optional), + hidden: Schema.Boolean.pipe(Schema.optional), }) {} export class Local extends Schema.Class("ConfigV2.Reference.Local")({ path: Schema.String, + description: Schema.String.pipe(Schema.optional), + hidden: Schema.Boolean.pipe(Schema.optional), }) {} export const Entry = Schema.Union([Schema.String, Git, Local]) export type Entry = typeof Entry.Type export const Info = Schema.Record(Schema.String, Entry) +export type Info = typeof Info.Type diff --git a/packages/core/src/control-plane/move-session.ts b/packages/core/src/control-plane/move-session.ts new file mode 100644 index 00000000000..e227f88df53 --- /dev/null +++ b/packages/core/src/control-plane/move-session.ts @@ -0,0 +1,148 @@ +export * as MoveSession from "./move-session" + +import { Context, DateTime, Effect, Layer, Schema } from "effect" +import { makeGlobalNode } from "../effect/app-node" +import { EventV2 } from "../event" +import { Git } from "../git" +import { Location } from "../location" +import { ProjectV2 } from "../project" +import { SessionV2 } from "../session" +import { SessionEvent } from "../session/event" +import { SessionSchema } from "../session/schema" +import { SessionStore } from "../session/store" +import { AbsolutePath, RelativePath } from "../schema" +import path from "path" + +export const Destination = Schema.Struct({ + directory: AbsolutePath, +}).annotate({ identifier: "MoveSession.Destination" }) +export type Destination = typeof Destination.Type + +export const Input = Schema.Struct({ + sessionID: SessionSchema.ID, + destination: Destination, + moveChanges: Schema.optional(Schema.Boolean), +}).annotate({ identifier: "MoveSession.Input" }) +export type Input = typeof Input.Type + +export class DestinationProjectMismatchError extends Schema.TaggedErrorClass()( + "MoveSession.DestinationProjectMismatchError", + { + expected: ProjectV2.ID, + actual: ProjectV2.ID, + }, +) {} + +export class ApplyChangesError extends Schema.TaggedErrorClass()("MoveSession.ApplyChangesError", { + message: Schema.String, +}) {} + +export class CaptureChangesError extends Schema.TaggedErrorClass()( + "MoveSession.CaptureChangesError", + { + message: Schema.String, + }, +) {} + +export class ResetSourceChangesError extends Schema.TaggedErrorClass()( + "MoveSession.ResetSourceChangesError", + { + directory: AbsolutePath, + message: Schema.String, + cause: Schema.optional(Schema.Defect()), + }, +) {} + +export type Error = + | SessionV2.NotFoundError + | DestinationProjectMismatchError + | CaptureChangesError + | ApplyChangesError + | ResetSourceChangesError + +export interface Interface { + readonly moveSession: (input: Input) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/ControlPlaneMoveSession") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const git = yield* Git.Service + const events = yield* EventV2.Service + const project = yield* ProjectV2.Service + const sessions = yield* SessionStore.Service + + const moveSession = Effect.fn("MoveSession.moveSession")(function* (input: Input) { + const current = yield* sessions.get(input.sessionID) + if (!current) return yield* new SessionV2.NotFoundError({ sessionID: input.sessionID }) + const directory = AbsolutePath.make(input.destination.directory) + if (current.location.directory === directory) return + + const source = yield* project.resolve(current.location.directory) + const destination = yield* project.resolve(directory) + if (current.projectID !== destination.id) { + return yield* new DestinationProjectMismatchError({ expected: current.projectID, actual: destination.id }) + } + + const moveChanges = input.moveChanges && source.directory !== destination.directory + const sourceRepository = moveChanges ? yield* git.repo.discover(current.location.directory) : undefined + if (moveChanges && !sourceRepository) + return yield* new CaptureChangesError({ message: "Source is not a Git repository" }) + const patch = sourceRepository + ? yield* git.change + .capture({ repository: sourceRepository, path: current.location.directory }) + .pipe(Effect.mapError((error) => new CaptureChangesError({ message: error.message }))) + : Git.ChangeSet.make("") + if (patch) { + const repository = yield* git.repo.discover(directory) + if (!repository) return yield* new ApplyChangesError({ message: "Destination is not a Git repository" }) + yield* git.change + .apply({ repository, path: directory, changes: patch }) + .pipe(Effect.mapError((error) => new ApplyChangesError({ message: error.message }))) + } + + yield* events.publish(SessionEvent.Moved, { + sessionID: input.sessionID, + location: Location.Ref.make({ directory }), + subdirectory: RelativePath.make(path.relative(destination.directory, directory).replaceAll("\\", "/")), + timestamp: yield* DateTime.now, + }) + + if (patch) { + const repository = yield* git.repo.discover(current.location.directory) + if (!repository) + return yield* new ResetSourceChangesError({ + directory: current.location.directory, + message: "Source is not a Git repository", + }) + yield* git.change + .discard({ + repository, + path: current.location.directory, + index: "preserve", + untracked: "remove", + }) + .pipe( + Effect.mapError( + (error) => + new ResetSourceChangesError({ + directory: current.location.directory, + message: error.message, + cause: error.cause, + }), + ), + ) + } + }) + + return Service.of({ moveSession }) + }), +) + +export const node = makeGlobalNode({ + service: Service, + layer, + deps: [Git.node, EventV2.node, ProjectV2.node, SessionStore.node], +}) diff --git a/packages/core/src/control-plane/workspace.sql.ts b/packages/core/src/control-plane/workspace.sql.ts new file mode 100644 index 00000000000..ef5195216ac --- /dev/null +++ b/packages/core/src/control-plane/workspace.sql.ts @@ -0,0 +1,20 @@ +import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core" +import { ProjectTable } from "../project/sql" +import { ProjectV2 } from "../project" +import { WorkspaceV2 } from "../workspace" + +export const WorkspaceTable = sqliteTable("workspace", { + id: text().$type().primaryKey(), + type: text().notNull(), + name: text().notNull().default(""), + branch: text(), + directory: text(), + extra: text({ mode: "json" }), + project_id: text() + .$type() + .notNull() + .references(() => ProjectTable.id, { onDelete: "cascade" }), + time_used: integer() + .notNull() + .$default(() => Date.now()), +}) diff --git a/packages/core/src/credential.ts b/packages/core/src/credential.ts new file mode 100644 index 00000000000..104b9897eb4 --- /dev/null +++ b/packages/core/src/credential.ts @@ -0,0 +1,452 @@ +export * as Credential from "./credential" + +import { asc, desc, eq } from "drizzle-orm" // kilocode_change +// kilocode_change start +import { Context, Effect, Layer, Option, Schema, Semaphore } from "effect" +// kilocode_change end +import { Credential } from "@opencode-ai/schema/credential" +import { Integration } from "@opencode-ai/schema/integration" +import { Database } from "./database/database" +import { makeGlobalNode } from "./effect/app-node" +import { CredentialTable } from "./credential/sql" +// kilocode_change start +import { FSUtil } from "./fs-util" +import { Global } from "./global" +import { DataMigrationTable } from "./data-migration.sql" +import path from "path" +import { parse as parseKiloAccounts } from "./kilocode/credential-migration" +import { isBusy } from "./kilocode/sqlite-error" +import { NonNegativeInt } from "./schema" +// kilocode_change end + +export const ID = Credential.ID +export type ID = Credential.ID + +export const OAuth = Credential.OAuth +export type OAuth = Credential.OAuth + +export const Key = Credential.Key +export type Key = Credential.Key + +export const Value = Credential.Value +export type Value = Credential.Value + +export class Info extends Schema.Class("Credential.Info")({ + id: ID, + integrationID: Integration.ID, + label: Schema.String, + value: Value, +}) {} + +// kilocode_change start - legacy JSON credential stores that predate the integration credential table +const LegacyOAuth = Schema.Struct({ + type: Schema.Literal("oauth"), + refresh: Schema.String, + access: Schema.String, + expires: NonNegativeInt, + accountId: Schema.optional(Schema.String), + enterpriseUrl: Schema.optional(Schema.String), +}) + +const LegacyKey = Schema.Struct({ + type: Schema.Literal("api"), + key: Schema.String, + metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), +}) + +// recognize config-bootstrap credentials without projecting them into model credentials +const LegacyWellKnown = Schema.Struct({ + type: Schema.Literal("wellknown"), + key: Schema.String, + token: Schema.String, +}) + +const LegacyValue = Schema.Union([LegacyOAuth, LegacyKey]) +const LegacyAuth = Schema.Union([LegacyOAuth, LegacyKey, LegacyWellKnown]) + +const legacyMethod = (integration: Integration.ID, type: "oauth" | "api") => + Integration.MethodID.make( + type === "api" ? "api-key" : integration === Integration.ID.make("openai") ? "chatgpt-browser" : "oauth", + ) + +const legacyValue = (integration: Integration.ID, credential: Schema.Schema.Type): Value => + credential.type === "api" + ? Key.make({ type: "key", key: credential.key, metadata: credential.metadata }) + : OAuth.make({ + type: "oauth", + methodID: legacyMethod(integration, credential.type), + refresh: credential.refresh, + access: credential.access, + expires: credential.expires, + metadata: { + ...(credential.accountId ? { accountID: credential.accountId } : {}), + ...(credential.enterpriseUrl ? { enterpriseURL: credential.enterpriseUrl } : {}), + }, + }) +// kilocode_change end + +export interface Interface { + /** Returns every stored credential. */ + readonly all: () => Effect.Effect + /** Returns stored credentials belonging to one integration. */ + readonly list: (integrationID: Integration.ID) => Effect.Effect + /** Returns one stored credential by ID. */ + readonly get: (id: ID) => Effect.Effect + /** Replaces any credential for an integration and returns the new record. */ + readonly create: (input: { + readonly integrationID: Integration.ID + readonly value: Value + readonly label?: string + }) => Effect.Effect + /** Updates the label or secret value of a stored credential. */ + readonly update: (id: ID, updates: Partial>) => Effect.Effect + /** Removes a stored credential. */ + readonly remove: (id: ID) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/Credential") {} + +// kilocode_change start - preserve Kilo's account JSON stores and reconcile auth.json on every startup +export const legacyImportLayer = Layer.effectDiscard( + Effect.gen(function* () { + const { db } = yield* Database.Service + const fs = yield* FSUtil.Service + const global = yield* Global.Service + // v3 repairs the active-only v2 import while remaining safe for users who already ran it. + const kiloName = "credential.kilo-account-json-v3" + if (!(yield* db.select().from(DataMigrationTable).where(eq(DataMigrationTable.name, kiloName)).get())) { + const current = yield* fs.readJson(path.join(global.data, "account.json")).pipe(Effect.option) + const prior = yield* fs.readJson(path.join(global.data, "auth-v2.json")).pipe(Effect.option) + const raw = Option.isSome(current) ? current.value : Option.getOrUndefined(prior) + const values = parseKiloAccounts(raw).toSorted( + (a, b) => a.connectorID.localeCompare(b.connectorID) || Number(a.active) - Number(b.active), + ) + if (values.length > 0) { + yield* db.transaction((tx) => + Effect.gen(function* () { + const existing = yield* tx.select().from(CredentialTable).all() + const used = new Set() + const created = Date.now() + for (const [index, item] of values.entries()) { + const integration = Integration.ID.make(item.connectorID.replace(/\/+$/, "")) + const value = legacyValue(integration, item.credential) + const current = existing.find( + (row) => + !used.has(row.id) && + row.integration_id === integration && + row.label === item.label && + JSON.stringify(row.value) === JSON.stringify(value), + ) + const time = created + index + if (current) { + used.add(current.id) + yield* tx + .update(CredentialTable) + .set({ time_created: time, time_updated: time }) + .where(eq(CredentialTable.id, current.id)) + .run() + continue + } + yield* tx.insert(CredentialTable).values({ + id: ID.make(`cred_kilo_${Buffer.from(item.id).toString("base64url")}`), + integration_id: integration, + label: item.label, + value, + time_created: time, + time_updated: time, + }) + } + yield* tx.insert(DataMigrationTable).values({ name: kiloName, time_completed: Date.now() }).run() + }), + ) + } + } + const name = "credential.auth-json" + const raw = yield* fs.readJson(path.join(global.data, "auth.json")).pipe(Effect.option) + if (Option.isNone(raw) || typeof raw.value !== "object" || raw.value === null || Array.isArray(raw.value)) return + const decode = Schema.decodeUnknownOption(LegacyValue) + const values = Object.entries(raw.value).flatMap(([integrationID, value]) => { + const decoded = decode(value) + if (Option.isNone(decoded)) return [] + const integration = Integration.ID.make(integrationID.replace(/\/+$/, "")) + return [{ integration, value: legacyValue(integration, decoded.value) }] + }) + const migrated = yield* db.select().from(DataMigrationTable).where(eq(DataMigrationTable.name, name)).get() + const existing = yield* db.select().from(CredentialTable).orderBy(desc(CredentialTable.time_created)).all() + const same = (left: Value, right: Value) => JSON.stringify(left) === JSON.stringify(right) + if ( + migrated && + values.every((item) => { + const current = existing.find((row) => row.integration_id === item.integration) + return current !== undefined && same(current.value, item.value) + }) + ) + return + yield* db.transaction((tx) => + Effect.gen(function* () { + for (const item of values) { + // reconcile on every startup so a released client can update auth.json after import. + const current = yield* tx + .select() + .from(CredentialTable) + .where(eq(CredentialTable.integration_id, item.integration)) + .orderBy(desc(CredentialTable.time_created)) // kilocode_change - reconcile the active imported account + .get() + if (current) { + if (!same(current.value, item.value)) + yield* tx + .update(CredentialTable) + .set({ value: item.value }) + .where(eq(CredentialTable.id, current.id)) + .run() + continue + } + yield* tx.insert(CredentialTable).values({ + id: ID.create(), + integration_id: item.integration, + label: "Imported", + value: item.value, + }) + } + yield* tx.insert(DataMigrationTable).values({ name, time_completed: Date.now() }).onConflictDoNothing().run() + }), + ) + }).pipe( + Effect.retry({ while: isBusy, times: 2 }), + Effect.catch((error) => + isBusy(error) + ? Effect.logWarning("legacy credential reconciliation deferred because the database is busy") + : Effect.fail(error), + ), + Effect.orDie, + ), +) +// kilocode_change end + +// kilocode_change - retained for Kilo migration and compatibility test layers +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const { db } = yield* Database.Service + const fs = Option.getOrUndefined(yield* Effect.serviceOption(FSUtil.Service)) + const global = Option.getOrUndefined(yield* Effect.serviceOption(Global.Service)) + const decode = Schema.decodeUnknownSync(Value) + const stored = (row: typeof CredentialTable.$inferSelect) => { + if (!row.integration_id) return + return new Info({ + id: row.id, + integrationID: row.integration_id, + label: row.label, + value: decode(row.value), + }) + } + + // kilocode_change start - process-local workspace credentials override host storage without being persisted + const content = process.env.KILO_AUTH_CONTENT + const injected = yield* content === undefined + ? Effect.succeed(new Map()) + : Effect.try({ + try: () => JSON.parse(content) as unknown, + catch: (cause) => cause, + }).pipe( + Effect.flatMap((raw) => { + if (typeof raw !== "object" || raw === null || Array.isArray(raw)) { + return Effect.succeed(new Map()) + } + const decode = Schema.decodeUnknownOption(LegacyAuth) + return Effect.succeed( + new Map( + Object.entries(raw).flatMap(([name, raw]) => { + const decoded = decode(raw) + if (Option.isNone(decoded) || decoded.value.type === "wellknown") return [] + const integration = Integration.ID.make(name.replace(/\/+$/, "")) + return [ + [ + integration, + new Info({ + id: ID.make(`cred_env_${Buffer.from(integration).toString("base64url")}`), + integrationID: integration, + label: "Environment", + value: legacyValue(integration, decoded.value), + }), + ] as const, + ] + }), + ), + ) + }), + Effect.catch((cause) => + Effect.logWarning("invalid KILO_AUTH_CONTENT; using no process-local credentials", { cause }).pipe( + Effect.as(new Map()), + ), + ), + ) + const isolated = content !== undefined + const local = new Map(injected) + const find = (id: ID) => [...local.values()].find((credential) => credential.id === id) + + const lock = Semaphore.makeUnsafe(1) + const writeLegacy = (integration: Integration.ID) => + lock.withPermit( + Effect.gen(function* () { + if (!fs || !global || isolated) return + const file = path.join(global.data, "auth.json") + const raw = yield* fs.readJson(file).pipe( + Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed({})), + Effect.catch((cause) => + Effect.logWarning("failed to read legacy auth.json; preserving existing file", { cause }).pipe( + Effect.as(undefined), + ), + ), + ) + if (raw === undefined) return + const data: Record = + typeof raw === "object" && raw !== null && !Array.isArray(raw) + ? { ...(raw as Record) } + : {} + const row = yield* db + .select() + .from(CredentialTable) + .where(eq(CredentialTable.integration_id, integration)) + .orderBy(desc(CredentialTable.time_created)) // kilocode_change - persist the active imported account + .get() + .pipe(Effect.orDie) + delete data[integration + "/"] + if (!row) delete data[integration] + else { + const value = decode(row.value) + data[integration] = + value.type === "key" + ? { type: "api", key: value.key, metadata: value.metadata } + : { + type: "oauth", + refresh: value.refresh, + access: value.access, + expires: value.expires, + accountId: value.metadata?.accountID, + enterpriseUrl: value.metadata?.enterpriseURL, + } + } + yield* fs.writeJson(file, data, 0o600).pipe(Effect.orDie) + }), + ) + // kilocode_change end + + return Service.of({ + all: Effect.fn("Credential.all")(function* () { + if (isolated) return [...local.values()] // kilocode_change + return (yield* db + .select() + .from(CredentialTable) + .orderBy(asc(CredentialTable.time_created)) + .all() + .pipe(Effect.orDie)).flatMap((row) => { + const credential = stored(row) + return credential ? [credential] : [] + }) + }), + list: Effect.fn("Credential.list")(function* (integrationID) { + // kilocode_change start + if (isolated) { + const credential = local.get(integrationID) + return credential ? [credential] : [] + } + // kilocode_change end + return (yield* db + .select() + .from(CredentialTable) + .where(eq(CredentialTable.integration_id, integrationID)) + .orderBy(asc(CredentialTable.time_created)) + .all() + .pipe(Effect.orDie)).flatMap((row) => { + const credential = stored(row) + return credential ? [credential] : [] + }) + }), + get: Effect.fn("Credential.get")(function* (id) { + if (isolated) return find(id) // kilocode_change - injected workspace credentials are process-local + const row = yield* db.select().from(CredentialTable).where(eq(CredentialTable.id, id)).get().pipe(Effect.orDie) + return row ? stored(row) : undefined + }), + create: Effect.fn("Credential.create")(function* (input) { + const credential = new Info({ + id: ID.create(), + integrationID: input.integrationID, + label: input.label ?? "default", + value: input.value, + }) + // kilocode_change start - credential changes in isolated workspaces are process-local + if (isolated) { + local.set(credential.integrationID, credential) + return credential + } + // kilocode_change end + yield* db + .transaction((tx) => + Effect.gen(function* () { + yield* tx + .delete(CredentialTable) + .where(eq(CredentialTable.integration_id, credential.integrationID)) + .run() + yield* tx + .insert(CredentialTable) + .values({ + id: credential.id, + integration_id: credential.integrationID, + label: credential.label, + value: credential.value, + }) + .run() + }), + ) + .pipe(Effect.orDie) + yield* writeLegacy(credential.integrationID) // kilocode_change + return credential + }), + update: Effect.fn("Credential.update")(function* (id, updates) { + if (!updates.label && !updates.value) return + // kilocode_change start - isolated updates never reach the host database + if (isolated) { + const credential = find(id) + if (!credential) return + local.set( + credential.integrationID, + new Info({ + ...credential, + label: updates.label ?? credential.label, + value: updates.value ?? credential.value, + }), + ) + return + } + const row = yield* db.select().from(CredentialTable).where(eq(CredentialTable.id, id)).get().pipe(Effect.orDie) + // kilocode_change end + yield* db + .update(CredentialTable) + .set({ label: updates.label, value: updates.value }) + .where(eq(CredentialTable.id, id)) + .run() + .pipe(Effect.orDie) + if (row?.integration_id) yield* writeLegacy(row.integration_id) // kilocode_change + }), + remove: Effect.fn("Credential.remove")(function* (id) { + // kilocode_change start - isolated removals remain process-local + if (isolated) { + const credential = find(id) + if (credential) local.delete(credential.integrationID) + return + } + const row = yield* db.select().from(CredentialTable).where(eq(CredentialTable.id, id)).get().pipe(Effect.orDie) + // kilocode_change end + yield* db.delete(CredentialTable).where(eq(CredentialTable.id, id)).run().pipe(Effect.orDie) + if (row?.integration_id) yield* writeLegacy(row.integration_id) // kilocode_change + }), + }) + }), +) + +export const node = makeGlobalNode({ + service: Service, + layer: layer.pipe(Layer.provideMerge(legacyImportLayer)), // kilocode_change + deps: [Database.node, FSUtil.node, Global.node], // kilocode_change +}) diff --git a/packages/core/src/credential/sql.ts b/packages/core/src/credential/sql.ts new file mode 100644 index 00000000000..f6b59197a3b --- /dev/null +++ b/packages/core/src/credential/sql.ts @@ -0,0 +1,14 @@ +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" +import { Timestamps } from "../database/schema.sql" +import type { Credential } from "../credential" + +export const CredentialTable = sqliteTable("credential", { + id: text().$type().primaryKey(), + integration_id: text().$type(), + label: text().notNull(), + value: text({ mode: "json" }).$type().notNull(), + connector_id: text(), + method_id: text(), + active: integer({ mode: "boolean" }), + ...Timestamps, +}) diff --git a/packages/core/src/cross-spawn-spawner.ts b/packages/core/src/cross-spawn-spawner.ts index 5c769569e63..58fb29912ee 100644 --- a/packages/core/src/cross-spawn-spawner.ts +++ b/packages/core/src/cross-spawn-spawner.ts @@ -2,6 +2,10 @@ import type * as Arr from "effect/Array" import { NodeFileSystem, NodeSink, NodeStream } from "@effect/platform-node" import * as NodePath from "@effect/platform-node/NodePath" import { prepareCommand as prepareSandbox } from "@kilocode/sandbox" // kilocode_change +import { tap as tapStdio, tapped } from "./kilocode/stdio-tap" // kilocode_change - Bun drops buffered stdio on close +import * as SpawnExit from "./kilocode/spawn-exit" // kilocode_change +import * as SpawnValidation from "./kilocode/spawn-validation" // kilocode_change +import { settle } from "./kilocode/exit-code" // kilocode_change - settle signal termination as 128 + signum import * as Deferred from "effect/Deferred" import * as Effect from "effect/Effect" import * as Exit from "effect/Exit" @@ -25,6 +29,8 @@ import { import * as NodeChildProcess from "node:child_process" import { PassThrough } from "node:stream" import launch from "cross-spawn" +import { makeGlobalNode } from "./effect/app-node" +import { filesystem, path } from "./effect/app-node-platform" const toError = (err: unknown): Error => (err instanceof globalThis.Error ? err : new globalThis.Error(String(err))) @@ -246,13 +252,13 @@ export const make = Effect.gen(function* () { ) => { let stdout = proc.stdout ? NodeStream.fromReadable({ - evaluate: () => proc.stdout!, + evaluate: () => tapped(proc, "stdout"), // kilocode_change - read the spawn-time tap onError: (cause) => toPlatformError("fromReadable(stdout)", toError(cause), command), }) : Stream.empty let stderr = proc.stderr ? NodeStream.fromReadable({ - evaluate: () => proc.stderr!, + evaluate: () => tapped(proc, "stderr"), // kilocode_change - read the spawn-time tap onError: (cause) => toPlatformError("fromReadable(stderr)", toError(cause), command), }) : Stream.empty @@ -263,10 +269,15 @@ export const make = Effect.gen(function* () { return { stdout, stderr, all: Stream.merge(stdout, stderr) } } - const spawn = (command: ChildProcess.StandardCommand, opts: NodeChildProcess.SpawnOptions) => + const spawn = ( + command: ChildProcess.StandardCommand, + opts: NodeChildProcess.SpawnOptions, + direct: boolean, // kilocode_change - avoid shadowing settle + ) => Effect.callback((resume) => { const signal = Deferred.makeUnsafe() const proc = launch(command.command, command.args, opts) + tapStdio(proc) // kilocode_change - must run in the same tick as spawn let end = false let exit: readonly [code: number | null, signal: NodeJS.Signals | null] | undefined proc.on("error", (err) => { @@ -274,6 +285,7 @@ export const make = Effect.gen(function* () { }) proc.on("exit", (...args) => { exit = args + if (direct) Deferred.doneUnsafe(signal, Exit.succeed(args)) // kilocode_change - bounded grep must not await inherited pipes }) proc.on("close", (...args) => { if (end) return @@ -320,6 +332,18 @@ export const make = Effect.gen(function* () { return Effect.fail(toPlatformError("kill", new Error("Failed to kill child process"), command)) }) + // kilocode_change start - inspect descendants owned by commands that settle on direct exit + const groupAlive = (proc: NodeChildProcess.ChildProcess) => { + if (process.platform === "win32") return false + try { + process.kill(-proc.pid!, 0) + return true + } catch { + return false + } + } + // kilocode_change end + const timeout = ( proc: NodeChildProcess.ChildProcess, @@ -363,6 +387,8 @@ export const make = Effect.gen(function* () { function* (command) { switch (command._tag) { case "StandardCommand": { + const validation = SpawnValidation.take(command) // kilocode_change - retain target validation through preparation + const direct = SpawnExit.take(command) // kilocode_change - opt selected commands into direct-exit settlement const dir = yield* cwd(command.options) // kilocode_change start - prepare agent-scoped commands through the selected sandbox backend const target = yield* prepareSandbox(command, dir, env(command.options)) @@ -372,23 +398,50 @@ export const make = Effect.gen(function* () { const extra = fds(target.options) // kilocode_change end + // kilocode_change start - close target-swap races at the raw spawn boundary + if (validation) + yield* validation.pipe( + Effect.mapError((cause) => + PlatformError.systemError({ + _tag: "Unknown", + module: "ChildProcess", + method: "validate", + pathOrDescriptor: command.command, + cause, + }), + ), + ) + // kilocode_change end + const [proc, signal] = yield* Effect.acquireRelease( // kilocode_change start - spawn the prepared command and options - spawn(target, { - cwd: dir, - env: env(target.options), - stdio: stdios(sin, sout, serr, extra), - detached: target.options.detached ?? process.platform !== "win32", - shell: target.options.shell, - // kilocode_change end - windowsHide: process.platform === "win32", - }), + spawn( + target, + { + cwd: dir, + env: env(target.options), + stdio: stdios(sin, sout, serr, extra), + detached: target.options.detached ?? process.platform !== "win32", + shell: target.options.shell, + // kilocode_change end + windowsHide: process.platform === "win32", + }, + direct, // kilocode_change + ), Effect.fnUntraced(function* ([proc, signal]) { const done = yield* Deferred.isDone(signal) const kill = timeout(proc, command, target.options) // kilocode_change if (done) { const [code] = yield* Deferred.await(signal) if (process.platform === "win32") return yield* Effect.void + // kilocode_change start - clean up only descendants owned by direct-settling commands + if (direct && groupAlive(proc)) { + yield* Effect.ignore(killGroup(command, proc, target.options.killSignal ?? "SIGTERM")) + yield* Effect.sleep("100 millis") + if (groupAlive(proc)) yield* Effect.ignore(killGroup(command, proc, "SIGKILL")) + return yield* Effect.void + } + // kilocode_change end if (code !== 0 && Predicate.isNotNull(code)) return yield* Effect.ignore(kill(killGroup)) return yield* Effect.void } @@ -420,16 +473,7 @@ export const make = Effect.gen(function* () { getInputFd: fd.getInputFd, getOutputFd: fd.getOutputFd, isRunning: Effect.map(Deferred.isDone(signal), (done) => !done), - exitCode: Effect.flatMap(Deferred.await(signal), ([code, signal]) => { - if (Predicate.isNotNull(code)) return Effect.succeed(ExitCode(code)) - return Effect.fail( - toPlatformError( - "exitCode", - new Error(`Process interrupted due to receipt of signal: '${signal}'`), - command, - ), - ) - }), + exitCode: Effect.flatMap(Deferred.await(signal), settle), // kilocode_change - signal termination settles as 128 + signum kill: (opts?: ChildProcess.KillOptions) => { const sig = opts?.killSignal ?? "SIGTERM" const send = (s: NodeJS.Signals) => @@ -503,11 +547,11 @@ export const make = Effect.gen(function* () { return makeSpawner(spawnCommand) }) -export const layer: Layer.Layer = Layer.effect( +const layer: Layer.Layer = Layer.effect( ChildProcessSpawner, make, ) -export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer), Layer.provide(NodePath.layer)) +export const node = makeGlobalNode({ service: ChildProcessSpawner, layer, deps: [filesystem, path] }) export * as CrossSpawnSpawner from "./cross-spawn-spawner" diff --git a/packages/opencode/src/data-migration.sql.ts b/packages/core/src/data-migration.sql.ts similarity index 100% rename from packages/opencode/src/data-migration.sql.ts rename to packages/core/src/data-migration.sql.ts diff --git a/packages/core/src/database/database.ts b/packages/core/src/database/database.ts new file mode 100644 index 00000000000..b1feddee646 --- /dev/null +++ b/packages/core/src/database/database.ts @@ -0,0 +1,75 @@ +export * as Database from "./database" + +import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" +import { layer as sqliteLayer } from "#sqlite" +import { Context, Effect, Layer } from "effect" +import { Global } from "../global" +import { Flag } from "../flag/flag" +import { isAbsolute, join } from "path" +import { existsSync } from "fs" // kilocode_change +import { DbPreflight } from "../kilocode/db-preflight" // kilocode_change +import { ensure as compat } from "../kilocode/database-compat" // kilocode_change +import { DatabaseMigration } from "./migration" +import { InstallationChannel } from "../installation/version" +import { makeGlobalNode } from "../effect/app-node" + +const makeDatabase = EffectDrizzleSqlite.makeWithDefaults() +type DatabaseShape = Effect.Success + +export interface Interface { + db: DatabaseShape +} + +export class Service extends Context.Service()("@opencode/v2/storage/Database") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const db = yield* makeDatabase + + // kilocode_change start - install SQLite's busy handler before concurrent processes can race to recover the WAL + yield* db.run("PRAGMA busy_timeout = 5000") + yield* db.run("PRAGMA journal_mode = WAL") + // kilocode_change end + yield* db.run("PRAGMA synchronous = NORMAL") + yield* db.run("PRAGMA cache_size = -64000") + yield* db.run("PRAGMA foreign_keys = ON") + yield* db.run("PRAGMA wal_checkpoint(PASSIVE)") + yield* DatabaseMigration.apply(db) + yield* compat(db) // kilocode_change - keep the shared database usable by released CLIs + + return { db } + }).pipe(Effect.orDie), +) + +export function layerFromPath(filename: string) { + DbPreflight.assertWritable(filename) // kilocode_change - actionable error (and self-heal for kilo-owned files) instead of an opaque wal_checkpoint crash on read-only db files + return layer.pipe(Layer.provide(sqliteLayer({ filename, disableWAL: true }))) // kilocode_change - Database configures WAL after busy_timeout +} + +export function path() { + if (Flag.KILO_DB) { + if (Flag.KILO_DB === ":memory:" || isAbsolute(Flag.KILO_DB)) return Flag.KILO_DB + return join(Global.Path.data, Flag.KILO_DB) + } + if ( + ["latest", "beta", "prod"].includes(InstallationChannel) || + process.env.KILO_DISABLE_CHANNEL_DB === "1" || + process.env.KILO_DISABLE_CHANNEL_DB === "true" + ) + return join(Global.Path.data, "kilo.db") + // kilocode_change start - kilo-branded dev-channel db name, falling back to a pre-existing opencode-named db + const safe = InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-") + const next = join(Global.Path.data, `kilo-${safe}.db`) + const prev = join(Global.Path.data, `opencode-${safe}.db`) + if (!existsSync(next) && existsSync(prev)) return prev + return next + // kilocode_change end +} + +// kilocode_change - resolve the database path when the layer builds, not at module evaluation, so KILO_DB overrides set after import (tests, embedded hosts) take effect +export const node = makeGlobalNode({ + service: Service, + layer: Layer.unwrap(Effect.sync(() => layerFromPath(path()))), + deps: [], +}) diff --git a/packages/core/src/database/migration.gen.ts b/packages/core/src/database/migration.gen.ts new file mode 100644 index 00000000000..bd0c9850dc1 --- /dev/null +++ b/packages/core/src/database/migration.gen.ts @@ -0,0 +1,45 @@ +import type { DatabaseMigration } from "./migration" + +export const migrations = ( + await Promise.all([ + import("./migration/20260127222353_familiar_lady_ursula"), + import("./migration/20260211171708_add_project_commands"), + import("./migration/20260213144116_wakeful_the_professor"), + import("./migration/20260225215848_workspace"), + import("./migration/20260227213759_add_session_workspace_id"), + import("./migration/20260228203230_blue_harpoon"), + import("./migration/20260303231226_add_workspace_fields"), + import("./migration/20260309230000_move_org_to_state"), + import("./migration/20260312043431_session_message_cursor"), + import("./migration/20260323234822_events"), + import("./migration/20260410174513_workspace-name"), + import("./migration/20260413175956_chief_energizer"), + import("./migration/20260423070820_add_icon_url_override"), + import("./migration/20260427172553_slow_nightmare"), + import("./migration/20260428004200_add_session_path"), + import("./migration/20260501142318_next_venus"), + import("./migration/20260504145000_add_sync_owner"), + import("./migration/20260507164347_add_workspace_time"), + import("./migration/20260510033149_session_usage"), + import("./migration/20260511000411_data_migration_state"), + import("./migration/20260511173437_session-metadata"), + import("./migration/20260601010001_normalize_storage_paths"), + import("./migration/20260601202201_amazing_prowler"), + import("./migration/20260602002951_lowly_union_jack"), + import("./migration/20260602182828_add_project_directories"), + import("./migration/20260603001617_session_message_projection_indexes"), + import("./migration/20260603040000_session_message_projection_order"), + import("./migration/20260603141458_session_input_inbox"), + import("./migration/20260603160727_jittery_ezekiel_stane"), + import("./migration/20260604172448_event_sourced_session_input"), + import("./migration/20260605003541_add_session_context_snapshot"), + import("./migration/20260605042240_add_context_epoch_agent"), + import("./migration/20260611035744_credential"), + import("./migration/20260611192811_lush_chimera"), + import("./migration/20260612174303_project_dir_strategy"), + import("./migration/20260622142730_simplify_session_context_epoch"), + import("./migration/20260622170816_reset_v2_session_state"), + import("./migration/20260622202450_simplify_session_input"), + import("./migration/20260714141136_session-message-legacy-writer-compat"), + ]) +).map((module) => module.default) satisfies DatabaseMigration.Migration[] diff --git a/packages/core/src/database/migration.ts b/packages/core/src/database/migration.ts new file mode 100644 index 00000000000..388a37e7a30 --- /dev/null +++ b/packages/core/src/database/migration.ts @@ -0,0 +1,86 @@ +export * as DatabaseMigration from "./migration" + +import { sql } from "drizzle-orm" +import { Effect, Semaphore } from "effect" +import type { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" +import { migrations } from "./migration.gen" +import schema from "./schema.gen" + +type Database = EffectDrizzleSqlite.EffectSQLiteDatabase +type Transaction = Parameters[0]>[0] +const lock = Semaphore.makeUnsafe(1) + +export type Migration = { + id: string + up: (tx: Transaction) => Effect.Effect +} + +export function apply(db: Database) { + return lock.withPermit( + Effect.gen(function* () { + const tables = yield* db.all<{ name: string }>( + sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`, + ) + if (tables.some((table) => table.name === "session")) return yield* applyOnly(db, migrations) + if (tables.length > 0) return yield* Effect.die("Database is not empty and has no session table") + yield* db.transaction((tx) => + Effect.gen(function* () { + yield* schema.up(tx) + yield* tx.run( + sql`CREATE TABLE ${sql.identifier("migration")} (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`, + ) + yield* Effect.forEach(migrations, (migration) => + tx.run( + sql`INSERT INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${migration.id}, ${Date.now()})`, + ), + ) + }), + ) + }), + ) +} + +export function applyOnly(db: Database, input: Migration[]) { + return Effect.gen(function* () { + yield* db.run( + sql`CREATE TABLE IF NOT EXISTS ${sql.identifier("migration")} (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`, + ) + let completed = new Set( + (yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id), + ) + if (completed.size === 0) { + // Existing installs used Drizzle's migration journal. Seed the new + // journal once so TypeScript migrations don't replay old SQL. + if ( + yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${"__drizzle_migrations"}`) + ) { + yield* db.run(sql` + INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed) + SELECT name, ${Date.now()} + FROM ${sql.identifier("__drizzle_migrations")} + WHERE name IS NOT NULL + `) + completed = new Set( + (yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id), + ) + } + } + + for (const migration of input) { + if (completed.has(migration.id)) continue + // kilocode_change start - another kilo process may have recorded this migration since the snapshot above; take the write lock and re-check before replaying, or the journal insert dies on the primary key + yield* db.transaction( + (tx) => + Effect.gen(function* () { + if (yield* tx.get(sql`SELECT id FROM ${sql.identifier("migration")} WHERE id = ${migration.id}`)) return + yield* migration.up(tx) + yield* tx.run( + sql`INSERT INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${migration.id}, ${Date.now()})`, + ) + }), + { behavior: "immediate" }, + ) + // kilocode_change end + } + }) +} diff --git a/packages/core/src/database/migration/20260127222353_familiar_lady_ursula.ts b/packages/core/src/database/migration/20260127222353_familiar_lady_ursula.ts new file mode 100644 index 00000000000..468a7103fb3 --- /dev/null +++ b/packages/core/src/database/migration/20260127222353_familiar_lady_ursula.ts @@ -0,0 +1,107 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260127222353_familiar_lady_ursula", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`project\` ( + \`id\` text PRIMARY KEY, + \`worktree\` text NOT NULL, + \`vcs\` text, + \`name\` text, + \`icon_url\` text, + \`icon_color\` text, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`time_initialized\` integer, + \`sandboxes\` text NOT NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`message\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`part\` ( + \`id\` text PRIMARY KEY, + \`message_id\` text NOT NULL, + \`session_id\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_part_message_id_message_id_fk\` FOREIGN KEY (\`message_id\`) REFERENCES \`message\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`permission\` ( + \`project_id\` text PRIMARY KEY, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_permission_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session\` ( + \`id\` text PRIMARY KEY, + \`project_id\` text NOT NULL, + \`parent_id\` text, + \`slug\` text NOT NULL, + \`directory\` text NOT NULL, + \`title\` text NOT NULL, + \`version\` text NOT NULL, + \`share_url\` text, + \`summary_additions\` integer, + \`summary_deletions\` integer, + \`summary_files\` integer, + \`summary_diffs\` text, + \`revert\` text, + \`permission\` text, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`time_compacting\` integer, + \`time_archived\` integer, + CONSTRAINT \`fk_session_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`todo\` ( + \`session_id\` text NOT NULL, + \`content\` text NOT NULL, + \`status\` text NOT NULL, + \`priority\` text NOT NULL, + \`position\` integer NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`todo_pk\` PRIMARY KEY(\`session_id\`, \`position\`), + CONSTRAINT \`fk_todo_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session_share\` ( + \`session_id\` text PRIMARY KEY, + \`id\` text NOT NULL, + \`secret\` text NOT NULL, + \`url\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`fk_session_share_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(`CREATE INDEX \`message_session_idx\` ON \`message\` (\`session_id\`);`) + yield* tx.run(`CREATE INDEX \`part_message_idx\` ON \`part\` (\`message_id\`);`) + yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`) + yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`) + yield* tx.run(`CREATE INDEX \`session_parent_idx\` ON \`session\` (\`parent_id\`);`) + yield* tx.run(`CREATE INDEX \`todo_session_idx\` ON \`todo\` (\`session_id\`);`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260211171708_add_project_commands.ts b/packages/core/src/database/migration/20260211171708_add_project_commands.ts new file mode 100644 index 00000000000..d31a533db3c --- /dev/null +++ b/packages/core/src/database/migration/20260211171708_add_project_commands.ts @@ -0,0 +1,11 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260211171708_add_project_commands", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`project\` ADD \`commands\` text;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260213144116_wakeful_the_professor.ts b/packages/core/src/database/migration/20260213144116_wakeful_the_professor.ts new file mode 100644 index 00000000000..8077182d939 --- /dev/null +++ b/packages/core/src/database/migration/20260213144116_wakeful_the_professor.ts @@ -0,0 +1,23 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260213144116_wakeful_the_professor", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`control_account\` ( + \`email\` text NOT NULL, + \`url\` text NOT NULL, + \`access_token\` text NOT NULL, + \`refresh_token\` text NOT NULL, + \`token_expiry\` integer, + \`active\` integer NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`control_account_pk\` PRIMARY KEY(\`email\`, \`url\`) + ); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260225215848_workspace.ts b/packages/core/src/database/migration/20260225215848_workspace.ts new file mode 100644 index 00000000000..cc816951ef9 --- /dev/null +++ b/packages/core/src/database/migration/20260225215848_workspace.ts @@ -0,0 +1,19 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260225215848_workspace", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`workspace\` ( + \`id\` text PRIMARY KEY, + \`branch\` text, + \`project_id\` text NOT NULL, + \`config\` text NOT NULL, + CONSTRAINT \`fk_workspace_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260227213759_add_session_workspace_id.ts b/packages/core/src/database/migration/20260227213759_add_session_workspace_id.ts new file mode 100644 index 00000000000..430407156df --- /dev/null +++ b/packages/core/src/database/migration/20260227213759_add_session_workspace_id.ts @@ -0,0 +1,12 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260227213759_add_session_workspace_id", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`session\` ADD \`workspace_id\` text;`) + yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260228203230_blue_harpoon.ts b/packages/core/src/database/migration/20260228203230_blue_harpoon.ts new file mode 100644 index 00000000000..83e2978f707 --- /dev/null +++ b/packages/core/src/database/migration/20260228203230_blue_harpoon.ts @@ -0,0 +1,30 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260228203230_blue_harpoon", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`account\` ( + \`id\` text PRIMARY KEY, + \`email\` text NOT NULL, + \`url\` text NOT NULL, + \`access_token\` text NOT NULL, + \`refresh_token\` text NOT NULL, + \`token_expiry\` integer, + \`selected_org_id\` text, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`account_state\` ( + \`id\` integer PRIMARY KEY NOT NULL, + \`active_account_id\` text, + FOREIGN KEY (\`active_account_id\`) REFERENCES \`account\`(\`id\`) ON UPDATE no action ON DELETE set null + ); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260303231226_add_workspace_fields.ts b/packages/core/src/database/migration/20260303231226_add_workspace_fields.ts new file mode 100644 index 00000000000..380e9cc68bf --- /dev/null +++ b/packages/core/src/database/migration/20260303231226_add_workspace_fields.ts @@ -0,0 +1,15 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260303231226_add_workspace_fields", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`workspace\` ADD \`type\` text NOT NULL;`) + yield* tx.run(`ALTER TABLE \`workspace\` ADD \`name\` text;`) + yield* tx.run(`ALTER TABLE \`workspace\` ADD \`directory\` text;`) + yield* tx.run(`ALTER TABLE \`workspace\` ADD \`extra\` text;`) + yield* tx.run(`ALTER TABLE \`workspace\` DROP COLUMN \`config\`;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260309230000_move_org_to_state.ts b/packages/core/src/database/migration/20260309230000_move_org_to_state.ts new file mode 100644 index 00000000000..bf39f3e5bf6 --- /dev/null +++ b/packages/core/src/database/migration/20260309230000_move_org_to_state.ts @@ -0,0 +1,15 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260309230000_move_org_to_state", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`account_state\` ADD \`active_org_id\` text;`) + yield* tx.run( + `UPDATE \`account_state\` SET \`active_org_id\` = (SELECT \`selected_org_id\` FROM \`account\` WHERE \`account\`.\`id\` = \`account_state\`.\`active_account_id\`);`, + ) + yield* tx.run(`ALTER TABLE \`account\` DROP COLUMN \`selected_org_id\`;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260312043431_session_message_cursor.ts b/packages/core/src/database/migration/20260312043431_session_message_cursor.ts new file mode 100644 index 00000000000..1603c3fa739 --- /dev/null +++ b/packages/core/src/database/migration/20260312043431_session_message_cursor.ts @@ -0,0 +1,16 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260312043431_session_message_cursor", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DROP INDEX IF EXISTS \`message_session_idx\`;`) + yield* tx.run(`DROP INDEX IF EXISTS \`part_message_idx\`;`) + yield* tx.run( + `CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`, + ) + yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260323234822_events.ts b/packages/core/src/database/migration/20260323234822_events.ts new file mode 100644 index 00000000000..2b1996fbacc --- /dev/null +++ b/packages/core/src/database/migration/20260323234822_events.ts @@ -0,0 +1,26 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260323234822_events", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`event_sequence\` ( + \`aggregate_id\` text PRIMARY KEY, + \`seq\` integer NOT NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`event\` ( + \`id\` text PRIMARY KEY, + \`aggregate_id\` text NOT NULL, + \`seq\` integer NOT NULL, + \`type\` text NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_event_aggregate_id_event_sequence_aggregate_id_fk\` FOREIGN KEY (\`aggregate_id\`) REFERENCES \`event_sequence\`(\`aggregate_id\`) ON DELETE CASCADE + ); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260410174513_workspace-name.ts b/packages/core/src/database/migration/20260410174513_workspace-name.ts new file mode 100644 index 00000000000..18483e1cf08 --- /dev/null +++ b/packages/core/src/database/migration/20260410174513_workspace-name.ts @@ -0,0 +1,29 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260410174513_workspace-name", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`PRAGMA foreign_keys=OFF;`) + yield* tx.run(` + CREATE TABLE \`__new_workspace\` ( + \`id\` text PRIMARY KEY, + \`type\` text NOT NULL, + \`name\` text DEFAULT '' NOT NULL, + \`branch\` text, + \`directory\` text, + \`extra\` text, + \`project_id\` text NOT NULL, + CONSTRAINT \`fk_workspace_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run( + `INSERT INTO \`__new_workspace\`(\`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\`) SELECT \`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\` FROM \`workspace\`;`, + ) + yield* tx.run(`DROP TABLE \`workspace\`;`) + yield* tx.run(`ALTER TABLE \`__new_workspace\` RENAME TO \`workspace\`;`) + yield* tx.run(`PRAGMA foreign_keys=ON;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260413175956_chief_energizer.ts b/packages/core/src/database/migration/20260413175956_chief_energizer.ts new file mode 100644 index 00000000000..a03477e09e3 --- /dev/null +++ b/packages/core/src/database/migration/20260413175956_chief_energizer.ts @@ -0,0 +1,24 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260413175956_chief_energizer", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`session_entry\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`type\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_session_entry_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(`CREATE INDEX \`session_entry_session_idx\` ON \`session_entry\` (\`session_id\`);`) + yield* tx.run(`CREATE INDEX \`session_entry_session_type_idx\` ON \`session_entry\` (\`session_id\`,\`type\`);`) + yield* tx.run(`CREATE INDEX \`session_entry_time_created_idx\` ON \`session_entry\` (\`time_created\`);`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260423070820_add_icon_url_override.ts b/packages/core/src/database/migration/20260423070820_add_icon_url_override.ts new file mode 100644 index 00000000000..20b1f9163a4 --- /dev/null +++ b/packages/core/src/database/migration/20260423070820_add_icon_url_override.ts @@ -0,0 +1,14 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260423070820_add_icon_url_override", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + ALTER TABLE \`project\` ADD \`icon_url_override\` text; + UPDATE \`project\` SET \`icon_url_override\` = \`icon_url\` WHERE \`icon_url\` IS NOT NULL; + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260427172553_slow_nightmare.ts b/packages/core/src/database/migration/20260427172553_slow_nightmare.ts new file mode 100644 index 00000000000..32e67decf3a --- /dev/null +++ b/packages/core/src/database/migration/20260427172553_slow_nightmare.ts @@ -0,0 +1,30 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260427172553_slow_nightmare", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`session_message\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`type\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_session_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(`DROP INDEX IF EXISTS \`session_entry_session_idx\`;`) + yield* tx.run(`DROP INDEX IF EXISTS \`session_entry_session_type_idx\`;`) + yield* tx.run(`DROP INDEX IF EXISTS \`session_entry_time_created_idx\`;`) + yield* tx.run(`CREATE INDEX \`session_message_session_idx\` ON \`session_message\` (\`session_id\`);`) + yield* tx.run( + `CREATE INDEX \`session_message_session_type_idx\` ON \`session_message\` (\`session_id\`,\`type\`);`, + ) + yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`) + yield* tx.run(`DROP TABLE \`session_entry\`;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260428004200_add_session_path.ts b/packages/core/src/database/migration/20260428004200_add_session_path.ts new file mode 100644 index 00000000000..a60ef377fc2 --- /dev/null +++ b/packages/core/src/database/migration/20260428004200_add_session_path.ts @@ -0,0 +1,11 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260428004200_add_session_path", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`session\` ADD \`path\` text;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260501142318_next_venus.ts b/packages/core/src/database/migration/20260501142318_next_venus.ts new file mode 100644 index 00000000000..6c5b078f8fa --- /dev/null +++ b/packages/core/src/database/migration/20260501142318_next_venus.ts @@ -0,0 +1,12 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260501142318_next_venus", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`session\` ADD \`agent\` text;`) + yield* tx.run(`ALTER TABLE \`session\` ADD \`model\` text;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260504145000_add_sync_owner.ts b/packages/core/src/database/migration/20260504145000_add_sync_owner.ts new file mode 100644 index 00000000000..33e85549145 --- /dev/null +++ b/packages/core/src/database/migration/20260504145000_add_sync_owner.ts @@ -0,0 +1,11 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260504145000_add_sync_owner", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`event_sequence\` ADD \`owner_id\` text;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260507164347_add_workspace_time.ts b/packages/core/src/database/migration/20260507164347_add_workspace_time.ts new file mode 100644 index 00000000000..df7e90fc931 --- /dev/null +++ b/packages/core/src/database/migration/20260507164347_add_workspace_time.ts @@ -0,0 +1,11 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260507164347_add_workspace_time", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`workspace\` ADD \`time_used\` integer NOT NULL DEFAULT 0;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260510033149_session_usage.ts b/packages/core/src/database/migration/20260510033149_session_usage.ts new file mode 100644 index 00000000000..5dcd1f658e7 --- /dev/null +++ b/packages/core/src/database/migration/20260510033149_session_usage.ts @@ -0,0 +1,56 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260510033149_session_usage", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`session\` ADD \`cost\` real DEFAULT 0 NOT NULL;`) + yield* tx.run(`ALTER TABLE \`session\` ADD \`tokens_input\` integer DEFAULT 0 NOT NULL;`) + yield* tx.run(`ALTER TABLE \`session\` ADD \`tokens_output\` integer DEFAULT 0 NOT NULL;`) + yield* tx.run(`ALTER TABLE \`session\` ADD \`tokens_reasoning\` integer DEFAULT 0 NOT NULL;`) + yield* tx.run(`ALTER TABLE \`session\` ADD \`tokens_cache_read\` integer DEFAULT 0 NOT NULL;`) + yield* tx.run(`ALTER TABLE \`session\` ADD \`tokens_cache_write\` integer DEFAULT 0 NOT NULL;`) + yield* tx.run(` + UPDATE session + SET + cost = coalesce(( + SELECT sum(coalesce(json_extract(message.data, '$.cost'), 0)) + FROM message + WHERE message.session_id = session.id + AND json_extract(message.data, '$.role') = 'assistant' + ), 0), + tokens_input = coalesce(( + SELECT sum(coalesce(json_extract(message.data, '$.tokens.input'), 0)) + FROM message + WHERE message.session_id = session.id + AND json_extract(message.data, '$.role') = 'assistant' + ), 0), + tokens_output = coalesce(( + SELECT sum(coalesce(json_extract(message.data, '$.tokens.output'), 0)) + FROM message + WHERE message.session_id = session.id + AND json_extract(message.data, '$.role') = 'assistant' + ), 0), + tokens_reasoning = coalesce(( + SELECT sum(coalesce(json_extract(message.data, '$.tokens.reasoning'), 0)) + FROM message + WHERE message.session_id = session.id + AND json_extract(message.data, '$.role') = 'assistant' + ), 0), + tokens_cache_read = coalesce(( + SELECT sum(coalesce(json_extract(message.data, '$.tokens.cache.read'), 0)) + FROM message + WHERE message.session_id = session.id + AND json_extract(message.data, '$.role') = 'assistant' + ), 0), + tokens_cache_write = coalesce(( + SELECT sum(coalesce(json_extract(message.data, '$.tokens.cache.write'), 0)) + FROM message + WHERE message.session_id = session.id + AND json_extract(message.data, '$.role') = 'assistant' + ), 0) + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260511000411_data_migration_state.ts b/packages/core/src/database/migration/20260511000411_data_migration_state.ts new file mode 100644 index 00000000000..7ff0b661891 --- /dev/null +++ b/packages/core/src/database/migration/20260511000411_data_migration_state.ts @@ -0,0 +1,16 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260511000411_data_migration_state", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`data_migration\` ( + \`name\` text PRIMARY KEY, + \`time_completed\` integer NOT NULL + ); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260511173437_session-metadata.ts b/packages/core/src/database/migration/20260511173437_session-metadata.ts new file mode 100644 index 00000000000..413f086671d --- /dev/null +++ b/packages/core/src/database/migration/20260511173437_session-metadata.ts @@ -0,0 +1,16 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260511173437_session-metadata", + up(tx) { + return Effect.gen(function* () { + // This column briefly shipped again under 20260530232709_lovely_romulus. + if ( + (yield* tx.all<{ name: string }>(`PRAGMA table_info(\`session\`)`)).some((column) => column.name === "metadata") + ) + return + yield* tx.run(`ALTER TABLE \`session\` ADD \`metadata\` text;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260601010001_normalize_storage_paths.ts b/packages/core/src/database/migration/20260601010001_normalize_storage_paths.ts new file mode 100644 index 00000000000..f3764e6aa6c --- /dev/null +++ b/packages/core/src/database/migration/20260601010001_normalize_storage_paths.ts @@ -0,0 +1,22 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260601010001_normalize_storage_paths", + up(tx) { + return Effect.gen(function* () { + yield* tx.run( + `UPDATE project SET worktree = REPLACE(worktree, char(92), '/') WHERE worktree GLOB '[A-Za-z]:' || char(92) || '*' OR worktree LIKE char(92) || char(92) || '%';`, + ) + yield* tx.run( + `UPDATE project SET sandboxes = REPLACE(sandboxes, char(92) || char(92), '/') WHERE instr(sandboxes, char(92)) > 0 AND (worktree GLOB '[A-Za-z]:*' OR worktree LIKE '//%');`, + ) + yield* tx.run( + `UPDATE session SET directory = REPLACE(directory, char(92), '/') WHERE directory GLOB '[A-Za-z]:' || char(92) || '*' OR directory LIKE char(92) || char(92) || '%';`, + ) + yield* tx.run( + `UPDATE session SET path = REPLACE(path, char(92), '/') WHERE path IS NOT NULL AND instr(path, char(92)) > 0 AND (directory GLOB '[A-Za-z]:*' OR directory LIKE '//%');`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260601202201_amazing_prowler.ts b/packages/core/src/database/migration/20260601202201_amazing_prowler.ts new file mode 100644 index 00000000000..84b619d2fc3 --- /dev/null +++ b/packages/core/src/database/migration/20260601202201_amazing_prowler.ts @@ -0,0 +1,11 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260601202201_amazing_prowler", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DROP TABLE \`permission\`;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260602002951_lowly_union_jack.ts b/packages/core/src/database/migration/20260602002951_lowly_union_jack.ts new file mode 100644 index 00000000000..6c75b52acc7 --- /dev/null +++ b/packages/core/src/database/migration/20260602002951_lowly_union_jack.ts @@ -0,0 +1,24 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260602002951_lowly_union_jack", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`permission\` ( + \`id\` text PRIMARY KEY, + \`project_id\` text NOT NULL, + \`action\` text NOT NULL, + \`resource\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`fk_permission_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run( + `CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260602182828_add_project_directories.ts b/packages/core/src/database/migration/20260602182828_add_project_directories.ts new file mode 100644 index 00000000000..a1200fd3640 --- /dev/null +++ b/packages/core/src/database/migration/20260602182828_add_project_directories.ts @@ -0,0 +1,20 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260602182828_add_project_directories", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`project_directory\` ( + \`project_id\` text NOT NULL, + \`directory\` text NOT NULL, + \`type\` text NOT NULL, + \`time_created\` integer NOT NULL, + CONSTRAINT \`project_directory_pk\` PRIMARY KEY(\`project_id\`, \`directory\`), + CONSTRAINT \`fk_project_directory_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260603001617_session_message_projection_indexes.ts b/packages/core/src/database/migration/20260603001617_session_message_projection_indexes.ts new file mode 100644 index 00000000000..85b5cd94633 --- /dev/null +++ b/packages/core/src/database/migration/20260603001617_session_message_projection_indexes.ts @@ -0,0 +1,19 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260603001617_session_message_projection_indexes", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_idx\`;`) + yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_type_idx\`;`) + yield* tx.run(`CREATE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`) + yield* tx.run( + `CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`, + ) + yield* tx.run( + `CREATE INDEX \`session_message_session_type_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`time_created\`,\`id\`);`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260603040000_session_message_projection_order.ts b/packages/core/src/database/migration/20260603040000_session_message_projection_order.ts new file mode 100644 index 00000000000..fb4f5285cf3 --- /dev/null +++ b/packages/core/src/database/migration/20260603040000_session_message_projection_order.ts @@ -0,0 +1,19 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260603040000_session_message_projection_order", + up(tx) { + return Effect.gen(function* () { + // Pre-launch Session projections were written before durable event persistence + // became unconditional, so they cannot be assigned truthful aggregate order. + yield* tx.run(`DELETE FROM \`session_message\`;`) + yield* tx.run(`ALTER TABLE \`session_message\` ADD COLUMN \`seq\` integer;`) // kilocode_change + yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_type_time_created_id_idx\`;`) + yield* tx.run(`CREATE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`) + yield* tx.run( + `CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260603141458_session_input_inbox.ts b/packages/core/src/database/migration/20260603141458_session_input_inbox.ts new file mode 100644 index 00000000000..329ab15c08b --- /dev/null +++ b/packages/core/src/database/migration/20260603141458_session_input_inbox.ts @@ -0,0 +1,25 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260603141458_session_input_inbox", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`session_input\` ( + \`seq\` integer PRIMARY KEY AUTOINCREMENT, + \`id\` text NOT NULL UNIQUE, + \`session_id\` text NOT NULL, + \`prompt\` text NOT NULL, + \`delivery\` text NOT NULL, + \`promoted_seq\` integer, + \`time_created\` integer NOT NULL, + CONSTRAINT \`fk_session_input_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run( + `CREATE INDEX \`session_input_session_pending_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`seq\`);`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260603160727_jittery_ezekiel_stane.ts b/packages/core/src/database/migration/20260603160727_jittery_ezekiel_stane.ts new file mode 100644 index 00000000000..bcfc1afdd68 --- /dev/null +++ b/packages/core/src/database/migration/20260603160727_jittery_ezekiel_stane.ts @@ -0,0 +1,20 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260603160727_jittery_ezekiel_stane", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DROP INDEX IF EXISTS \`session_input_session_pending_seq_idx\`;`) + yield* tx.run( + `CREATE INDEX IF NOT EXISTS \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`, + ) + yield* tx.run( + `CREATE INDEX IF NOT EXISTS \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`seq\`);`, + ) + yield* tx.run( + `CREATE INDEX IF NOT EXISTS \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260604172448_event_sourced_session_input.ts b/packages/core/src/database/migration/20260604172448_event_sourced_session_input.ts new file mode 100644 index 00000000000..24a31bfae14 --- /dev/null +++ b/packages/core/src/database/migration/20260604172448_event_sourced_session_input.ts @@ -0,0 +1,47 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260604172448_event_sourced_session_input", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DELETE FROM \`session_input\`;`) + yield* tx.run(`DELETE FROM \`session_message\`;`) + yield* tx.run(`DELETE FROM \`event\`;`) + yield* tx.run(`DELETE FROM \`event_sequence\`;`) + yield* tx.run(`UPDATE \`session\` SET \`workspace_id\` = NULL;`) + yield* tx.run(`DELETE FROM \`workspace\`;`) + yield* tx.run(`DROP INDEX IF EXISTS \`event_aggregate_seq_idx\`;`) + yield* tx.run(`CREATE UNIQUE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`) + yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_seq_idx\`;`) + yield* tx.run( + `CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`, + ) + yield* tx.run(`PRAGMA foreign_keys=OFF;`) + yield* tx.run(` + CREATE TABLE \`__new_session_input\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`prompt\` text NOT NULL, + \`delivery\` text NOT NULL, + \`admitted_seq\` integer NOT NULL, + \`promoted_seq\` integer, + \`time_created\` integer NOT NULL, + CONSTRAINT \`fk_session_input_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(`DROP TABLE \`session_input\`;`) + yield* tx.run(`ALTER TABLE \`__new_session_input\` RENAME TO \`session_input\`;`) + yield* tx.run(`PRAGMA foreign_keys=ON;`) + yield* tx.run( + `CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`, + ) + yield* tx.run( + `CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`, + ) + yield* tx.run( + `CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260605003541_add_session_context_snapshot.ts b/packages/core/src/database/migration/20260605003541_add_session_context_snapshot.ts new file mode 100644 index 00000000000..d1648969dda --- /dev/null +++ b/packages/core/src/database/migration/20260605003541_add_session_context_snapshot.ts @@ -0,0 +1,21 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260605003541_add_session_context_snapshot", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`session_context_epoch\` ( + \`session_id\` text PRIMARY KEY, + \`baseline\` text NOT NULL, + \`snapshot\` text NOT NULL, + \`baseline_seq\` integer NOT NULL, + \`replacement_seq\` integer, + \`revision\` integer DEFAULT 0 NOT NULL, + CONSTRAINT \`fk_session_context_epoch_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260605042240_add_context_epoch_agent.ts b/packages/core/src/database/migration/20260605042240_add_context_epoch_agent.ts new file mode 100644 index 00000000000..cefd6ca037a --- /dev/null +++ b/packages/core/src/database/migration/20260605042240_add_context_epoch_agent.ts @@ -0,0 +1,11 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260605042240_add_context_epoch_agent", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`session_context_epoch\` ADD \`agent\` text DEFAULT 'build' NOT NULL;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260611035744_credential.ts b/packages/core/src/database/migration/20260611035744_credential.ts new file mode 100644 index 00000000000..80df43cdf58 --- /dev/null +++ b/packages/core/src/database/migration/20260611035744_credential.ts @@ -0,0 +1,25 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260611035744_credential", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`credential\` ( + \`id\` text PRIMARY KEY, + \`connector_id\` text NOT NULL, + \`method_id\` text NOT NULL, + \`label\` text NOT NULL, + \`value\` text NOT NULL, + \`active\` integer DEFAULT false NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL + ); + `) + yield* tx.run( + `CREATE UNIQUE INDEX \`credential_connector_active_idx\` ON \`credential\` (\`connector_id\`) WHERE "credential"."active" = 1;`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260611192811_lush_chimera.ts b/packages/core/src/database/migration/20260611192811_lush_chimera.ts new file mode 100644 index 00000000000..306b10d5327 --- /dev/null +++ b/packages/core/src/database/migration/20260611192811_lush_chimera.ts @@ -0,0 +1,25 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260611192811_lush_chimera", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DROP INDEX IF EXISTS \`credential_connector_active_idx\`;`) + yield* tx.run(`DROP TABLE \`credential\`;`) + yield* tx.run(` + CREATE TABLE \`credential\` ( + \`id\` text PRIMARY KEY, + \`integration_id\` text, + \`label\` text NOT NULL, + \`value\` text NOT NULL, + \`connector_id\` text, + \`method_id\` text, + \`active\` integer, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL + ); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260612174303_project_dir_strategy.ts b/packages/core/src/database/migration/20260612174303_project_dir_strategy.ts new file mode 100644 index 00000000000..1f09c40a707 --- /dev/null +++ b/packages/core/src/database/migration/20260612174303_project_dir_strategy.ts @@ -0,0 +1,29 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260612174303_project_dir_strategy", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`project_directory\` ADD \`strategy\` text;`) + yield* tx.run(`PRAGMA foreign_keys=OFF;`) + yield* tx.run(` + CREATE TABLE \`__new_project_directory\` ( + \`project_id\` text NOT NULL, + \`directory\` text NOT NULL, + \`type\` text, + \`strategy\` text, + \`time_created\` integer NOT NULL, + CONSTRAINT \`project_directory_pk\` PRIMARY KEY(\`project_id\`, \`directory\`), + CONSTRAINT \`fk_project_directory_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run( + `INSERT INTO \`__new_project_directory\`(\`project_id\`, \`directory\`, \`type\`, \`time_created\`) SELECT \`project_id\`, \`directory\`, \`type\`, \`time_created\` FROM \`project_directory\`;`, + ) + yield* tx.run(`DROP TABLE \`project_directory\`;`) + yield* tx.run(`ALTER TABLE \`__new_project_directory\` RENAME TO \`project_directory\`;`) + yield* tx.run(`PRAGMA foreign_keys=ON;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260622142730_simplify_session_context_epoch.ts b/packages/core/src/database/migration/20260622142730_simplify_session_context_epoch.ts new file mode 100644 index 00000000000..1520bac4c14 --- /dev/null +++ b/packages/core/src/database/migration/20260622142730_simplify_session_context_epoch.ts @@ -0,0 +1,13 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260622142730_simplify_session_context_epoch", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`session_context_epoch\` DROP COLUMN \`agent\`;`) + yield* tx.run(`ALTER TABLE \`session_context_epoch\` DROP COLUMN \`replacement_seq\`;`) + yield* tx.run(`ALTER TABLE \`session_context_epoch\` DROP COLUMN \`revision\`;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260622170816_reset_v2_session_state.ts b/packages/core/src/database/migration/20260622170816_reset_v2_session_state.ts new file mode 100644 index 00000000000..b771a64bb74 --- /dev/null +++ b/packages/core/src/database/migration/20260622170816_reset_v2_session_state.ts @@ -0,0 +1,17 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260622170816_reset_v2_session_state", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DELETE FROM \`session_context_epoch\`;`) + yield* tx.run(`DELETE FROM \`session_input\`;`) + yield* tx.run(`DELETE FROM \`session_message\`;`) + yield* tx.run(`DELETE FROM \`event\`;`) + yield* tx.run(`DELETE FROM \`event_sequence\`;`) + yield* tx.run(`UPDATE \`session\` SET \`workspace_id\` = NULL WHERE \`workspace_id\` IS NOT NULL;`) + yield* tx.run(`DELETE FROM \`workspace\`;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260622202450_simplify_session_input.ts b/packages/core/src/database/migration/20260622202450_simplify_session_input.ts new file mode 100644 index 00000000000..0b5ddd1bfde --- /dev/null +++ b/packages/core/src/database/migration/20260622202450_simplify_session_input.ts @@ -0,0 +1,17 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260622202450_simplify_session_input", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DELETE FROM \`session_context_epoch\`;`) + yield* tx.run(`DELETE FROM \`session_input\`;`) + yield* tx.run(`DELETE FROM \`session_message\`;`) + yield* tx.run(`DELETE FROM \`event\`;`) + yield* tx.run(`DELETE FROM \`event_sequence\`;`) + yield* tx.run(`UPDATE \`session\` SET \`workspace_id\` = NULL WHERE \`workspace_id\` IS NOT NULL;`) + yield* tx.run(`DELETE FROM \`workspace\`;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260714141136_session-message-legacy-writer-compat.ts b/packages/core/src/database/migration/20260714141136_session-message-legacy-writer-compat.ts new file mode 100644 index 00000000000..7028d70c630 --- /dev/null +++ b/packages/core/src/database/migration/20260714141136_session-message-legacy-writer-compat.ts @@ -0,0 +1,30 @@ +// kilocode_change - new file +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260714141136_session-message-legacy-writer-compat", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`__new_session_message\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`type\` text NOT NULL, + \`seq\` integer, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_session_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(`INSERT INTO \`__new_session_message\`(\`id\`, \`session_id\`, \`type\`, \`seq\`, \`time_created\`, \`time_updated\`, \`data\`) SELECT \`id\`, \`session_id\`, \`type\`, \`seq\`, \`time_created\`, \`time_updated\`, \`data\` FROM \`session_message\`;`) + yield* tx.run(`DROP TABLE \`session_message\`;`) + yield* tx.run(`ALTER TABLE \`__new_session_message\` RENAME TO \`session_message\`;`) + yield* tx.run(`CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`) + yield* tx.run(`CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`) + yield* tx.run(`CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`) + yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/path.ts b/packages/core/src/database/path.ts new file mode 100644 index 00000000000..379d5f8aa76 --- /dev/null +++ b/packages/core/src/database/path.ts @@ -0,0 +1,91 @@ +import nodePath from "path" +import { customType } from "drizzle-orm/sqlite-core" +import { AbsolutePath } from "../schema" + +function storagePath(input: string) { + if (process.platform !== "win32") return input + return input.replaceAll("\\", "/") +} + +function isWindowsStoragePath(input: string) { + return /^[A-Za-z]:\//.test(input) || input.startsWith("//") +} + +function absolute(input: string) { + const result = storagePath(input) + if (!nodePath.posix.isAbsolute(result) && !(process.platform === "win32" && isWindowsStoragePath(result))) { + throw new Error(`Path is not absolute: ${input}`) + } + return result +} + +function toPlatform(input: string) { + if (process.platform !== "win32" || !isWindowsStoragePath(input)) return input + return input.replaceAll("/", "\\") +} + +export const absoluteColumn = customType<{ + data: AbsolutePath + driverData: string + driverOutput: string +}>({ + dataType() { + return "text" + }, + toDriver(input) { + return absolute(input) + }, + fromDriver(input) { + return AbsolutePath.make(toPlatform(absolute(input))) + }, +}) + +// Legacy sessions may persist an empty directory. Keep that existing value +// readable while normalizing and validating every real directory. +export const directoryColumn = customType<{ + data: string + driverData: string + driverOutput: string +}>({ + dataType() { + return "text" + }, + toDriver(input) { + return input ? absolute(input) : input + }, + fromDriver(input) { + return input ? toPlatform(absolute(input)) : input + }, +}) + +export const pathColumn = customType<{ + data: string + driverData: string + driverOutput: string +}>({ + dataType() { + return "text" + }, + toDriver(input) { + return storagePath(input) + }, + fromDriver(input) { + return storagePath(input) + }, +}) + +export const absoluteArrayColumn = customType<{ + data: AbsolutePath[] + driverData: string + driverOutput: string +}>({ + dataType() { + return "text" + }, + toDriver(input) { + return JSON.stringify(input.map(absolute)) + }, + fromDriver(input) { + return (JSON.parse(input) as string[]).map((item) => AbsolutePath.make(toPlatform(absolute(item)))) + }, +}) diff --git a/packages/core/src/database/schema.gen.ts b/packages/core/src/database/schema.gen.ts new file mode 100644 index 00000000000..16a39052686 --- /dev/null +++ b/packages/core/src/database/schema.gen.ts @@ -0,0 +1,277 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "./migration" + +export default { + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`workspace\` ( + \`id\` text PRIMARY KEY, + \`type\` text NOT NULL, + \`name\` text DEFAULT '' NOT NULL, + \`branch\` text, + \`directory\` text, + \`extra\` text, + \`project_id\` text NOT NULL, + \`time_used\` integer NOT NULL, + CONSTRAINT \`fk_workspace_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`data_migration\` ( + \`name\` text PRIMARY KEY, + \`time_completed\` integer NOT NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`account_state\` ( + \`id\` integer PRIMARY KEY, + \`active_account_id\` text, + \`active_org_id\` text, + CONSTRAINT \`fk_account_state_active_account_id_account_id_fk\` FOREIGN KEY (\`active_account_id\`) REFERENCES \`account\`(\`id\`) ON DELETE SET NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`account\` ( + \`id\` text PRIMARY KEY, + \`email\` text NOT NULL, + \`url\` text NOT NULL, + \`access_token\` text NOT NULL, + \`refresh_token\` text NOT NULL, + \`token_expiry\` integer, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`control_account\` ( + \`email\` text NOT NULL, + \`url\` text NOT NULL, + \`access_token\` text NOT NULL, + \`refresh_token\` text NOT NULL, + \`token_expiry\` integer, + \`active\` integer NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`control_account_pk\` PRIMARY KEY(\`email\`, \`url\`) + ); + `) + yield* tx.run(` + CREATE TABLE \`credential\` ( + \`id\` text PRIMARY KEY, + \`integration_id\` text, + \`label\` text NOT NULL, + \`value\` text NOT NULL, + \`connector_id\` text, + \`method_id\` text, + \`active\` integer, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`event_sequence\` ( + \`aggregate_id\` text PRIMARY KEY, + \`seq\` integer NOT NULL, + \`owner_id\` text + ); + `) + yield* tx.run(` + CREATE TABLE \`event\` ( + \`id\` text PRIMARY KEY, + \`aggregate_id\` text NOT NULL, + \`seq\` integer NOT NULL, + \`type\` text NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_event_aggregate_id_event_sequence_aggregate_id_fk\` FOREIGN KEY (\`aggregate_id\`) REFERENCES \`event_sequence\`(\`aggregate_id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`permission\` ( + \`id\` text PRIMARY KEY, + \`project_id\` text NOT NULL, + \`action\` text NOT NULL, + \`resource\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`fk_permission_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`project_directory\` ( + \`project_id\` text NOT NULL, + \`directory\` text NOT NULL, + \`type\` text, + \`strategy\` text, + \`time_created\` integer NOT NULL, + CONSTRAINT \`project_directory_pk\` PRIMARY KEY(\`project_id\`, \`directory\`), + CONSTRAINT \`fk_project_directory_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`project\` ( + \`id\` text PRIMARY KEY, + \`worktree\` text NOT NULL, + \`vcs\` text, + \`name\` text, + \`icon_url\` text, + \`icon_url_override\` text, + \`icon_color\` text, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`time_initialized\` integer, + \`sandboxes\` text NOT NULL, + \`commands\` text + ); + `) + yield* tx.run(` + CREATE TABLE \`message\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`part\` ( + \`id\` text PRIMARY KEY, + \`message_id\` text NOT NULL, + \`session_id\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_part_message_id_message_id_fk\` FOREIGN KEY (\`message_id\`) REFERENCES \`message\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session_context_epoch\` ( + \`session_id\` text PRIMARY KEY, + \`baseline\` text NOT NULL, + \`snapshot\` text NOT NULL, + \`baseline_seq\` integer NOT NULL, + CONSTRAINT \`fk_session_context_epoch_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session_input\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`prompt\` text NOT NULL, + \`delivery\` text NOT NULL, + \`admitted_seq\` integer NOT NULL, + \`promoted_seq\` integer, + \`time_created\` integer NOT NULL, + CONSTRAINT \`fk_session_input_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session_message\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`type\` text NOT NULL, + \`seq\` integer, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_session_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session\` ( + \`id\` text PRIMARY KEY, + \`project_id\` text NOT NULL, + \`workspace_id\` text, + \`parent_id\` text, + \`slug\` text NOT NULL, + \`directory\` text NOT NULL, + \`path\` text, + \`title\` text NOT NULL, + \`version\` text NOT NULL, + \`share_url\` text, + \`summary_additions\` integer, + \`summary_deletions\` integer, + \`summary_files\` integer, + \`summary_diffs\` text, + \`metadata\` text, + \`cost\` real DEFAULT 0 NOT NULL, + \`tokens_input\` integer DEFAULT 0 NOT NULL, + \`tokens_output\` integer DEFAULT 0 NOT NULL, + \`tokens_reasoning\` integer DEFAULT 0 NOT NULL, + \`tokens_cache_read\` integer DEFAULT 0 NOT NULL, + \`tokens_cache_write\` integer DEFAULT 0 NOT NULL, + \`revert\` text, + \`permission\` text, + \`agent\` text, + \`model\` text, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`time_compacting\` integer, + \`time_archived\` integer, + CONSTRAINT \`fk_session_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`todo\` ( + \`session_id\` text NOT NULL, + \`content\` text NOT NULL, + \`status\` text NOT NULL, + \`priority\` text NOT NULL, + \`position\` integer NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`todo_pk\` PRIMARY KEY(\`session_id\`, \`position\`), + CONSTRAINT \`fk_todo_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session_share\` ( + \`session_id\` text PRIMARY KEY, + \`id\` text NOT NULL, + \`secret\` text NOT NULL, + \`url\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`fk_session_share_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(`CREATE UNIQUE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`) + yield* tx.run(`CREATE INDEX \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`) + yield* tx.run( + `CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`, + ) + yield* tx.run( + `CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`, + ) + yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`) + yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`) + yield* tx.run( + `CREATE INDEX \`recall_part_search_idx\` ON \`part\` (\`session_id\`,\`id\`,\`message_id\`,json_extract("data", '$.type'),CASE WHEN json_extract("data", '$.type') = 'text' THEN coalesce(json_extract("data", '$.text'), '') WHEN json_extract("data", '$.type') = 'file' THEN trim(coalesce(json_extract("data", '$.filename'), '') || ' ' || CASE WHEN coalesce(json_extract("data", '$.url'), '') NOT LIKE 'data:%' THEN coalesce(json_extract("data", '$.url'), '') ELSE '' END || ' ' || coalesce(json_extract("data", '$.source.path'), '') || ' ' || coalesce(json_extract("data", '$.source.name'), '') || ' ' || CASE WHEN coalesce(json_extract("data", '$.source.uri'), '') NOT LIKE 'data:%' THEN coalesce(json_extract("data", '$.source.uri'), '') ELSE '' END || ' ' || coalesce(json_extract("data", '$.source.clientName'), '')) ELSE coalesce(json_extract("data", '$.state.error'), '') END) WHERE json_valid("part"."data") AND ((json_extract("part"."data", '$.type') = 'text' AND coalesce(json_extract("part"."data", '$.synthetic'), 0) = 0 AND coalesce(json_extract("part"."data", '$.ignored'), 0) = 0) OR json_extract("part"."data", '$.type') = 'file' OR (json_extract("part"."data", '$.type') = 'tool' AND json_extract("part"."data", '$.state.status') = 'error'));`, + ) + yield* tx.run( + `CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`, + ) + yield* tx.run( + `CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`, + ) + yield* tx.run( + `CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`, + ) + yield* tx.run( + `CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`, + ) + yield* tx.run( + `CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`, + ) + yield* tx.run( + `CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`, + ) + yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`) + yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`) + yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`) + yield* tx.run(`CREATE INDEX \`session_parent_idx\` ON \`session\` (\`parent_id\`);`) + yield* tx.run(`CREATE INDEX \`todo_session_idx\` ON \`todo\` (\`session_id\`);`) + }) + }, +} satisfies Omit diff --git a/packages/opencode/src/storage/schema.sql.ts b/packages/core/src/database/schema.sql.ts similarity index 100% rename from packages/opencode/src/storage/schema.sql.ts rename to packages/core/src/database/schema.sql.ts diff --git a/packages/core/src/database/sqlite.bun.ts b/packages/core/src/database/sqlite.bun.ts new file mode 100644 index 00000000000..e15f4c117e4 --- /dev/null +++ b/packages/core/src/database/sqlite.bun.ts @@ -0,0 +1,183 @@ +import { Database } from "bun:sqlite" +import { drizzle } from "drizzle-orm/bun-sqlite" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Scope from "effect/Scope" +import * as Semaphore from "effect/Semaphore" +import * as Stream from "effect/Stream" +import * as Reactivity from "effect/unstable/reactivity/Reactivity" +import * as Client from "effect/unstable/sql/SqlClient" +import type { Connection } from "effect/unstable/sql/SqlConnection" +import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError" +import * as Statement from "effect/unstable/sql/Statement" +import { Sqlite } from "./sqlite" + +const ATTR_DB_SYSTEM_NAME = "db.system.name" + +const TypeId = "~@opencode-ai/core/database/SqliteBun" as const +type TypeId = typeof TypeId + +interface SqliteClient extends Client.SqlClient { + readonly [TypeId]: TypeId + readonly config: Config + readonly export: Effect.Effect + readonly loadExtension: (path: string) => Effect.Effect + readonly updateValues: never +} + +interface Config { + readonly filename: string + readonly readonly?: boolean + readonly create?: boolean + readonly readwrite?: boolean + readonly disableWAL?: boolean + readonly spanAttributes?: Record + readonly transformResultNames?: (str: string) => string + readonly transformQueryNames?: (str: string) => string +} + +interface SqliteConnection extends Connection { + readonly export: Effect.Effect + readonly loadExtension: (path: string) => Effect.Effect +} + +const make = (options: Config) => + Effect.gen(function* () { + const native = (yield* Sqlite.Native) as Database + + const compiler = Statement.makeCompilerSqlite(options.transformQueryNames) + const transformRows = options.transformResultNames + ? Statement.defaultTransforms(options.transformResultNames).array + : undefined + + const run = (query: string, params: ReadonlyArray = []) => + Effect.withFiber>, SqlError>((fiber) => { + const statement = native.query(query) + // @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627 + statement.safeIntegers(Context.get(fiber.context, Client.SafeIntegers)) + try { + return Effect.succeed((statement.all(...(params as any)) ?? []) as Array>) + } catch (cause) { + return Effect.fail( + new SqlError({ + reason: classifySqliteError(cause, { message: "Failed to execute statement", operation: "execute" }), + }), + ) + } + }) + + const runValues = (query: string, params: ReadonlyArray = []) => + Effect.withFiber, SqlError>((fiber) => { + const statement = native.query(query) + // @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627 + statement.safeIntegers(Context.get(fiber.context, Client.SafeIntegers)) + try { + return Effect.succeed((statement.values(...(params as any)) ?? []) as Array) + } catch (cause) { + return Effect.fail( + new SqlError({ + reason: classifySqliteError(cause, { message: "Failed to execute statement", operation: "execute" }), + }), + ) + } + }) + + const connection = identity({ + execute(query, params, transformRows) { + return transformRows ? Effect.map(run(query, params), transformRows) : run(query, params) + }, + executeRaw(query, params) { + return run(query, params) + }, + executeValues(query, params) { + return runValues(query, params) + }, + executeUnprepared(query, params, transformRows) { + return this.execute(query, params, transformRows) + }, + executeStream() { + return Stream.die("executeStream not implemented") + }, + export: Effect.try({ + try: () => native.serialize(), + catch: (cause) => + new SqlError({ + reason: classifySqliteError(cause, { message: "Failed to export database", operation: "export" }), + }), + }), + loadExtension: (path) => + Effect.try({ + try: () => native.loadExtension(path), + catch: (cause) => + new SqlError({ + reason: classifySqliteError(cause, { message: "Failed to load extension", operation: "loadExtension" }), + }), + }), + }) + + const semaphore = yield* Semaphore.make(1) + const acquirer = semaphore.withPermits(1)(Effect.succeed(connection)) + const transactionAcquirer = Effect.uninterruptibleMask((restore) => { + const fiber = Fiber.getCurrent()! + const scope = Context.getUnsafe(fiber.context, Scope.Scope) + return Effect.as( + Effect.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))), + connection, + ) + }) + + const client = Object.assign( + (yield* Client.make({ + acquirer, + compiler, + transactionAcquirer, + spanAttributes: [ + ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), + [ATTR_DB_SYSTEM_NAME, "sqlite"], + ], + transformRows, + })) as SqliteClient, + { + [TypeId]: TypeId, + config: options, + export: Effect.flatMap(acquirer, (_) => _.export), + loadExtension: (path: string) => Effect.flatMap(acquirer, (_) => _.loadExtension(path)), + }, + ) + + return client + }) + +const nativeLayer = (config: Config) => + Layer.effect( + Sqlite.Native, + Effect.gen(function* () { + const native = new Database(config.filename, { + readonly: config.readonly, + readwrite: config.readwrite ?? true, + create: config.create ?? true, + }) + yield* Effect.addFinalizer(() => Effect.sync(() => native.close())) + if (config.disableWAL !== true) native.run("PRAGMA journal_mode = WAL;") + return native + }), + ) + +const sqliteLayer = (config: Config) => Layer.effect(Client.SqlClient, make(config)) + +const drizzleLayer = Layer.effect( + Sqlite.Drizzle, + Effect.gen(function* () { + return drizzle({ client: (yield* Sqlite.Native) as Database }) + }), +) + +export const layer = (config: Config) => { + const native = nativeLayer(config) + return Layer.merge(native, Layer.merge(sqliteLayer(config), drizzleLayer).pipe(Layer.provide(native))).pipe( + Layer.provide(Reactivity.layer), + ) +} diff --git a/packages/core/src/database/sqlite.node.ts b/packages/core/src/database/sqlite.node.ts new file mode 100644 index 00000000000..6eaecbee26e --- /dev/null +++ b/packages/core/src/database/sqlite.node.ts @@ -0,0 +1,178 @@ +import { DatabaseSync, type SQLInputValue } from "node:sqlite" +import { drizzle } from "drizzle-orm/node-sqlite" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Scope from "effect/Scope" +import * as Semaphore from "effect/Semaphore" +import * as Stream from "effect/Stream" +import * as Reactivity from "effect/unstable/reactivity/Reactivity" +import * as Client from "effect/unstable/sql/SqlClient" +import type { Connection } from "effect/unstable/sql/SqlConnection" +import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError" +import * as Statement from "effect/unstable/sql/Statement" +import { Sqlite } from "./sqlite" + +const ATTR_DB_SYSTEM_NAME = "db.system.name" + +const TypeId = "~@opencode-ai/core/database/SqliteNode" as const +type TypeId = typeof TypeId + +interface SqliteClient extends Client.SqlClient { + readonly [TypeId]: TypeId + readonly config: Config + readonly loadExtension: (path: string) => Effect.Effect + readonly updateValues: never +} + +interface Config { + readonly filename: string + readonly readonly?: boolean + readonly create?: boolean + readonly readwrite?: boolean + readonly disableWAL?: boolean + readonly timeout?: number + readonly allowExtension?: boolean + readonly spanAttributes?: Record + readonly transformResultNames?: (str: string) => string + readonly transformQueryNames?: (str: string) => string +} + +interface SqliteConnection extends Connection { + readonly loadExtension: (path: string) => Effect.Effect +} + +const make = (options: Config) => + Effect.gen(function* () { + const native = (yield* Sqlite.Native) as DatabaseSync + + const compiler = Statement.makeCompilerSqlite(options.transformQueryNames) + const transformRows = options.transformResultNames + ? Statement.defaultTransforms(options.transformResultNames).array + : undefined + + const run = (query: string, params: ReadonlyArray = []) => + Effect.withFiber>, SqlError>((fiber) => { + const statement = native.prepare(query) + statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers)) + try { + return Effect.succeed(statement.all(...(params as SQLInputValue[])) as Array>) + } catch (cause) { + return Effect.fail( + new SqlError({ + reason: classifySqliteError(cause, { message: "Failed to execute statement", operation: "execute" }), + }), + ) + } + }) + + const runValues = (query: string, params: ReadonlyArray = []) => + Effect.withFiber>, SqlError>((fiber) => { + const statement = native.prepare(query) + statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers)) + statement.setReturnArrays(true) + try { + return Effect.succeed( + statement.all(...(params as SQLInputValue[])) as unknown as ReadonlyArray>, + ) + } catch (cause) { + return Effect.fail( + new SqlError({ + reason: classifySqliteError(cause, { message: "Failed to execute statement", operation: "execute" }), + }), + ) + } + }) + + const connection = identity({ + execute(query, params, transformRows) { + return transformRows ? Effect.map(run(query, params), transformRows) : run(query, params) + }, + executeRaw(query, params) { + return run(query, params) + }, + executeValues(query, params) { + return runValues(query, params) + }, + executeUnprepared(query, params, transformRows) { + return this.execute(query, params, transformRows) + }, + executeStream() { + return Stream.die("executeStream not implemented") + }, + loadExtension: (path) => + Effect.try({ + try: () => native.loadExtension(path), + catch: (cause) => + new SqlError({ + reason: classifySqliteError(cause, { message: "Failed to load extension", operation: "loadExtension" }), + }), + }), + }) + + const semaphore = yield* Semaphore.make(1) + const acquirer = semaphore.withPermits(1)(Effect.succeed(connection)) + const transactionAcquirer = Effect.uninterruptibleMask((restore) => { + const fiber = Fiber.getCurrent()! + const scope = Context.getUnsafe(fiber.context, Scope.Scope) + return Effect.as( + Effect.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))), + connection, + ) + }) + + const client = Object.assign( + (yield* Client.make({ + acquirer, + compiler, + transactionAcquirer, + spanAttributes: [ + ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), + [ATTR_DB_SYSTEM_NAME, "sqlite"], + ], + transformRows, + })) as SqliteClient, + { + [TypeId]: TypeId, + config: options, + loadExtension: (path: string) => Effect.flatMap(acquirer, (_) => _.loadExtension(path)), + }, + ) + + return client + }) + +const nativeLayer = (config: Config) => + Layer.effect( + Sqlite.Native, + Effect.gen(function* () { + const native = new DatabaseSync(config.filename, { + readOnly: config.readonly, + timeout: config.timeout, + allowExtension: config.allowExtension, + enableForeignKeyConstraints: true, + open: true, + }) + yield* Effect.addFinalizer(() => Effect.sync(() => native.close())) + if (config.disableWAL !== true && config.readonly !== true) native.exec("PRAGMA journal_mode = WAL;") + return native + }), + ) + +const sqliteLayer = (config: Config) => Layer.effect(Client.SqlClient, make(config)) + +const drizzleLayer = Layer.effect( + Sqlite.Drizzle, + Effect.gen(function* () { + return drizzle({ client: (yield* Sqlite.Native) as DatabaseSync }) as unknown as Sqlite.DrizzleClient + }), +) + +export const layer = (config: Config) => { + const native = nativeLayer(config) + return Layer.merge(native, Layer.merge(sqliteLayer(config), drizzleLayer).pipe(Layer.provide(native))).pipe( + Layer.provide(Reactivity.layer), + ) +} diff --git a/packages/core/src/database/sqlite.ts b/packages/core/src/database/sqlite.ts new file mode 100644 index 00000000000..d2304a54737 --- /dev/null +++ b/packages/core/src/database/sqlite.ts @@ -0,0 +1,8 @@ +export * as Sqlite from "./sqlite" + +import { Context } from "effect" +import type { drizzle } from "drizzle-orm/bun-sqlite" + +export type DrizzleClient = ReturnType +export class Native extends Context.Service()("@opencode-ai/core/database/SqliteNative") {} +export class Drizzle extends Context.Service()("@opencode-ai/core/database/SqliteDrizzle") {} diff --git a/packages/core/src/effect/app-node-builder.ts b/packages/core/src/effect/app-node-builder.ts new file mode 100644 index 00000000000..5f01469dab0 --- /dev/null +++ b/packages/core/src/effect/app-node-builder.ts @@ -0,0 +1,23 @@ +import { buildLocationServiceMap } from "../location-services" +import { LocationServiceMap } from "../location-service-map" +import { LayerNode } from "./layer-node" +import { makeGlobalNode } from "./app-node" + +export function build(root: LayerNode.Node, replacements: LayerNode.Replacements = []) { + let allReplacements = replacements + + // Only build the location service map if it's actually needed + if (LayerNode.hasUnbound(root, LocationServiceMap.node) && !hasReplacement(replacements, LocationServiceMap.node)) { + const locationMap = buildLocationServiceMap(replacements) + const locationMapNode = makeGlobalNode({ service: LocationServiceMap.Service, layer: locationMap, deps: [] }) + allReplacements = replacements.concat([[LocationServiceMap.node, locationMapNode]]) + } + + return LayerNode.compile(root, allReplacements) +} + +function hasReplacement(replacements: LayerNode.Replacements, node: LayerNode.Node) { + return replacements.some(([source]) => source.name === node.name) +} + +export * as AppNodeBuilder from "./app-node-builder" diff --git a/packages/core/src/effect/app-node-platform.ts b/packages/core/src/effect/app-node-platform.ts new file mode 100644 index 00000000000..adba0eb9cc9 --- /dev/null +++ b/packages/core/src/effect/app-node-platform.ts @@ -0,0 +1,18 @@ +import { NodeFileSystem, NodePath } from "@effect/platform-node" +import { LLMClient, RequestExecutor } from "@opencode-ai/llm/route" +import { FileSystem, Path } from "effect" +import { FetchHttpClient } from "effect/unstable/http" +import { HttpClient } from "effect/unstable/http" +import { makeGlobalNode } from "./app-node" + +export const filesystem = makeGlobalNode({ service: FileSystem.FileSystem, layer: NodeFileSystem.layer, deps: [] }) +export const path = makeGlobalNode({ service: Path.Path, layer: NodePath.layer, deps: [] }) +export const httpClient = makeGlobalNode({ service: HttpClient.HttpClient, layer: FetchHttpClient.layer, deps: [] }) +export const requestExecutor = makeGlobalNode({ + service: RequestExecutor.Service, + layer: RequestExecutor.layer, + deps: [httpClient], +}) +export const llmClient = makeGlobalNode({ service: LLMClient.Service, layer: LLMClient.layer, deps: [requestExecutor] }) + +export * as LayerNodePlatform from "./app-node-platform" diff --git a/packages/core/src/effect/app-node.ts b/packages/core/src/effect/app-node.ts new file mode 100644 index 00000000000..e8112f921ad --- /dev/null +++ b/packages/core/src/effect/app-node.ts @@ -0,0 +1,14 @@ +import { LayerNode } from "./layer-node" + +export const tags = LayerNode.tags({ + location: ["global"], + global: [], +}) + +export type GlobalNode = LayerNode.Node +export type LocationNode = LayerNode.Node + +export const makeGlobalNode = tags.make("global") +export const makeLocationNode = tags.make("location") + +export * as Node from "./app-node" diff --git a/packages/core/src/effect/dfdf b/packages/core/src/effect/dfdf new file mode 100644 index 00000000000..77f2d579689 --- /dev/null +++ b/packages/core/src/effect/dfdf @@ -0,0 +1 @@ +File to save in: ~/.local/share/opencode/worktree/012780/location-layer-tiers/packages/core/src/effect/ \ No newline at end of file diff --git a/packages/core/src/effect/keyed-mutex.ts b/packages/core/src/effect/keyed-mutex.ts new file mode 100644 index 00000000000..e7c69b7db91 --- /dev/null +++ b/packages/core/src/effect/keyed-mutex.ts @@ -0,0 +1,45 @@ +export * as KeyedMutex from "./keyed-mutex" + +import { Effect, Semaphore } from "effect" + +export interface KeyedMutex { + readonly size: Effect.Effect + readonly withLock: (key: Key) => (effect: Effect.Effect) => Effect.Effect +} + +/** + * Creates an in-memory mutex with one lock per key. Entries are removed when no + * holder or waiter remains. + * + * same key -> queue + * different key -> run independently + * + * `users` counts holders and waiters so an entry is not removed while a waiter + * will reuse it. + */ +export const makeUnsafe = (): KeyedMutex => { + const locks = new Map() + + const withLock = + (key: Key) => + (effect: Effect.Effect) => + Effect.suspend(() => { + const current = locks.get(key) + const entry = current ?? { semaphore: Semaphore.makeUnsafe(1), users: 0 } + if (!current) locks.set(key, entry) + entry.users++ + return entry.semaphore.withPermit(effect).pipe( + Effect.ensuring( + Effect.sync(() => { + entry.users-- + if (entry.users === 0) locks.delete(key) + }), + ), + ) + }) + + return { size: Effect.sync(() => locks.size), withLock } +} + +/** Creates an in-memory keyed mutex inside an Effect workflow. */ +export const make = (): Effect.Effect> => Effect.sync(makeUnsafe) diff --git a/packages/core/src/effect/layer-node.ts b/packages/core/src/effect/layer-node.ts new file mode 100644 index 00000000000..ef979821149 --- /dev/null +++ b/packages/core/src/effect/layer-node.ts @@ -0,0 +1,363 @@ +import { Brand, Context, Layer } from "effect" + +type AnyNode = Node +type RuntimeLayer = Layer.Layer +type NodeList = readonly [] | readonly [Item, ...Item[]] +export type Output = [Item] extends [never] ? never : Item extends Node ? A : never +export type Error = [Item] extends [never] ? never : Item extends Node ? E : never +type NodeTag = [Item] extends [never] ? undefined : Item extends Node ? T : never +type Missing = Exclude> +type CheckDependencies = [ + Missing, Dependencies>, +] extends [never] + ? unknown + : { readonly "Missing dependencies": Missing, Dependencies> } +declare const $OutputType: unique symbol +declare const $ErrorType: unique symbol + +export type Tag = Name & Brand.Brand<"LayerNode.Tag"> + +const makeTag = Brand.nominal() + +export interface Node { + readonly kind: "layer" | "unbound" | "group" + readonly name: string + readonly service?: Context.Service.Any + readonly implementation?: Layer.Any + readonly dependencies: readonly AnyNode[] + readonly tag?: T + readonly [$OutputType]?: () => A + readonly [$ErrorType]?: () => E +} + +type NodeIdentity = + | { readonly service: Context.Service.Any; readonly name?: never } + | { readonly name: string; readonly service?: never } +type DistributiveOmit = A extends unknown ? Omit : never + +export type TagConfig = Readonly> +type TagNames = keyof Config & string +type NodeInTags = Node | undefined> +type CheckTags = [Exclude>] extends [ + never, +] + ? unknown + : { readonly "Invalid tag dependencies": Exclude> } + +export interface Tags { + readonly values: { readonly [Name in TagNames]: Tag } + readonly make: >( + name: Name, + ) => ( + input: DistributiveOmit>, "tag"> & + CheckTags>, + ) => Node, Layer.Error | Error, Tag> +} + +export function tags( + config: Config, +): Tags { + const names = Object.keys(config) as TagNames[] + const values = Object.fromEntries(names.map((name) => [name, makeTag(name)])) as Tags["values"] + return { + values, + make: ((name: TagNames) => (input: DistributiveOmit, "tag">) => + make({ ...input, tag: values[name] })) as Tags["make"], + } +} + +// Nodes --------------------------------------------------------------------- + +type MakeInput< + Implementation extends Layer.Any, + Items extends NodeList, + T extends Tag | undefined = undefined, +> = NodeIdentity & { + readonly layer: Implementation + readonly deps: Items & CheckDependencies> + readonly tag?: T +} + +export function make< + const Implementation extends Layer.Any, + const Items extends NodeList, + const T extends Tag | undefined = undefined, +>( + input: MakeInput, +): Node, Layer.Error | Error, T> { + return { + kind: "layer", + name: input.service !== undefined ? input.service.key : input.name, + service: input.service, + implementation: input.layer, + dependencies: input.deps, + tag: input.tag, + } +} + +export function unbound(service: Context.Key, tag: T): Node { + return { + kind: "unbound", + name: service.key, + service, + dependencies: [], + tag, + } +} + +export function group( + dependencies: Items, +): Node, Error, NodeTag> { + return { kind: "group", name: "group", dependencies } +} + +export type Replacement = readonly [source: AnyNode, replacement: AnyNode | Layer.Any] +export type Replacements = readonly Replacement[] + +type CheckReplacementErrors = [Exclude] extends [never] + ? unknown + : { readonly "New replacement errors": Exclude } + +type CheckReplacement = Item extends readonly [Node, infer Replacement] + ? Replacement extends Node, infer E2, T> + ? CheckReplacementErrors> + : Replacement extends Layer.Layer, infer E2, never> + ? CheckReplacementErrors> + : { readonly "Invalid replacement": Replacement } + : { readonly "Invalid replacement": Item } + +type CheckReplacements = { + readonly [K in keyof Items]: CheckReplacement +} + +type ValidReplacements = Items & CheckReplacements + +function replacementNode(source: AnyNode, replacement: AnyNode | Layer.Any) { + const replacementNode = isNode(replacement) + ? replacement + : make({ + ...nodeMakeIdentity(source), + layer: replacement as Layer.Layer, + deps: [], + tag: source.tag, + }) + if (source.name !== replacementNode.name) { + throw new Error(`Cannot replace ${source.name} with ${replacementNode.name}`) + } + if (source.tag !== replacementNode.tag) { + throw new Error(`Cannot replace ${source.name} across tags`) + } + return replacementNode +} + +function nodeMakeIdentity(node: AnyNode): NodeIdentity { + if (node.service !== undefined) return { service: node.service } + return { name: node.name } +} + +function isNode(input: Layer.Any | AnyNode): input is AnyNode { + return "kind" in input && "dependencies" in input +} + +// Tree ----------------------------------------------------------------------- + +type Visit = (node: AnyNode, context: VisitContext) => Result + +type VisitContext = { + readonly cache: Map + readonly visit: (node: AnyNode) => Result +} + +function walk( + root: AnyNode, + visit: Visit, + options: { + readonly cache?: Map + readonly resolve?: (node: AnyNode) => AnyNode + readonly detectCycles?: boolean + } = {}, +) { + const cache = options.cache ?? new Map() + const visiting = new Set() + const stack: AnyNode[] = [] + + const recur = (node: AnyNode): Result => { + const target = options.resolve?.(node) ?? node + const cached = cache.get(target) + if (cached !== undefined || cache.has(target)) return cached! + + if (options.detectCycles !== false && visiting.has(target)) { + const start = stack.indexOf(target) + throw new Error( + `Cycle detected in layer tree: ${[...stack.slice(start), target].map((item) => item.name).join(" -> ")}`, + ) + } + + visiting.add(target) + stack.push(target) + try { + const result = visit(target, { cache, visit: recur }) + if (!cache.has(target)) cache.set(target, result) + return result + } finally { + stack.pop() + visiting.delete(target) + } + } + + return recur(root) +} + +export function hoist( + root: Node, + tag: T, + replacements?: ValidReplacements, +): { + readonly node: Node + readonly hoisted: Node +} { + const hoisted = new Map() + const replacementMap = replacementMapFrom(replacements) + + const node = walk( + root, + (node, context) => { + if (node.kind === "group") { + return { ...node, dependencies: node.dependencies.map(context.visit) } + } + if (node.tag === tag) { + const existing = hoisted.get(node.name) + if (existing && existing !== node) { + throw new Error(`Tag ${tag} has conflicting implementations for ${node.name}`) + } + hoisted.set(node.name, rewriteReplacementDependencies(node, replacementMap)) + return group([]) + } + if (node.kind === "unbound") { + return node + } + return { ...node, dependencies: node.dependencies.map(context.visit) } + }, + { resolve: (node) => replacementMap.get(node.name) ?? node }, + ) + + return { + node: node as Node, + hoisted: group(Array.from(hoisted.values())) as Node, + } +} + +export function compile( + root: Node, + replacements?: ValidReplacements, +): Layer.Layer { + const replacementMap = replacementMapFrom(replacements) + const cache = new Map() + const compileNode = (node: AnyNode) => + walk( + node, + (node, context) => { + if (node.kind === "unbound") throw new Error(`Unbound layer node: ${node.name}`) + const dependencies = node.dependencies.flatMap(flatten).map(context.visit) + const implementation = node.implementation! as RuntimeLayer + return dependencies.length === 0 + ? implementation + : implementation.pipe(Layer.provide(dependencies as [RuntimeLayer, ...RuntimeLayer[]])) + }, + { cache, resolve: (node) => replacementMap.get(node.name) ?? node }, + ) + const layers = flatten(root).map((node) => compileNode(node)) + const layer = layers.reduce((result, layer) => layer.pipe(Layer.provideMerge(result)), Layer.empty) + return layer as Layer.Layer +} + +function replacementMapFrom(replacements?: Replacements) { + return ( + replacements?.reduce((map, [source, replacement]) => { + const normalized = rewriteReplacementDependencies(replacementNode(source, replacement), map) + const current = new Map([[source.name, normalized]]) + for (const [name, node] of map) map.set(name, rewriteReplacementDependencies(node, current)) + map.set(source.name, normalized) + return map + }, new Map()) ?? new Map() + ) +} + +function rewriteReplacementDependencies(root: AnyNode, replacements: ReadonlyMap) { + if (replacements.size === 0) return root + const cache = new Map() + const visiting = new Set() + const stack: AnyNode[] = [] + + const recur = (node: AnyNode, isRoot = false): AnyNode => { + const target = isRoot ? node : (replacements.get(node.name) ?? node) + const cached = cache.get(target) + if (cached !== undefined || cache.has(target)) return cached! + if (visiting.has(target)) { + const start = stack.indexOf(target) + throw new Error( + `Cycle detected in layer tree: ${[...stack.slice(start), target].map((item) => item.name).join(" -> ")}`, + ) + } + + visiting.add(target) + stack.push(target) + try { + const dependencies = target.dependencies.map((dependency) => recur(dependency)) + const result = dependencies.every((dependency, index) => dependency === target.dependencies[index]) + ? target + : { ...target, dependencies } + cache.set(target, result) + return result + } finally { + stack.pop() + visiting.delete(target) + } + } + + return recur(root, true) +} + +export function hasUnbound(root: Node, source: AnyNode): boolean { + if (source.kind !== "unbound") throw new Error(`Cannot check non-unbound layer node: ${source.name}`) + return walk(root, (node, context) => { + if (node === source) return true + return node.dependencies.some(context.visit) + }) +} + +function flatten(node: AnyNode): readonly AnyNode[] { + return node.kind === "group" ? node.dependencies.flatMap(flatten) : [node] +} + +// kilocode_change start - defer node construction to break circular dependency chains +export function suspend(fn: () => Node): Node { + let cached: Node | undefined + const get = () => { + if (!cached) cached = fn() + return cached + } + return { + get kind() { + return get().kind + }, + get name() { + return get().name + }, + get service() { + return get().service + }, + get implementation() { + return get().implementation + }, + get dependencies() { + return get().dependencies + }, + get tag() { + return get().tag + }, + } +} +// kilocode_change end + +export * as LayerNode from "./layer-node" diff --git a/packages/core/src/effect/logger.ts b/packages/core/src/effect/logger.ts deleted file mode 100644 index 69f9631e06b..00000000000 --- a/packages/core/src/effect/logger.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Cause, Effect, Logger, References } from "effect" -import * as Log from "../util/log" - -type Fields = Record - -const normalizeKey = (key: string) => (key === "sessionID" ? "session.id" : key) - -export interface Handle { - readonly debug: (msg?: unknown, extra?: Fields) => Effect.Effect - readonly info: (msg?: unknown, extra?: Fields) => Effect.Effect - readonly warn: (msg?: unknown, extra?: Fields) => Effect.Effect - readonly error: (msg?: unknown, extra?: Fields) => Effect.Effect - readonly with: (extra: Fields) => Handle -} - -const clean = (input?: Fields): Fields => - Object.fromEntries( - Object.entries(input ?? {}) - .filter((entry) => entry[1] !== undefined && entry[1] !== null) - .map(([key, value]) => [normalizeKey(key), value]), - ) - -const text = (input: unknown): string => { - // oxlint-disable-next-line no-base-to-string - if (Array.isArray(input)) return input.map((item) => String(item)).join(" ") - // oxlint-disable-next-line no-base-to-string - return input === undefined ? "" : String(input) -} - -const call = (run: (msg?: unknown) => Effect.Effect, base: Fields, msg?: unknown, extra?: Fields) => { - const ann = clean({ ...base, ...extra }) - const fx = run(msg) - return Object.keys(ann).length ? Effect.annotateLogs(fx, ann) : fx -} - -export const logger = Logger.make((opts) => { - const extra = clean(opts.fiber.getRef(References.CurrentLogAnnotations)) - const now = opts.date.getTime() - for (const [key, start] of opts.fiber.getRef(References.CurrentLogSpans)) { - extra[`logSpan.${key}`] = `${now - start}ms` - } - if (opts.cause.reasons.length > 0) { - extra.cause = Cause.pretty(opts.cause) - } - - const svc = typeof extra.service === "string" ? extra.service : undefined - if (svc) delete extra.service - const log = svc ? Log.create({ service: svc }) : Log.Default - const msg = text(opts.message) - - switch (opts.logLevel) { - case "Trace": - case "Debug": - return log.debug(msg, extra) - case "Warn": - return log.warn(msg, extra) - case "Error": - case "Fatal": - return log.error(msg, extra) - default: - return log.info(msg, extra) - } -}) - -export const layer = Logger.layer([logger], { mergeWithExisting: false }) - -export const create = (base: Fields = {}): Handle => ({ - debug: (msg, extra) => call((item) => Effect.logDebug(item), base, msg, extra), - info: (msg, extra) => call((item) => Effect.logInfo(item), base, msg, extra), - warn: (msg, extra) => call((item) => Effect.logWarning(item), base, msg, extra), - error: (msg, extra) => call((item) => Effect.logError(item), base, msg, extra), - with: (extra) => create({ ...base, ...extra }), -}) diff --git a/packages/core/src/effect/observability.ts b/packages/core/src/effect/observability.ts deleted file mode 100644 index d9c24369b7d..00000000000 --- a/packages/core/src/effect/observability.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Effect, Layer, Logger } from "effect" -import { FetchHttpClient } from "effect/unstable/http" -import { OtlpLogger, OtlpSerialization } from "effect/unstable/observability" -import * as EffectLogger from "./logger" -import { Flag } from "../flag/flag" -import { InstallationChannel, InstallationVersion } from "../installation/version" -import { ensureProcessMetadata } from "../util/opencode-process" - -const base = Flag.OTEL_EXPORTER_OTLP_ENDPOINT -export const enabled = !!base -const processID = crypto.randomUUID() - -const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS - ? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce( - (acc, x) => { - const [key, ...value] = x.split("=") - acc[key] = value.join("=") - return acc - }, - {} as Record, - ) - : undefined - -export function resource(): { serviceName: string; serviceVersion: string; attributes: Record } { - const processMetadata = ensureProcessMetadata("main") - const attributes: Record = (() => { - const value = process.env.OTEL_RESOURCE_ATTRIBUTES - if (!value) return {} - try { - return Object.fromEntries( - value.split(",").map((entry) => { - const index = entry.indexOf("=") - if (index < 1) throw new Error("Invalid OTEL_RESOURCE_ATTRIBUTES entry") - return [decodeURIComponent(entry.slice(0, index)), decodeURIComponent(entry.slice(index + 1))] - }), - ) - } catch { - return {} - } - })() - - return { - serviceName: "opencode", - serviceVersion: InstallationVersion, - attributes: { - ...attributes, - "deployment.environment.name": InstallationChannel, - "opencode.client": Flag.KILO_CLIENT, - "opencode.process_role": processMetadata.processRole, - "opencode.run_id": processMetadata.runID, - "service.instance.id": processID, - }, - } -} - -function logs() { - return Logger.layer( - [ - EffectLogger.logger, - OtlpLogger.make({ - url: `${base}/v1/logs`, - resource: resource(), - headers, - }), - ], - { mergeWithExisting: false }, - ).pipe(Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer)) -} - -const traces = async () => { - const NodeSdk = await import("@effect/opentelemetry/NodeSdk") - const OTLP = await import("@opentelemetry/exporter-trace-otlp-http") - const SdkBase = await import("@opentelemetry/sdk-trace-base") - - // @effect/opentelemetry creates a NodeTracerProvider but never calls - // register(), so the global @opentelemetry/api context manager stays - // as the no-op default. Non-Effect code (like the AI SDK) that calls - // tracer.startActiveSpan() relies on context.active() to find the - // parent span - without a real context manager every span starts a - // new trace. Registering AsyncLocalStorageContextManager fixes this. - const { AsyncLocalStorageContextManager } = await import("@opentelemetry/context-async-hooks") - const { context } = await import("@opentelemetry/api") - const mgr = new AsyncLocalStorageContextManager() - mgr.enable() - context.setGlobalContextManager(mgr) - - return NodeSdk.layer(() => ({ - resource: resource(), - spanProcessor: new SdkBase.BatchSpanProcessor( - new OTLP.OTLPTraceExporter({ - url: `${base}/v1/traces`, - headers, - }), - ), - })) -} - -export const layer = !base - ? EffectLogger.layer - : Layer.unwrap( - Effect.gen(function* () { - const trace = yield* Effect.promise(traces) - return Layer.mergeAll(trace, logs()) - }), - ) - -export const Observability = { enabled, layer } diff --git a/packages/core/src/effect/runtime.ts b/packages/core/src/effect/runtime.ts index e4f68270989..6ad0f85176c 100644 --- a/packages/core/src/effect/runtime.ts +++ b/packages/core/src/effect/runtime.ts @@ -1,6 +1,6 @@ import { Layer, type Context, ManagedRuntime, type Effect } from "effect" import { memoMap } from "./memo-map" -import { Observability } from "./observability" +import { Observability } from "../observability" export function makeRuntime(service: Context.Service, layer: Layer.Layer) { let rt: ManagedRuntime.ManagedRuntime | undefined diff --git a/packages/core/src/event.ts b/packages/core/src/event.ts index 339fbddecf1..c38de61ef30 100644 --- a/packages/core/src/event.ts +++ b/packages/core/src/event.ts @@ -1,157 +1,641 @@ export * as EventV2 from "./event" -import { Context, Effect, Layer, Option, PubSub, Schema, Stream } from "effect" +import { Cause, Context, Effect, Layer, Option, PubSub, Queue, Schema, Stream } from "effect" +import { Event } from "@opencode-ai/schema/event" +import type { Data, Definition, Payload } from "@opencode-ai/schema/event" +import { and, asc, eq, gt, inArray } from "drizzle-orm" +import { Database } from "./database/database" +import { EventSequenceTable, EventTable } from "./event/sql" +import * as EventStorage from "./kilocode/event-storage" // kilocode_change - released tool content shapes import { Location } from "./location" -import { withStatics } from "./schema" -import { Identifier } from "./util/identifier" - -export const ID = Schema.String.pipe( - Schema.brand("Event.ID"), - withStatics((schema) => ({ create: () => schema.make("evt_" + Identifier.ascending()) })), -) -export type ID = typeof ID.Type - -export type Definition = { - readonly type: Type - readonly version?: number - readonly aggregate?: string - readonly data: DataSchema -} +import { makeGlobalNode } from "./effect/app-node" +import { isDeepStrictEqual } from "node:util" +import { Durable } from "@opencode-ai/schema/durable-event-manifest" + +export const ID = Event.ID +export type ID = import("@opencode-ai/schema/event").ID +export type { Data, Definition, Payload } from "@opencode-ai/schema/event" + +export type Subscriber = (event: Payload) => Effect.Effect +export type Unsubscribe = Effect.Effect -export type Data = Schema.Schema.Type +export const latestSequence = Effect.fn("EventV2.latestSequence")(function* ( + db: Database.Interface["db"], + aggregateID: string, +) { + const row = yield* db + .select({ seq: EventSequenceTable.seq }) + .from(EventSequenceTable) + .where(eq(EventSequenceTable.aggregate_id, aggregateID)) + .get() + .pipe(Effect.orDie) + return row?.seq ?? -1 +}) -export type Payload = { +export type SerializedEvent = { readonly id: ID - readonly type: D["type"] - readonly data: Data - readonly version?: number - readonly location?: Location.Ref - readonly metadata?: Record + readonly type: string + readonly seq: number + readonly aggregateID: string + readonly data: Record } -export type Sync = (event: Payload) => Effect.Effect - -export const registry = new Map() - -export function define(input: { - readonly type: Type - readonly version?: number - readonly aggregate?: string - readonly schema: Fields -}): Schema.Schema>>> & Definition> { - const Data = Schema.Struct(input.schema) - const Payload = Schema.Struct({ - id: ID, - metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), - type: Schema.Literal(input.type), - version: Schema.optional(Schema.Number), - location: Schema.optional(Location.Ref), - data: Data, - }).annotate({ identifier: input.type }) - - const definition = Object.assign(Payload, { - type: input.type, - ...(input.version === undefined ? {} : { version: input.version }), - ...(input.aggregate === undefined ? {} : { aggregate: input.aggregate }), - data: Data, - }) - registry.set(input.type, definition) - return definition as Schema.Schema>>> & - Definition> -} +export class InvalidDurableEventError extends Schema.TaggedErrorClass()( + "EventV2.InvalidDurableEvent", + { + type: Schema.String, + message: Schema.String, + }, +) {} -export function definitions() { - return registry.values().toArray() +const decodeSerializedEvent = (event: SerializedEvent): Payload => { + const definition = Durable.get(event.type) + if (!definition?.durable) { + throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }) + } + return { + id: event.id, + type: definition.type, + durable: { aggregateID: event.aggregateID, seq: event.seq, version: definition.durable.version }, + data: Schema.decodeUnknownSync(definition.data)(EventStorage.decode(definition.type, event.data)), // kilocode_change + } } +export const readAggregate = Effect.fn("EventV2.readAggregate")(function* ( + db: Database.Interface["db"], + input: { + readonly aggregateID: string + readonly after?: number + readonly limit: number + readonly manifest: { + readonly definitions: ReadonlyMap + readonly schema: Schema.Decoder + } + }, +) { + const after = input.after ?? -1 + const rows = yield* db + .select() + .from(EventTable) + .where( + and( + eq(EventTable.aggregate_id, input.aggregateID), + gt(EventTable.seq, after), + inArray(EventTable.type, Array.from(input.manifest.definitions.keys())), + ), + ) + .orderBy(asc(EventTable.seq)) + .limit(input.limit + 1) + .all() + .pipe(Effect.orDie) + const page = rows.slice(0, input.limit) + const decode = Schema.decodeUnknownSync(input.manifest.schema) + const events = page.map((event) => { + const type = input.manifest.definitions.get(event.type)?.type ?? event.type + return decode({ + id: event.id, + type, + durable: { + aggregateID: event.aggregate_id, + seq: event.seq, + version: input.manifest.definitions.get(event.type)?.durable?.version, + }, + data: EventStorage.decode(type, event.data), // kilocode_change + }) + }) + return { + events, + hasMore: rows.length > input.limit, + } +}) + +export class SubscriberOverflowError extends Schema.TaggedErrorClass()( + "EventV2.SubscriberOverflow", + { capacity: Schema.Int }, +) {} + +export const define = Event.define +export const versionedType = Event.versionedType + export interface PublishOptions { readonly id?: ID readonly metadata?: Record + readonly location?: Location.Ref + /** Local operational projection committed atomically with a new durable event. Not replayed or serialized. */ + readonly commit?: (seq: number) => Effect.Effect } -export type Unsubscribe = Effect.Effect - export interface Interface { readonly publish: ( definition: D, data: Data, options?: PublishOptions, ) => Effect.Effect> - readonly publishEvent: (event: Payload) => Effect.Effect> readonly subscribe: (definition: D) => Stream.Stream> readonly all: () => Stream.Stream - readonly sync: (handler: Sync) => Effect.Effect + readonly durable: (input: { readonly aggregateID: string; readonly after?: number }) => Stream.Stream + /** @deprecated Use `all()` and consume the returned stream. */ + readonly listen: (listener: Subscriber) => Effect.Effect + readonly project: (definition: D, projector: Subscriber) => Effect.Effect + readonly replay: ( + event: SerializedEvent, + options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }, + ) => Effect.Effect + readonly replayAll: ( + events: SerializedEvent[], + options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }, + ) => Effect.Effect + readonly remove: (aggregateID: string) => Effect.Effect + readonly claim: (aggregateID: string, ownerID: string) => Effect.Effect } export class Service extends Context.Service()("@opencode/Event") {} -export const layer = Layer.effect( - Service, +export const allBounded = (events: Interface, capacity: number) => Effect.gen(function* () { - const all = yield* PubSub.unbounded() - const typed = new Map>() - const syncHandlers = new Array() - - const getOrCreate = (definition: Definition) => - Effect.gen(function* () { - const existing = typed.get(definition.type) - if (existing) return existing - const pubsub = yield* PubSub.unbounded() - typed.set(definition.type, pubsub) - return pubsub - }) - - yield* Effect.addFinalizer(() => - Effect.gen(function* () { - yield* PubSub.shutdown(all) - yield* Effect.forEach(typed.values(), PubSub.shutdown, { discard: true }) - }), + const queue = yield* Queue.dropping(capacity) + const unsubscribe = yield* events.listen((event) => + Queue.offer(queue, event).pipe( + Effect.flatMap((accepted) => + accepted ? Effect.void : Queue.fail(queue, new SubscriberOverflowError({ capacity })).pipe(Effect.asVoid), + ), + ), ) + yield* Effect.addFinalizer(() => unsubscribe.pipe(Effect.andThen(Queue.shutdown(queue)), Effect.asVoid)) + return Stream.fromQueue(queue) + }) - function publishEvent(event: Payload) { - return Effect.gen(function* () { - for (const sync of syncHandlers) { - yield* sync(event as Payload) - } - const pubsub = typed.get(event.type) - if (pubsub) yield* PubSub.publish(pubsub, event as Payload) - yield* PubSub.publish(all, event as Payload) - return event - }) - } +export interface LayerOptions { + readonly beforeAggregateRead?: (aggregateID: string) => Effect.Effect +} - function publish(definition: D, data: Data, options?: PublishOptions) { - return Effect.gen(function* () { - const location = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service)) - const event = { - id: options?.id ?? ID.create(), - ...(options?.metadata ? { metadata: options.metadata } : {}), - type: definition.type, - ...(definition.version === undefined ? {} : { version: definition.version }), - ...(location ? { location: { directory: location.directory, workspaceID: location.workspaceID } } : {}), - data, - } as Payload - return yield* publishEvent(event) - }) - } +export const layerWith = (options?: LayerOptions) => + Layer.effect( + Service, + Effect.gen(function* () { + const pubsub = { + all: yield* PubSub.unbounded(), + durable: new Map>>(), + typed: new Map>(), + } + const projectors = new Map() + // TODO: Bind durable projectors to exact type+version before supporting incompatible historical payloads. + const listeners = new Array() + const { db } = yield* Database.Service + + const getOrCreate = (definition: Definition) => + Effect.gen(function* () { + const existing = pubsub.typed.get(definition.type) + if (existing) return existing + const created = yield* PubSub.unbounded() + pubsub.typed.set(definition.type, created) + return created + }) - const subscribe = (definition: D): Stream.Stream> => - Stream.unwrap(getOrCreate(definition).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub)))).pipe( - Stream.map((event) => event as Payload), + yield* Effect.addFinalizer(() => + Effect.gen(function* () { + yield* PubSub.shutdown(pubsub.all) + yield* Effect.forEach( + pubsub.durable.values(), + (pubsubs) => Effect.forEach(pubsubs, PubSub.shutdown, { discard: true }), + { discard: true }, + ) + yield* Effect.forEach(pubsub.typed.values(), PubSub.shutdown, { discard: true }) + }), ) - const streamAll = (): Stream.Stream => Stream.fromPubSub(all) - const sync = (handler: Sync): Effect.Effect => - Effect.sync(() => { - syncHandlers.push(handler) - return Effect.sync(() => { - const index = syncHandlers.indexOf(handler) - if (index >= 0) syncHandlers.splice(index, 1) + function commitDurableEvent( + definition: Definition, + event: Payload, + input?: { + readonly seq: number + readonly aggregateID: string + readonly ownerID?: string + readonly strictOwner?: boolean + }, + commit?: (seq: number) => Effect.Effect, + ) { + return Effect.gen(function* () { + const durable = definition?.durable + if (durable) { + const aggregateID = (event.data as Record)[durable.aggregate] + if (typeof aggregateID !== "string") { + yield* Effect.die( + new InvalidDurableEventError({ + type: event.type, + message: `Expected string aggregate field ${durable.aggregate}`, + }), + ) + } else { + if (input && input.aggregateID !== aggregateID) { + yield* Effect.die( + new InvalidDurableEventError({ + type: event.type, + message: `Aggregate mismatch: expected ${input.aggregateID}, got ${aggregateID}`, + }), + ) + } + const list = projectors.get(event.type) ?? [] + return yield* Effect.uninterruptible( + Effect.gen(function* () { + const committed = yield* db + .transaction( + () => + Effect.gen(function* () { + const row = yield* db + .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id }) + .from(EventSequenceTable) + .where(eq(EventSequenceTable.aggregate_id, aggregateID)) + .get() + .pipe(Effect.orDie) + const latest = row?.seq ?? -1 + // kilocode_change - persist tool content in the released shape + const encoded = EventStorage.encode( + definition.type, + Schema.encodeUnknownSync(definition.data)(event.data), + ) as Record + if (input?.strictOwner && row?.ownerID && row.ownerID !== input.ownerID) { + yield* Effect.die( + new InvalidDurableEventError({ + type: event.type, + message: `Replay owner mismatch for aggregate ${aggregateID}: expected ${row.ownerID}, got ${input.ownerID ?? "none"}`, + }), + ) + } + if (input && input.seq <= latest) { + const stored = yield* db + .select() + .from(EventTable) + .where(and(eq(EventTable.aggregate_id, aggregateID), eq(EventTable.seq, input.seq))) + .get() + .pipe(Effect.orDie) + if ( + stored?.id === event.id && + stored.type === versionedType(definition.type, durable.version) && + isDeepStrictEqual(stored.data, encoded) + ) { + if (input.ownerID && row?.ownerID == null) { + yield* db + .update(EventSequenceTable) + .set({ owner_id: input.ownerID }) + .where(eq(EventSequenceTable.aggregate_id, aggregateID)) + .run() + .pipe(Effect.orDie) + } + return + } + yield* Effect.die( + new InvalidDurableEventError({ + type: event.type, + message: `Replay diverged at aggregate ${aggregateID} sequence ${input.seq}`, + }), + ) + } + if (input && row?.ownerID && row.ownerID !== input.ownerID) { + return + } + const seq = input?.seq ?? latest + 1 + if (input && seq !== latest + 1) { + yield* Effect.die( + new InvalidDurableEventError({ + type: event.type, + message: `Sequence mismatch for aggregate ${aggregateID}: expected ${latest + 1}, got ${seq}`, + }), + ) + } + const stored = yield* db + .select({ aggregateID: EventTable.aggregate_id, seq: EventTable.seq }) + .from(EventTable) + .where(eq(EventTable.id, event.id)) + .get() + .pipe(Effect.orDie) + if (stored) + yield* Effect.die( + new InvalidDurableEventError({ + type: event.type, + message: `Event ${event.id} already exists at aggregate ${stored.aggregateID} sequence ${stored.seq}`, + }), + ) + const committed = { + ...event, + durable: { aggregateID, seq, version: durable.version }, + } as Payload + for (const projector of list) { + yield* projector(committed) + } + if (commit) yield* commit(seq) + yield* db + .insert(EventSequenceTable) + .values([{ aggregate_id: aggregateID, seq, owner_id: input?.ownerID }]) + .onConflictDoUpdate({ + target: EventSequenceTable.aggregate_id, + set: { + seq, + ...(input?.ownerID && row?.ownerID == null ? { owner_id: input.ownerID } : {}), + }, + }) + .run() + .pipe(Effect.orDie) + yield* db + .insert(EventTable) + .values([ + { + id: event.id, + aggregate_id: aggregateID, + seq, + type: versionedType(definition.type, durable.version), + data: encoded, + }, + ]) + .run() + .pipe(Effect.orDie) + return { aggregateID, seq } + }), + { behavior: "immediate" }, + ) + .pipe(Effect.orDie) + if (committed) { + yield* Effect.forEach( + pubsub.durable.get(committed.aggregateID) ?? [], + (wake) => PubSub.publish(wake, undefined), + { discard: true }, + ) + } + return committed + }), + ) + } + } }) - }) + } + + function publishEvent(definition: D, event: Payload, commit?: PublishOptions["commit"]) { + return Effect.gen(function* () { + if (!definition?.durable && commit) + return yield* Effect.die( + new InvalidDurableEventError({ + type: event.type, + message: "Local commit hooks require a durable event", + }), + ) + if (definition?.durable) { + const committed = yield* commitDurableEvent(definition, event as Payload, undefined, commit) + if (committed) { + event = { + ...event, + durable: { + aggregateID: committed.aggregateID, + seq: committed.seq, + version: definition.durable.version, + }, + } + yield* notify(event as Payload, true) + return event + } + } + yield* notify(event as Payload, false) + return event + }) + } + + const observe = (event: Payload, observer: (event: Payload) => Effect.Effect) => + Effect.suspend(() => observer(event)).pipe( + Effect.catchCauseIf( + (cause) => !Cause.hasInterrupts(cause), + (cause) => Effect.logError("Event listener failed", { eventID: event.id, eventType: event.type, cause }), + ), + ) + + function notify(event: Payload, isolateListeners: boolean) { + return Effect.gen(function* () { + yield* Effect.forEach( + listeners, + (listener) => (isolateListeners ? observe(event, listener) : listener(event)), + { discard: true }, + ) + const typed = pubsub.typed.get(event.type) + if (typed) yield* PubSub.publish(typed, event) + yield* PubSub.publish(pubsub.all, event) + }) + } + + function publish(definition: D, data: Data, options?: PublishOptions) { + return Effect.gen(function* () { + const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service)) + const location = + options?.location ?? + (serviceLocation + ? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID } + : undefined) + return yield* publishEvent( + definition, + { + id: options?.id ?? ID.create(), + ...(options?.metadata ? { metadata: options.metadata } : {}), + type: definition.type, + ...(location ? { location } : {}), + data, + } as Payload, + options?.commit, + ) + }) + } + + function replay( + event: SerializedEvent, + options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }, + ) { + return Effect.gen(function* () { + const definition = Durable.get(event.type) + if (!definition?.durable) { + yield* Effect.die( + new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }), + ) + } else { + const payload = { + id: event.id, + type: definition.type, + data: Schema.decodeUnknownSync(definition.data)(EventStorage.decode(definition.type, event.data)), // kilocode_change + } as Payload + const committed = yield* commitDurableEvent(definition, payload, { + seq: event.seq, + aggregateID: event.aggregateID, + ownerID: options?.ownerID, + strictOwner: options?.strictOwner, + }) + if (committed && options?.publish) { + yield* notify( + { + ...payload, + durable: { + aggregateID: committed.aggregateID, + seq: committed.seq, + version: definition.durable.version, + }, + }, + true, + ) + } + } + }) + } + + function replayAll( + events: SerializedEvent[], + options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }, + ) { + return Effect.gen(function* () { + const source = events[0]?.aggregateID + if (!source) return undefined + if (events.some((event) => event.aggregateID !== source)) { + yield* Effect.die( + new InvalidDurableEventError({ + type: events[0]?.type ?? "unknown", + message: "Replay events must belong to the same aggregate", + }), + ) + } + const start = events[0]?.seq ?? 0 + for (const [index, event] of events.entries()) { + const seq = start + index + if (event.seq !== seq) { + yield* Effect.die( + new InvalidDurableEventError({ + type: event.type, + message: `Replay sequence mismatch at index ${index}: expected ${seq}, got ${event.seq}`, + }), + ) + } + } + for (const event of events) { + yield* replay(event, options) + } + return source + }) + } - return Service.of({ publish, publishEvent, subscribe, all: streamAll, sync }) - }), -) + function remove(aggregateID: string) { + return db + .transaction(() => + Effect.gen(function* () { + yield* db.delete(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, aggregateID)).run() + yield* db.delete(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).run() + }), + ) + .pipe(Effect.orDie) + } + + function claim(aggregateID: string, ownerID: string) { + return db + .update(EventSequenceTable) + .set({ owner_id: ownerID }) + .where(eq(EventSequenceTable.aggregate_id, aggregateID)) + .run() + .pipe(Effect.orDie) + } + + const subscribe = (definition: D): Stream.Stream> => + Stream.unwrap(getOrCreate(definition).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub)))).pipe( + Stream.map((event) => event as Payload), + ) + + const streamAll = (): Stream.Stream => Stream.fromPubSub(pubsub.all) + + const readAfter = (aggregateID: string, after: number) => + (options?.beforeAggregateRead?.(aggregateID) ?? Effect.void).pipe( + Effect.andThen( + db + .select() + .from(EventTable) + .where(and(eq(EventTable.aggregate_id, aggregateID), gt(EventTable.seq, after))) + .orderBy(asc(EventTable.seq)) + .all(), + ), + Effect.orDie, + Effect.map((rows) => + rows.map((event) => + decodeSerializedEvent({ + id: event.id, + aggregateID: event.aggregate_id, + seq: event.seq, + type: event.type, + data: event.data, + }), + ), + ), + ) + + const subscribeDurable = (aggregateID: string) => + Effect.gen(function* () { + const wake = yield* PubSub.sliding(1) + const subscription = yield* PubSub.subscribe(wake) + yield* Effect.acquireRelease( + Effect.sync(() => { + const wakes = pubsub.durable.get(aggregateID) ?? new Set() + wakes.add(wake) + pubsub.durable.set(aggregateID, wakes) + }), + () => + Effect.sync(() => { + const wakes = pubsub.durable.get(aggregateID) + wakes?.delete(wake) + if (wakes?.size === 0) pubsub.durable.delete(aggregateID) + }).pipe(Effect.andThen(PubSub.shutdown(wake))), + ) + return subscription + }) + + const durable = (input: { readonly aggregateID: string; readonly after?: number }): Stream.Stream => + Stream.unwrap( + Effect.gen(function* () { + const wakes = yield* subscribeDurable(input.aggregateID) + let sequence = input.after ?? -1 + const read = Effect.suspend(() => readAfter(input.aggregateID, sequence)).pipe( + Effect.tap((events) => + Effect.sync(() => { + sequence = events.at(-1)?.durable?.seq ?? sequence + }), + ), + ) + const historical = yield* read + const live = Stream.fromSubscription(wakes).pipe( + Stream.mapEffect(() => read), + Stream.flattenIterable, + ) + return Stream.concat(Stream.fromIterable(historical), live) + }), + ) + + const listen = (listener: Subscriber): Effect.Effect => + Effect.sync(() => { + listeners.push(listener) + return Effect.sync(() => { + const index = listeners.indexOf(listener) + if (index >= 0) listeners.splice(index, 1) + }) + }) + + const project = (definition: D, projector: Subscriber): Effect.Effect => + Effect.sync(() => { + const list = projectors.get(definition.type) ?? [] + list.push((event) => projector(event as Payload)) + projectors.set(definition.type, list) + }) + + return Service.of({ + publish, + subscribe, + all: streamAll, + durable, + listen, + project, + replay, + replayAll, + remove, + claim, + }) + }), + ) -export const defaultLayer = layer +const layer = layerWith() +export const node = makeGlobalNode({ service: Service, layer: layer, deps: [Database.node] }) diff --git a/packages/core/src/event/sql.ts b/packages/core/src/event/sql.ts new file mode 100644 index 00000000000..38fe34f1e32 --- /dev/null +++ b/packages/core/src/event/sql.ts @@ -0,0 +1,25 @@ +import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core" +import type { EventV2 } from "../event" + +export const EventSequenceTable = sqliteTable("event_sequence", { + aggregate_id: text().notNull().primaryKey(), + seq: integer().notNull(), + owner_id: text(), +}) + +export const EventTable = sqliteTable( + "event", + { + id: text().$type().primaryKey(), + aggregate_id: text() + .notNull() + .references(() => EventSequenceTable.aggregate_id, { onDelete: "cascade" }), + seq: integer().notNull(), + type: text().notNull(), + data: text({ mode: "json" }).$type>().notNull(), + }, + (table) => [ + uniqueIndex("event_aggregate_seq_idx").on(table.aggregate_id, table.seq), + index("event_aggregate_type_seq_idx").on(table.aggregate_id, table.type, table.seq), + ], +) diff --git a/packages/core/src/file-mutation.ts b/packages/core/src/file-mutation.ts new file mode 100644 index 00000000000..80a3a449f60 --- /dev/null +++ b/packages/core/src/file-mutation.ts @@ -0,0 +1,207 @@ +export * as FileMutation from "./file-mutation" + +import { makeLocationNode } from "./effect/app-node" +import { Context, Effect, Layer, Schema } from "effect" +import { dirname } from "path" +import { KeyedMutex } from "./effect/keyed-mutex" +import { FSUtil } from "./fs-util" + +export interface Target { + readonly canonical: string + readonly resource: string +} + +export interface WriteInput { + readonly target: Target + readonly content: string | Uint8Array +} + +export interface TextWriteInput { + readonly target: Target + readonly content: string +} + +export interface ConditionalWriteInput extends WriteInput { + readonly expected: Uint8Array +} + +export interface RemoveInput { + readonly target: Target +} + +export class StaleContentError extends Schema.TaggedErrorClass()("FileMutation.StaleContentError", { + path: Schema.String, +}) {} + +export class TargetExistsError extends Schema.TaggedErrorClass()("FileMutation.TargetExistsError", { + path: Schema.String, +}) {} + +export interface WriteResult { + readonly operation: "write" + readonly target: string + readonly resource: string + readonly existed: boolean +} + +export interface RemoveResult { + readonly operation: "remove" + readonly target: string + readonly resource: string + readonly existed: boolean +} + +export interface Interface { + /** Create without replacing an existing target. */ + readonly create: (input: WriteInput) => Effect.Effect + readonly write: (input: WriteInput) => Effect.Effect + /** Write text while retaining an existing UTF-8 BOM and emitting at most one BOM. */ + readonly writeTextPreservingBom: (input: TextWriteInput) => Effect.Effect + /** Commit only if an existing target still has the expected bytes. */ + readonly writeIfUnchanged: ( + input: ConditionalWriteInput, + ) => Effect.Effect + readonly remove: (input: RemoveInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/FileMutation") {} + +/** + * Serialize file changes by canonical target. Conditional writes compare and + * write under the same process-local lock so cooperating OpenCode mutations do + * not overwrite changes made from the same stale content. + */ +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const locks = KeyedMutex.makeUnsafe() + const withTargetLock = + (target: Target) => + (effect: Effect.Effect) => + locks.withLock(target.canonical)(Effect.uninterruptible(effect)) + + const writeResult = (target: Target, existed: boolean): WriteResult => ({ + operation: "write", + target: target.canonical, + resource: target.resource, + existed, + }) + + const removeResult = (target: Target, existed: boolean): RemoveResult => ({ + operation: "remove", + target: target.canonical, + resource: target.resource, + existed, + }) + + const write = Effect.fn("FileMutation.write")((input: WriteInput) => + withTargetLock(input.target)( + Effect.gen(function* () { + const existed = yield* fs.exists(input.target.canonical) + yield* fs.writeWithDirs(input.target.canonical, input.content) + return writeResult(input.target, existed) + }), + ), + ) + + const writeTextPreservingBom = Effect.fn("FileMutation.writeTextPreservingBom")((input: TextWriteInput) => + withTargetLock(input.target)( + Effect.gen(function* () { + const next = splitBom(input.content) + const current = yield* fs + .readFile(input.target.canonical) + .pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined))) + yield* fs.writeWithDirs( + input.target.canonical, + joinBom(next.text, Boolean(current && hasUtf8Bom(current)) || next.bom), + ) + return writeResult(input.target, current !== undefined) + }), + ), + ) + + const create = Effect.fn("FileMutation.create")((input: WriteInput) => + withTargetLock(input.target)( + Effect.gen(function* () { + const write = + typeof input.content === "string" + ? fs.writeFileString(input.target.canonical, input.content, { flag: "wx" }) + : fs.writeFile(input.target.canonical, input.content, { flag: "wx" }) + yield* write.pipe( + Effect.catchReason("PlatformError", "NotFound", () => + fs.ensureDir(dirname(input.target.canonical)).pipe(Effect.andThen(write)), + ), + Effect.catchReason("PlatformError", "AlreadyExists", () => + Effect.fail(new TargetExistsError({ path: input.target.canonical })), + ), + ) + return writeResult(input.target, false) + }), + ), + ) + + const writeIfUnchanged = Effect.fn("FileMutation.writeIfUnchanged")((input: ConditionalWriteInput) => + withTargetLock(input.target)( + Effect.gen(function* () { + const current = yield* fs.readFile(input.target.canonical) + if (!sameBytes(current, input.expected)) { + return yield* new StaleContentError({ path: input.target.canonical }) + } + yield* typeof input.content === "string" + ? fs.writeFileString(input.target.canonical, input.content) + : fs.writeFile(input.target.canonical, input.content) + return writeResult(input.target, true) + }), + ), + ) + + const remove = Effect.fn("FileMutation.remove")((input: RemoveInput) => + withTargetLock(input.target)( + Effect.gen(function* () { + const existed = yield* fs.remove(input.target.canonical).pipe( + Effect.as(true), + Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(false)), + ) + return removeResult(input.target, existed) + }), + ), + ) + + return Service.of({ create, write, writeTextPreservingBom, writeIfUnchanged, remove }) + }), +) + +function splitBom(text: string) { + const stripped = text.replace(/^\uFEFF+/, "") + return { bom: stripped.length !== text.length, text: stripped } +} + +function joinBom(text: string, bom: boolean) { + const stripped = splitBom(text).text + return bom ? `\uFEFF${stripped}` : stripped +} + +function hasUtf8Bom(content: Uint8Array) { + return content[0] === 0xef && content[1] === 0xbb && content[2] === 0xbf +} + +function sameBytes(left: Uint8Array, right: Uint8Array) { + if (left.length !== right.length) return false + return left.every((byte, index) => byte === right[index]) +} + +export const locationLayer = layer + +export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node] }) + +/** + * Deferred until the corresponding V2 integrations exist. + */ +// TODO: Add formatter integration after V2 formatter runtime exists. +// TODO: Publish watcher/file-edit events after V2 watcher integration exists. +// TODO: Add snapshots / undo after V2 snapshot design exists. +// TODO: Notify LSP and collect diagnostics after V2 LSP runtime exists. +// TODO: Design multi-file transactions / rollback if apply_patch needs atomic edits. +// Until then, edits are sequential and report partial application. +// TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement. diff --git a/packages/core/src/file.ts b/packages/core/src/file.ts new file mode 100644 index 00000000000..87745c01eeb --- /dev/null +++ b/packages/core/src/file.ts @@ -0,0 +1,6 @@ +export * as File from "./file" + +import { Revert } from "@opencode-ai/schema/revert" + +export const Diff = Revert.FileDiff +export type Diff = typeof Diff.Type diff --git a/packages/core/src/filesystem.ts b/packages/core/src/filesystem.ts index 758e60e78ee..9e3b7fc89d4 100644 --- a/packages/core/src/filesystem.ts +++ b/packages/core/src/filesystem.ts @@ -1,251 +1,147 @@ -import { NodeFileSystem } from "@effect/platform-node" -import { decorateFileSystem, ensureDirectory } from "@kilocode/sandbox" // kilocode_change -import { dirname, isAbsolute, join, relative, resolve as pathResolve, sep } from "path" // kilocode_change - harden containment checks -import { realpathSync } from "fs" -import * as NFS from "fs/promises" -import { lookup } from "mime-types" -import { Context, Effect, FileSystem, Layer, Schema } from "effect" -import type { PlatformError } from "effect/PlatformError" -import { Glob } from "./util/glob" -import { serviceUse } from "./effect/service-use" - -export namespace AppFileSystem { - export class FileSystemError extends Schema.TaggedErrorClass()("FileSystemError", { - method: Schema.String, - cause: Schema.optional(Schema.Defect), - }) {} - - export type Error = PlatformError | FileSystemError - - export interface DirEntry { - readonly name: string - readonly type: "file" | "directory" | "symlink" | "other" - } - - export interface Interface extends FileSystem.FileSystem { - readonly isDir: (path: string) => Effect.Effect - readonly isFile: (path: string) => Effect.Effect - readonly existsSafe: (path: string) => Effect.Effect - readonly readFileStringSafe: (path: string) => Effect.Effect - readonly readJson: (path: string) => Effect.Effect - readonly writeJson: (path: string, data: unknown, mode?: number) => Effect.Effect - readonly ensureDir: (path: string) => Effect.Effect - readonly writeWithDirs: (path: string, content: string | Uint8Array, mode?: number) => Effect.Effect - readonly readDirectoryEntries: (path: string) => Effect.Effect - readonly findUp: (target: string, start: string, stop?: string) => Effect.Effect - readonly up: (options: { targets: string[]; start: string; stop?: string }) => Effect.Effect - readonly globUp: (pattern: string, start: string, stop?: string) => Effect.Effect - readonly glob: (pattern: string, options?: Glob.Options) => Effect.Effect - readonly globMatch: (pattern: string, filepath: string) => boolean - } - - export class Service extends Context.Service()("@opencode/FileSystem") {} - - export const use = serviceUse(Service) - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const fs = decorateFileSystem(yield* FileSystem.FileSystem) // kilocode_change - - const existsSafe = Effect.fn("FileSystem.existsSafe")(function* (path: string) { - return yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false)) - }) - - const readFileStringSafe = Effect.fn("FileSystem.readFileStringSafe")(function* (path: string) { - return yield* fs - .readFileString(path) - .pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined))) - }) - - const isDir = Effect.fn("FileSystem.isDir")(function* (path: string) { - const info = yield* fs.stat(path).pipe(Effect.catch(() => Effect.void)) - return info?.type === "Directory" - }) - - const isFile = Effect.fn("FileSystem.isFile")(function* (path: string) { - const info = yield* fs.stat(path).pipe(Effect.catch(() => Effect.void)) - return info?.type === "File" - }) +export * as FileSystem from "./filesystem" + +import { makeLocationNode } from "./effect/app-node" +import path from "path" +import { Context, Effect, Layer, Option, Schema } from "effect" // kilocode_change +import { FSUtil } from "./fs-util" +import { Location } from "./location" +import { PositiveInt, RelativePath } from "./schema" +import { FileSystemSearch } from "./filesystem/search" +import { Entry, FileSystem, FindInput, Match } from "@opencode-ai/schema/filesystem" +import * as SearchTarget from "./kilocode/search-target" // kilocode_change +export { Entry, Match, Submatch } from "@opencode-ai/schema/filesystem" + +export const ReadInput = Schema.Struct({ + path: RelativePath, +}) +export type ReadInput = typeof ReadInput.Type + +export const Content = Schema.Struct({ + uri: Schema.String, + name: Schema.String.pipe(Schema.optional), + content: Schema.String, + encoding: Schema.Literals(["utf8", "base64"]), + mime: Schema.String, +}).annotate({ identifier: "FileSystem.Content" }) +export type Content = typeof Content.Type + +export const ListInput = Schema.Struct({ + path: RelativePath.pipe(Schema.optional), +}) +export type ListInput = typeof ListInput.Type + +export { FindInput } + +export const DEFAULT_SEARCH_LIMIT = 100 // kilocode_change - preserve bounded Kilo tool searches +export const MAX_SEARCH_LIMIT = 100 // kilocode_change +export const SearchLimit = PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_SEARCH_LIMIT)) // kilocode_change + +export class GlobInput extends Schema.Class("FileSystem.GlobInput")({ + pattern: Schema.String, + path: RelativePath.pipe(Schema.optional), + limit: PositiveInt.pipe(Schema.optional), +}) {} + +export class GrepInput extends Schema.Class("FileSystem.GrepInput")({ + pattern: Schema.String, + path: RelativePath.pipe(Schema.optional), + include: Schema.String.pipe(Schema.optional), + limit: PositiveInt.pipe(Schema.optional), +}) {} + +export const Event = FileSystem.Event + +export interface Interface { + readonly read: (input: ReadInput) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }> + readonly list: (input?: ListInput) => Effect.Effect + readonly find: (input: FindInput) => Effect.Effect + readonly glob: (input: GlobInput) => Effect.Effect + readonly grep: (input: GrepInput) => Effect.Effect +} - const readDirectoryEntries = Effect.fn("FileSystem.readDirectoryEntries")(function* (dirPath: string) { - return yield* Effect.tryPromise({ - try: async () => { - const entries = await NFS.readdir(dirPath, { withFileTypes: true }) - return entries.map( - (e): DirEntry => ({ - name: e.name, - type: e.isDirectory() ? "directory" : e.isSymbolicLink() ? "symlink" : e.isFile() ? "file" : "other", - }), +export class Service extends Context.Service()("@opencode/v2/FileSystem") {} + +const baseLayer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const location = yield* Location.Service + const search = yield* FileSystemSearch.Service + const root = yield* fs.realPath(location.directory).pipe(Effect.orDie) + const resolve = Effect.fnUntraced(function* (input?: RelativePath) { + const absolute = path.resolve(location.directory, input ?? ".") + if (!FSUtil.contains(location.directory, absolute)) + return yield* Effect.die(new Error("Path escapes the location")) + const real = yield* fs.realPath(absolute).pipe(Effect.orDie) + if (!FSUtil.contains(root, real)) return yield* Effect.die(new Error("Path escapes the location")) + const target = yield* SearchTarget.inspect(fs, real).pipe(Effect.orDie) // kilocode_change + return { absolute, real, directory: location.directory, root, target } // kilocode_change + }) + return Service.of({ + find: search.find, + glob: search.glob, + grep: search.grep, + read: Effect.fn("FileSystem.read")(function* (input) { + const target = yield* resolve(input.path) + if (target.target.type !== "file") return yield* Effect.die(new Error("Path is not a file")) // kilocode_change + // kilocode_change start - read from the validated descriptor, not a second pathname lookup. + return yield* Effect.scoped( + Effect.gen(function* () { + const file = yield* fs.open(target.real, { flag: "r" }).pipe(Effect.orDie) + const info = yield* file.stat.pipe(Effect.orDie) + if ( + info.type !== "File" || + info.dev !== target.target.dev || + Option.getOrUndefined(info.ino) !== target.target.ino ) - }, - catch: (cause) => new FileSystemError({ method: "readDirectoryEntries", cause }), - }) - }) - - const readJson = Effect.fn("FileSystem.readJson")(function* (path: string) { - const text = yield* fs.readFileString(path) - return JSON.parse(text) - }) - - const writeJson = Effect.fn("FileSystem.writeJson")(function* (path: string, data: unknown, mode?: number) { - const content = JSON.stringify(data, null, 2) - yield* fs.writeFileString(path, content) - if (mode) yield* fs.chmod(path, mode) - }) - - const ensureDir = Effect.fn("FileSystem.ensureDir")(function* (path: string) { - yield* ensureDirectory(fs, path) // kilocode_change - mutate through the sandbox-confined filesystem - }) - - const writeWithDirs = Effect.fn("FileSystem.writeWithDirs")(function* ( - path: string, - content: string | Uint8Array, - mode?: number, - ) { - const write = typeof content === "string" ? fs.writeFileString(path, content) : fs.writeFile(path, content) - - yield* write.pipe( - Effect.catchIf( - (e) => e.reason._tag === "NotFound", - () => - Effect.gen(function* () { - yield* ensureDirectory(fs, dirname(path)) // kilocode_change - sandbox-confined mkdir - yield* write - }), + return yield* Effect.die(new Error("Path changed during read")) + const chunks: Uint8Array[] = [] + while (true) { + const chunk = yield* file.readAlloc(64 * 1024).pipe(Effect.orDie) + if (Option.isNone(chunk)) break + chunks.push(chunk.value) + } + return { + content: new Uint8Array(Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)))), + mime: FSUtil.mimeType(target.real), + } + }), + ) + // kilocode_change end + }), + list: Effect.fn("FileSystem.list")(function* (input = {}) { + const target = yield* resolve(input.path) + if (target.target.type !== "directory") return yield* Effect.die(new Error("Path is not a directory")) // kilocode_change + // kilocode_change start - reject directory replacement during enumeration + yield* SearchTarget.validate(fs, target.target).pipe(Effect.orDie) + const entries = yield* fs.readDirectoryEntries(target.real).pipe( + Effect.orDie, + Effect.map((items) => + items + .flatMap((item) => { + if (item.type !== "file" && item.type !== "directory") return [] + const absolute = path.join(target.absolute, item.name) + const relative = path.relative(target.directory, absolute) + return [ + Entry.make({ + path: RelativePath.make(relative + (item.type === "directory" ? path.sep : "")), + type: item.type, + }), + ] + }) + .sort((a, b) => (a.type === b.type ? a.path.localeCompare(b.path) : a.type === "directory" ? -1 : 1)), ), ) - if (mode) yield* fs.chmod(path, mode) - }) - - const glob = Effect.fn("FileSystem.glob")(function* (pattern: string, options?: Glob.Options) { - return yield* Effect.tryPromise({ - try: () => Glob.scan(pattern, options), - catch: (cause) => new FileSystemError({ method: "glob", cause }), - }) - }) - - const findUp = Effect.fn("FileSystem.findUp")(function* (target: string, start: string, stop?: string) { - const result: string[] = [] - let current = start - while (true) { - const search = join(current, target) - if (yield* fs.exists(search)) result.push(search) - if (stop === current) break - const parent = dirname(current) - if (parent === current) break - current = parent - } - return result - }) - - const up = Effect.fn("FileSystem.up")(function* (options: { targets: string[]; start: string; stop?: string }) { - const result: string[] = [] - let current = options.start - while (true) { - for (const target of options.targets) { - const search = join(current, target) - if (yield* fs.exists(search)) result.push(search) - } - if (options.stop === current) break - const parent = dirname(current) - if (parent === current) break - current = parent - } - return result - }) - - const globUp = Effect.fn("FileSystem.globUp")(function* (pattern: string, start: string, stop?: string) { - const result: string[] = [] - let current = start - while (true) { - const matches = yield* glob(pattern, { cwd: current, absolute: true, include: "file", dot: true }).pipe( - Effect.catch(() => Effect.succeed([] as string[])), - ) - result.push(...matches) - if (stop === current) break - const parent = dirname(current) - if (parent === current) break - current = parent - } - return result - }) - - return Service.of({ - ...fs, - existsSafe, - readFileStringSafe, - isDir, - isFile, - readDirectoryEntries, - readJson, - writeJson, - ensureDir, - writeWithDirs, - findUp, - up, - globUp, - glob, - globMatch: Glob.match, - }) - }), - ) - - export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer)) - - // Pure helpers that don't need Effect (path manipulation, sync operations) - export function mimeType(p: string): string { - return lookup(p) || "application/octet-stream" - } - - export function normalizePath(p: string): string { - if (process.platform !== "win32") return p - const resolved = pathResolve(windowsPath(p)) - try { - return realpathSync.native(resolved) - } catch { - return resolved - } - } - - export function normalizePathPattern(p: string): string { - if (process.platform !== "win32") return p - if (p === "*") return p - const match = p.match(/^(.*)[\\/]\*$/) - if (!match) return normalizePath(p) - const dir = /^[A-Za-z]:$/.test(match[1]) ? match[1] + "\\" : match[1] - return join(normalizePath(dir), "*") - } - - export function resolve(p: string): string { - const resolved = pathResolve(windowsPath(p)) - try { - return normalizePath(realpathSync(resolved)) - } catch (e: any) { - if (e?.code === "ENOENT") return normalizePath(resolved) - throw e - } - } - - export function windowsPath(p: string): string { - if (process.platform !== "win32") return p - return p - .replace(/^\/([a-zA-Z]):(?:[\\/]|$)/, (_, drive) => `${drive.toUpperCase()}:/`) - .replace(/^\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`) - .replace(/^\/cygdrive\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`) - .replace(/^\/mnt\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`) - } - - export function overlaps(a: string, b: string) { - const relA = relative(a, b) - const relB = relative(b, a) - return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..") - } - - export function contains(parent: string, child: string) { - // kilocode_change start - reject cross-drive and escaped relative paths - const rel = relative(parent, child) - return rel === "" || (!isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`)) - // kilocode_change end - } -} + yield* SearchTarget.validate(fs, target.target).pipe(Effect.orDie) + return entries + // kilocode_change end + }), + }) + }), +) + +export const node = makeLocationNode({ + service: Service, + layer: baseLayer, + deps: [FSUtil.node, Location.node, FileSystemSearch.node], +}) diff --git a/packages/core/src/filesystem/fff.bun.ts b/packages/core/src/filesystem/fff.bun.ts new file mode 100644 index 00000000000..9843419ed11 --- /dev/null +++ b/packages/core/src/filesystem/fff.bun.ts @@ -0,0 +1,140 @@ +import { + FileFinder, + type DirItem, + type DirSearchResult, + type FileItem, + type GrepCursor, + type GrepMatch, + type GrepResult, + type InitOptions, + type MixedItem, + type MixedSearchResult, + type SearchResult, +} from "@ff-labs/fff-bun" + +declare global { + const FFF_LIBC: "gnu" | "musl" +} + +export type Result = { ok: true; value: T } | { ok: false; error: string } + +export type Init = InitOptions + +export interface Search { + items: FileItem[] + scores: SearchResult["scores"] + totalMatched: number + totalFiles: number +} + +export interface DirSearch { + items: DirItem[] + scores: DirSearchResult["scores"] + totalMatched: number + totalDirs: number +} + +export interface MixedSearch { + items: MixedItem[] + scores: MixedSearchResult["scores"] + totalMatched: number + totalFiles: number + totalDirs: number +} + +export type File = FileItem +export type Directory = DirItem +export type Mixed = MixedItem +export type Cursor = GrepCursor | null +export type Hit = GrepMatch + +export interface Grep { + items: GrepResult["items"] + totalMatched: number + totalFilesSearched: number + totalFiles: number + filteredFileCount: number + nextCursor: Cursor + regexFallbackError?: string +} + +export interface Picker { + destroy(): void + isScanning(): boolean + waitForScan(timeoutMs?: number): Promise> + refreshGitStatus(): Result + fileSearch( + query: string, + opts?: { + currentFile?: string + pageIndex?: number + pageSize?: number + }, + ): Result + glob( + pattern: string, + opts?: { + currentFile?: string + pageIndex?: number + pageSize?: number + }, + ): Result + directorySearch( + query: string, + opts?: { + currentFile?: string + pageIndex?: number + pageSize?: number + }, + ): Result + mixedSearch( + query: string, + opts?: { + currentFile?: string + pageIndex?: number + pageSize?: number + }, + ): Result + grep( + query: string, + opts?: { + mode?: "plain" | "regex" | "fuzzy" + maxMatchesPerFile?: number + timeBudgetMs?: number + beforeContext?: number + afterContext?: number + cursor?: Cursor + pageSize?: number + }, + ): Result + trackQuery(query: string, file: string): Result + getHistoricalQuery(offset: number): Result +} + +export function available() { + return FileFinder.isAvailable() +} + +export function create(opts: Init): Result { + const made = FileFinder.create(opts) + if (!made.ok) return made + const pick = made.value + return { + ok: true, + value: { + destroy: () => pick.destroy(), + isScanning: () => pick.isScanning(), + waitForScan: (timeoutMs) => pick.waitForScan(timeoutMs), + refreshGitStatus: () => pick.refreshGitStatus(), + fileSearch: (query, next) => pick.fileSearch(query, next), + glob: (pattern, next) => pick.glob(pattern, next), + directorySearch: (query, next) => pick.directorySearch(query, next), + mixedSearch: (query, next) => pick.mixedSearch(query, next), + grep: (query, next) => pick.grep(query, next), + trackQuery: (query, file) => pick.trackQuery(query, file), + getHistoricalQuery: (offset) => pick.getHistoricalQuery(offset), + }, + } +} + +export * as Fff from "./fff.bun" diff --git a/packages/core/src/filesystem/fff.node.ts b/packages/core/src/filesystem/fff.node.ts new file mode 100644 index 00000000000..464c2853d7e --- /dev/null +++ b/packages/core/src/filesystem/fff.node.ts @@ -0,0 +1,138 @@ +export type Result = { ok: true; value: T } | { ok: false; error: string } + +export interface Init { + basePath: string + frecencyDbPath?: string + historyDbPath?: string + useUnsafeNoLock?: boolean + disableMmapCache?: boolean + disableContentIndexing?: boolean + disableWatch?: boolean + aiMode?: boolean + logFilePath?: string + logLevel?: "trace" | "debug" | "info" | "warn" | "error" + enableFsRootScanning?: boolean + enableHomeDirScanning?: boolean +} + +export interface File { + relativePath: string + fileName: string + modified: number +} + +export interface Directory { + relativePath: string + dirName: string + maxAccessFrecency: number +} + +export type Mixed = { type: "file"; item: File } | { type: "directory"; item: Directory } + +export interface Search { + items: File[] + scores: Array<{ total: number }> + totalMatched: number + totalFiles: number +} + +export interface DirSearch { + items: Directory[] + scores: Array<{ total: number }> + totalMatched: number + totalDirs: number +} + +export interface MixedSearch { + items: Mixed[] + scores: Array<{ total: number }> + totalMatched: number + totalFiles: number + totalDirs: number +} + +export type Cursor = null + +export interface Hit { + relativePath: string + fileName: string + lineNumber: number + byteOffset: number + lineContent: string + matchRanges: [number, number][] + contextBefore?: string[] + contextAfter?: string[] +} + +export interface Grep { + items: Hit[] + totalMatched: number + totalFilesSearched: number + totalFiles: number + filteredFileCount: number + nextCursor: Cursor + regexFallbackError?: string +} + +export interface Picker { + destroy(): void + isScanning(): boolean + waitForScan(timeoutMs?: number): Promise> + refreshGitStatus(): Result + fileSearch( + query: string, + opts?: { + currentFile?: string + pageIndex?: number + pageSize?: number + }, + ): Result + glob( + pattern: string, + opts?: { + currentFile?: string + pageIndex?: number + pageSize?: number + }, + ): Result + directorySearch( + query: string, + opts?: { + currentFile?: string + pageIndex?: number + pageSize?: number + }, + ): Result + mixedSearch( + query: string, + opts?: { + currentFile?: string + pageIndex?: number + pageSize?: number + }, + ): Result + grep( + query: string, + opts?: { + mode?: "plain" | "regex" | "fuzzy" + maxMatchesPerFile?: number + timeBudgetMs?: number + beforeContext?: number + afterContext?: number + cursor?: Cursor + pageSize?: number + }, + ): Result + trackQuery(query: string, file: string): Result + getHistoricalQuery(offset: number): Result +} + +export function available() { + return false +} + +export function create(_opts: Init): Result { + return { ok: false, error: "fff unavailable on node runtime" } +} + +export * as Fff from "./fff.node" diff --git a/packages/core/src/filesystem/ignore.ts b/packages/core/src/filesystem/ignore.ts new file mode 100644 index 00000000000..2f5f52bf25a --- /dev/null +++ b/packages/core/src/filesystem/ignore.ts @@ -0,0 +1,67 @@ +import { Glob } from "../util/glob" + +const FOLDERS = new Set([ + "node_modules", + "bower_components", + ".pnpm-store", + "vendor", + ".npm", + "dist", + "build", + "out", + ".next", + "target", + "bin", + "obj", + ".git", + ".svn", + ".hg", + ".vscode", + ".idea", + ".turbo", + ".output", + "desktop", + ".sst", + ".cache", + ".webkit-cache", + "__pycache__", + ".pytest_cache", + "mypy_cache", + ".history", + ".gradle", +]) + +const FILES = [ + "**/*.swp", + "**/*.swo", + "**/*.pyc", + "**/.DS_Store", + "**/Thumbs.db", + "**/logs/**", + "**/tmp/**", + "**/temp/**", + "**/*.log", + "**/coverage/**", + "**/.nyc_output/**", +] + +export const PATTERNS = [...FILES, ...FOLDERS] + +export function match(filepath: string, opts?: { extra?: string[]; whitelist?: string[] }) { + for (const pattern of opts?.whitelist || []) { + if (Glob.match(pattern, filepath)) return false + } + + const parts = filepath.split(/[/\\]/) + for (const part of parts) { + if (FOLDERS.has(part)) return true + } + + for (const pattern of [...FILES, ...(opts?.extra || [])]) { + if (Glob.match(pattern, filepath)) return true + } + + return false +} + +export * as Ignore from "./ignore" diff --git a/packages/core/src/filesystem/protected.ts b/packages/core/src/filesystem/protected.ts new file mode 100644 index 00000000000..a7646dfb48f --- /dev/null +++ b/packages/core/src/filesystem/protected.ts @@ -0,0 +1,53 @@ +import os from "os" +import path from "path" + +const home = os.homedir() + +const DARWIN_HOME = [ + "Music", + "Pictures", + "Movies", + "Downloads", + "Desktop", + "Documents", + "Public", + "Applications", + "Library", +] + +const DARWIN_LIBRARY = [ + "Application Support/AddressBook", + "Calendars", + "Mail", + "Messages", + "Safari", + "Cookies", + "Application Support/com.apple.TCC", + "PersonalizationPortrait", + "Metadata/CoreSpotlight", + "Suggestions", +] + +const DARWIN_ROOT = ["/.DocumentRevisions-V100", "/.Spotlight-V100", "/.Trashes", "/.fseventsd"] +const WIN32_HOME = ["AppData", "Downloads", "Desktop", "Documents", "Pictures", "Music", "Videos", "OneDrive"] + +/** Directory basenames to skip when scanning the home directory. */ +export function names(): ReadonlySet { + if (process.platform === "darwin") return new Set(DARWIN_HOME) + if (process.platform === "win32") return new Set(WIN32_HOME) + return new Set() +} + +/** Absolute paths that should never be watched, stated, or scanned. */ +export function paths(): string[] { + if (process.platform === "darwin") + return [ + ...DARWIN_HOME.map((name) => path.join(home, name)), + ...DARWIN_LIBRARY.map((name) => path.join(home, "Library", name)), + ...DARWIN_ROOT, + ] + if (process.platform === "win32") return WIN32_HOME.map((name) => path.join(home, name)) + return [] +} + +export * as Protected from "./protected" diff --git a/packages/core/src/filesystem/search.ts b/packages/core/src/filesystem/search.ts new file mode 100644 index 00000000000..66ad56a4b68 --- /dev/null +++ b/packages/core/src/filesystem/search.ts @@ -0,0 +1,336 @@ +export * as FileSystemSearch from "./search" + +import { makeLocationNode } from "../effect/app-node" +import path from "path" +import { Context, Duration, Effect, Layer, Scope } from "effect" +import { Fff } from "#fff" +import fuzzysort from "fuzzysort" +import { FileSystem } from "../filesystem" +import { FSUtil } from "../fs-util" +import { Location } from "../location" +import { Ripgrep } from "../ripgrep" +import { RelativePath } from "../schema" +import { Flag } from "../flag/flag" +// kilocode_change start +import * as SearchTarget from "../kilocode/search-target" +import { scanning } from "../kilocode/fff" +// kilocode_change end + +export interface Interface { + readonly find: (input: FileSystem.FindInput) => Effect.Effect + readonly glob: (input: FileSystem.GlobInput) => Effect.Effect + readonly grep: (input: FileSystem.GrepInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/FileSystem/Search") {} + +export const ripgrepLayer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const location = yield* Location.Service + const ripgrep = yield* Ripgrep.Service + const scope = yield* Scope.Scope + // kilocode_change start - confine every search to the canonical active Location. + const inspect = Effect.fnUntraced(function* (input?: string) { + const root = yield* SearchTarget.inspect(fs, location.directory).pipe(Effect.orDie) + const requested = path.resolve(location.directory, input ?? ".") + if (!FSUtil.contains(location.directory, requested)) + return yield* Effect.die(new Error("Path escapes the location")) + const target = yield* SearchTarget.inspect(fs, requested).pipe(Effect.orDie) + if (root.type !== "directory" || !FSUtil.contains(root.path, target.path)) + return yield* Effect.die(new Error("Path escapes the location")) + return target + }) + // kilocode_change end + const state = { + files: [] as string[], + directories: [] as string[], + } + const directories = new Set() + yield* ripgrep + .find({ + cwd: location.directory, + pattern: "*", + limit: location.vcs ? Number.MAX_SAFE_INTEGER : 100_000, + onEntry: (entry) => + Effect.sync(() => { + state.files.push(entry.path) + const parts = entry.path.split("/") + parts.slice(0, -1).forEach((_, index) => directories.add(parts.slice(0, index + 1).join("/") + path.sep)) + state.directories = Array.from(directories) + }), + }) + .pipe(Effect.orDie, Effect.asVoid, Effect.forkIn(scope)) + return Service.of({ + glob: (input) => + Effect.gen(function* () { + // kilocode_change start + const target = yield* inspect(input.path) + const cwd = target.type === "file" ? path.dirname(target.path) : target.path + // kilocode_change end + return yield* ripgrep + .glob({ + cwd, + pattern: input.pattern, + limit: input.limit ?? Number.MAX_SAFE_INTEGER, + validate: SearchTarget.validate(fs, target), // kilocode_change + }) + .pipe( + Effect.map((result) => + result.items.map( + // kilocode_change - validate wraps results in SearchResult + (entry) => + FileSystem.Entry.make({ + ...entry, + path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, entry.path))), + }), + ), + ), + Effect.orDie, + ) + }), + grep: (input) => + Effect.gen(function* () { + // kilocode_change start + const target = yield* inspect(input.path) + const cwd = target.type === "file" ? path.dirname(target.path) : target.path + // kilocode_change end + return yield* ripgrep + .grep({ + cwd, + pattern: input.pattern, + file: target.type === "file" ? path.basename(target.path) : undefined, // kilocode_change + include: input.include, + limit: input.limit ?? Number.MAX_SAFE_INTEGER, + validate: SearchTarget.validate(fs, target), // kilocode_change + }) + .pipe( + Effect.map((result) => + result.items.map( + // kilocode_change - validate wraps results in SearchResult + (match) => + FileSystem.Match.make({ + ...match, + entry: FileSystem.Entry.make({ + ...match.entry, + path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, match.entry.path))), + }), + }), + ), + ), + Effect.orDie, + ) + }), + find: (input) => + Effect.gen(function* () { + const items = + input.type === "file" + ? state.files + : input.type === "directory" + ? state.directories + : [...state.files, ...state.directories] + return fuzzysort.go(input.query, items, { limit: input.limit ?? 50 }).map((item) => { + const relative = item.target + const type = relative.endsWith(path.sep) ? ("directory" as const) : ("file" as const) + return FileSystem.Entry.make({ + path: RelativePath.make(relative), + type, + }) + }) + }), + }) + }), +) + +export const fffLayer = Layer.effect( + Service, + Effect.gen(function* () { + const location = yield* Location.Service + // kilocode_change start + const fs = yield* FSUtil.Service + const inspect = Effect.fnUntraced(function* (input?: string) { + const root = yield* SearchTarget.inspect(fs, location.directory).pipe(Effect.orDie) + const requested = path.resolve(location.directory, input ?? ".") + if (!FSUtil.contains(location.directory, requested)) + return yield* Effect.die(new Error("Path escapes the location")) + const target = yield* SearchTarget.inspect(fs, requested).pipe(Effect.orDie) + if (root.type !== "directory" || !FSUtil.contains(root.path, target.path)) + return yield* Effect.die(new Error("Path escapes the location")) + return { root, target } + }) + const safe = Effect.fnUntraced(function* (root: SearchTarget.Target, relative: string) { + const absolute = path.resolve(location.directory, relative) + if (!FSUtil.contains(location.directory, absolute)) return false + const real = yield* fs.realPath(absolute).pipe(Effect.catch(() => Effect.succeed(undefined))) + return real !== undefined && FSUtil.contains(root.path, real) + }) + // kilocode_change end + // kilocode_change start - defer FFF until search because other location consumers do not need its native index. + const scope = yield* Scope.Scope + const release = (entry: { finder: { destroy(): void }; closed: boolean }) => + Effect.sync(() => { + if (entry.closed) return + entry.closed = true + entry.finder.destroy() + }).pipe(Effect.ignore) + const make = Effect.uninterruptible( + Effect.gen(function* () { + const result = yield* Effect.try({ + try: () => + Fff.create({ + basePath: location.directory, + aiMode: true, + disableMmapCache: true, + disableContentIndexing: true, + ...scanning(location.directory), + }), + catch: (cause) => cause, + }).pipe(Effect.orDie) + if (!result.ok) return yield* Effect.die(result.error) + const entry = { finder: result.value, closed: false } + yield* Scope.addFinalizer(scope, release(entry)) + return entry + }), + ) + const [load, invalidate] = yield* Effect.cachedInvalidateWithTTL( + make.pipe( + Effect.flatMap((entry) => + Effect.promise(() => entry.finder.waitForScan()) + .pipe( + Effect.orDie, + Effect.onError(() => release(entry)), + ) + .pipe( + Effect.flatMap((scan) => { + if (!scan.ok || !scan.value) return Effect.die(new Error("FFF initial scan did not complete")) + return Effect.succeed(entry) + }), + ), + ), + ), + Duration.infinity, + ) + const get = load.pipe(Effect.onError(() => invalidate)) + yield* Scope.addFinalizer(scope, invalidate) + // kilocode_change end + return Service.of({ + glob: (input) => + // kilocode_change start + Effect.gen(function* () { + const { root, target } = yield* inspect(input.path) + const result = yield* get + // kilocode_change end + const prefix = input.path?.replaceAll("\\", "/").replace(/\/$/, "") + // kilocode_change start + const found = yield* Effect.sync(() => + result.finder.glob(prefix ? `${prefix}/${input.pattern}` : input.pattern, { + pageIndex: 0, + pageSize: input.limit, + }), + ) + // kilocode_change end + if (!found.ok) throw found.error + // kilocode_change start + yield* SearchTarget.validate(fs, target).pipe(Effect.orDie) + const items = yield* Effect.filter(found.value.items, (item) => safe(root, item.relativePath)) + return items.map((item) => + FileSystem.Entry.make({ + path: RelativePath.make(item.relativePath.replaceAll("\\", "/")), + type: "file", + }), + ) + // kilocode_change end + }), + grep: (input) => + // kilocode_change start + Effect.gen(function* () { + const { root, target } = yield* inspect(input.path) + const result = yield* get + // kilocode_change end + const prefix = input.path?.replaceAll("\\", "/").replace(/\/$/, "") + // kilocode_change start + const found = yield* Effect.sync( + () => + result.finder.grep( + [prefix ? `${prefix}/**` : undefined, input.include, input.pattern] + .filter((value) => value !== undefined) + .join(" "), + { mode: "regex", pageSize: input.limit, timeBudgetMs: 1_500 }, + ), + // kilocode_change end + ) + if (!found.ok) throw found.error + // kilocode_change start + yield* SearchTarget.validate(fs, target).pipe(Effect.orDie) + const items = yield* Effect.filter(found.value.items, (item) => safe(root, item.relativePath)) + return items.map((match) => { + const bytes = Buffer.from(match.lineContent) + return FileSystem.Match.make({ + entry: FileSystem.Entry.make({ + path: RelativePath.make(match.relativePath.replaceAll("\\", "/")), + type: "file", + }), + line: match.lineNumber, + offset: match.byteOffset, + text: match.lineContent.length > 2_000 ? match.lineContent.slice(0, 2_000) + "..." : match.lineContent, + submatches: match.matchRanges.map(([start, end]) => ({ + text: bytes.subarray(start, end).toString("utf8"), + start, + end, + })), + }) + }) + // kilocode_change end + }), + find: (input) => + Effect.gen(function* () { + // kilocode_change - load the native index only for an actual search. + const result = yield* get // kilocode_change + const options = { pageIndex: 0, pageSize: input.limit ?? 50 } + const items = (() => { + if (input.type === "file") { + const found = result.finder.fileSearch(input.query.trim(), options) + if (!found.ok) throw found.error + return found.value.items.map((item, index) => ({ + path: item.relativePath, + type: "file" as const, + score: found.value.scores[index]?.total ?? 0, + })) + } + if (input.type === "directory") { + const found = result.finder.directorySearch(input.query.trim(), options) + if (!found.ok) throw found.error + return found.value.items.map((item, index) => ({ + path: item.relativePath, + type: "directory" as const, + score: found.value.scores[index]?.total ?? 0, + })) + } + const found = result.finder.mixedSearch(input.query.trim(), options) + if (!found.ok) throw found.error + return found.value.items.map((item, index) => ({ + path: item.item.relativePath, + type: item.type, + score: found.value.scores[index]?.total ?? 0, + })) + })() + return items + .sort((a, b) => b.score - a.score || a.path.length - b.path.length) + .map((item) => { + const relative = item.path.replaceAll("\\", "/").replace(/\/$/, "") + return FileSystem.Entry.make({ + path: RelativePath.make(relative + (item.type === "directory" ? path.sep : "")), + type: item.type, + }) + }) + }), + }) + }), +) + +const layer = Layer.unwrap(Effect.sync(() => (Flag.KILO_DISABLE_FFF || !Fff.available() ? ripgrepLayer : fffLayer))) + +export const locationLayer = layer + +export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Location.node, Ripgrep.node] }) diff --git a/packages/core/src/filesystem/watcher.ts b/packages/core/src/filesystem/watcher.ts new file mode 100644 index 00000000000..9e4ec029188 --- /dev/null +++ b/packages/core/src/filesystem/watcher.ts @@ -0,0 +1,140 @@ +export * as Watcher from "./watcher" + +// @ts-ignore +import { createWrapper } from "@parcel/watcher/wrapper" +import type ParcelWatcher from "@parcel/watcher" +import { makeLocationNode } from "../effect/app-node" +import { Cause, Context, Effect, Layer } from "effect" +import { FileSystemWatcher } from "@opencode-ai/schema/filesystem-watcher" +import path from "path" +import { Config } from "../config" +import { EventV2 } from "../event" +import { Flag } from "../flag/flag" +import { FSUtil } from "../fs-util" +import { Git } from "../git" +import { Location } from "../location" +import { lazy } from "../util/lazy" +import { Ignore } from "./ignore" +import { Protected } from "./protected" + +declare const KILO_LIBC: string | undefined + +const SUBSCRIBE_TIMEOUT_MS = 10_000 + +export const Event = FileSystemWatcher.Event + +const watcher = lazy((): typeof import("@parcel/watcher") | undefined => { + try { + const libc = typeof KILO_LIBC === "undefined" ? undefined : KILO_LIBC + const binding = require( + `@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${libc || "glibc"}` : ""}`, + ) + return createWrapper(binding) as typeof import("@parcel/watcher") + } catch { + return + } +}) + +function getBackend() { + if (process.platform === "win32") return "windows" + if (process.platform === "darwin") return "fs-events" + if (process.platform === "linux") return "inotify" +} + +function protecteds(dir: string) { + return Protected.paths().filter((item) => { + const relative = path.relative(dir, item) + return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative) + }) +} + +export const hasNativeBinding = () => !!watcher() + +export interface Interface {} + +export class Service extends Context.Service()("@opencode/v2/FileWatcher") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + if (yield* Flag.KILO_EXPERIMENTAL_DISABLE_FILEWATCHER) return Service.of({}) + + const backend = getBackend() + const location = yield* Location.Service + if (!backend) { + yield* Effect.logError("watcher backend not supported", { + directory: location.directory, + platform: process.platform, + }) + return Service.of({}) + } + + const w = watcher() + if (!w) return Service.of({}) + + yield* Effect.logInfo("watcher backend", { directory: location.directory, platform: process.platform, backend }) + const events = yield* EventV2.Service + const fs = yield* FSUtil.Service + const git = yield* Git.Service + const context = yield* Effect.context() + const runFork = Effect.runForkWith(context) + const subscriptions: ParcelWatcher.AsyncSubscription[] = [] + yield* Effect.addFinalizer(() => + Effect.promise(() => Promise.allSettled(subscriptions.map((subscription) => subscription.unsubscribe()))), + ) + + const callback: ParcelWatcher.SubscribeCallback = (_error, updates) => { + for (const update of updates) { + if (update.type === "create") runFork(events.publish(Event.Updated, { file: update.path, event: "add" })) + if (update.type === "update") runFork(events.publish(Event.Updated, { file: update.path, event: "change" })) + if (update.type === "delete") runFork(events.publish(Event.Updated, { file: update.path, event: "unlink" })) + } + } + + const subscribe = (directory: string, ignore: string[]) => { + const pending = w.subscribe(directory, callback, { ignore, backend }) + return Effect.promise(() => pending).pipe( + Effect.tap((subscription) => Effect.sync(() => subscriptions.push(subscription))), + Effect.timeout(SUBSCRIBE_TIMEOUT_MS), + Effect.catchCause((cause) => { + pending.then((subscription) => subscription.unsubscribe()).catch(() => {}) + return Effect.logError("failed to subscribe", { directory, cause: Cause.pretty(cause) }) + }), + ) + } + + const config = (yield* (yield* Config.Service).entries()) + .filter((entry): entry is Config.Document => entry.type === "document") + .flatMap((item) => item.info.watcher?.ignore ?? []) + if (location.vcs && (yield* Flag.KILO_EXPERIMENTAL_FILEWATCHER)) { + yield* Effect.forkScoped( + subscribe(location.directory, [...Ignore.PATTERNS, ...config, ...protecteds(location.directory)]), + ) + } + + if (location.vcs?.type === "git") { + const resolved = (yield* git.repo.discover(location.directory))?.gitDirectory + const vcs = resolved ? yield* fs.realPath(resolved).pipe(Effect.catch(() => Effect.succeed(resolved))) : undefined + if (vcs && !config.includes(".git") && !config.includes(vcs) && (!resolved || !config.includes(resolved))) { + const ignore = (yield* fs.readDirectoryEntries(vcs).pipe(Effect.catch(() => Effect.succeed([])))).flatMap( + (entry) => (entry.name === "HEAD" ? [] : [entry.name]), + ) + yield* Effect.forkScoped(subscribe(vcs, ignore)) + } + } + + return Service.of({}) + }).pipe( + Effect.catchCause((cause) => { + return Effect.logError("failed to init watcher service", { cause: Cause.pretty(cause) }).pipe( + Effect.as(Service.of({})), + ) + }), + ), +) + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [FSUtil.node, Location.node, Config.node, Git.node, EventV2.node], +}) diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index 58ec9713629..3b8736ebee1 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -1,11 +1,12 @@ import { Config } from "effect" -import { InstallationChannel } from "../installation/version" +import { InstallationChannel } from "../installation/version" // kilocode_change -function truthy(key: string) { +export function truthy(key: string) { const value = process.env[key]?.toLowerCase() return value === "true" || value === "1" } +// kilocode_change start function falsy(key: string) { const value = process.env[key]?.toLowerCase() return value === "false" || value === "0" @@ -26,17 +27,19 @@ function number(key: string) { const KILO_EXPERIMENTAL = truthy("KILO_EXPERIMENTAL") const KILO_DISABLE_CLAUDE_CODE = truthy("KILO_DISABLE_CLAUDE_CODE") const KILO_DISABLE_CLAUDE_CODE_SKILLS = KILO_DISABLE_CLAUDE_CODE || truthy("KILO_DISABLE_CLAUDE_CODE_SKILLS") +// kilocode_change end const copy = process.env["KILO_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"] +const fff = process.env["KILO_DISABLE_FFF"] function enabledByExperimental(key: string) { - return process.env[key] === undefined ? KILO_EXPERIMENTAL : truthy(key) + return process.env[key] === undefined ? truthy("KILO_EXPERIMENTAL") : truthy(key) } export const Flag = { OTEL_EXPORTER_OTLP_ENDPOINT: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"], OTEL_EXPORTER_OTLP_HEADERS: process.env["OTEL_EXPORTER_OTLP_HEADERS"], - KILO_AUTO_SHARE: truthy("KILO_AUTO_SHARE"), + KILO_AUTO_SHARE: truthy("KILO_AUTO_SHARE"), // kilocode_change KILO_AUTO_HEAP_SNAPSHOT: truthy("KILO_AUTO_HEAP_SNAPSHOT"), KILO_GIT_BASH_PATH: process.env["KILO_GIT_BASH_PATH"], KILO_CONFIG: process.env["KILO_CONFIG"], @@ -46,56 +49,91 @@ export const Flag = { KILO_DISABLE_PRUNE: truthy("KILO_DISABLE_PRUNE"), KILO_DISABLE_TERMINAL_TITLE: truthy("KILO_DISABLE_TERMINAL_TITLE"), KILO_SHOW_TTFD: truthy("KILO_SHOW_TTFD"), + // kilocode_change start KILO_DISABLE_DEFAULT_PLUGINS: truthy("KILO_DISABLE_DEFAULT_PLUGINS"), KILO_DISABLE_LSP_DOWNLOAD: truthy("KILO_DISABLE_LSP_DOWNLOAD"), KILO_ENABLE_EXPERIMENTAL_MODELS: truthy("KILO_ENABLE_EXPERIMENTAL_MODELS"), + // kilocode_change end KILO_DISABLE_AUTOCOMPACT: truthy("KILO_DISABLE_AUTOCOMPACT"), KILO_DISABLE_MODELS_FETCH: truthy("KILO_DISABLE_MODELS_FETCH"), KILO_DISABLE_MOUSE: truthy("KILO_DISABLE_MOUSE"), + // kilocode_change start KILO_DISABLE_CLAUDE_CODE, KILO_DISABLE_CLAUDE_CODE_PROMPT: KILO_DISABLE_CLAUDE_CODE || truthy("KILO_DISABLE_CLAUDE_CODE_PROMPT"), KILO_DISABLE_CLAUDE_CODE_SKILLS, KILO_DISABLE_EXTERNAL_SKILLS: truthy("KILO_DISABLE_EXTERNAL_SKILLS"), - KILO_EXPERIMENTAL_CUSTOMIZE_SKILL: unstableDefault("KILO_EXPERIMENTAL_CUSTOMIZE_SKILL"), // kilocode_change + KILO_EXPERIMENTAL_CUSTOMIZE_SKILL: unstableDefault("KILO_EXPERIMENTAL_CUSTOMIZE_SKILL"), + // kilocode_change end KILO_FAKE_VCS: process.env["KILO_FAKE_VCS"], KILO_SERVER_PASSWORD: process.env["KILO_SERVER_PASSWORD"], KILO_SERVER_USERNAME: process.env["KILO_SERVER_USERNAME"], - KILO_ENABLE_QUESTION_TOOL: truthy("KILO_ENABLE_QUESTION_TOOL"), + KILO_ENABLE_QUESTION_TOOL: truthy("KILO_ENABLE_QUESTION_TOOL"), // kilocode_change + + KILO_EXPERIMENTAL, // kilocode_change + + KILO_EXPERIMENTAL_FILEWATCHER: Config.boolean("KILO_EXPERIMENTAL_FILEWATCHER").pipe(Config.withDefault(false)), // kilocode_change - KILO_EXPERIMENTAL, - KILO_EXPERIMENTAL_FILEWATCHER: Config.boolean("KILO_EXPERIMENTAL_FILEWATCHER").pipe(Config.withDefault(false)), KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: Config.boolean("KILO_EXPERIMENTAL_DISABLE_FILEWATCHER").pipe( Config.withDefault(false), ), - KILO_EXPERIMENTAL_ICON_DISCOVERY: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_ICON_DISCOVERY"), + + KILO_EXPERIMENTAL_ICON_DISCOVERY: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_ICON_DISCOVERY"), // kilocode_change + KILO_EXPERIMENTAL_DISABLE_COPY_ON_SELECT: copy === undefined ? process.platform === "win32" : truthy("KILO_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"), - KILO_ENABLE_EXA: truthy("KILO_ENABLE_EXA") || KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_EXA"), - KILO_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: number("KILO_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS"), - KILO_EXPERIMENTAL_OUTPUT_TOKEN_MAX: number("KILO_EXPERIMENTAL_OUTPUT_TOKEN_MAX"), - KILO_EXPERIMENTAL_OXFMT: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_OXFMT"), - KILO_EXPERIMENTAL_LSP_TY: truthy("KILO_EXPERIMENTAL_LSP_TY"), - KILO_EXPERIMENTAL_LSP_TOOL: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_LSP_TOOL"), - KILO_EXPERIMENTAL_PLAN_MODE: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_PLAN_MODE"), - KILO_EXPERIMENTAL_SCOUT: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_SCOUT"), - KILO_EXPERIMENTAL_MARKDOWN: !falsy("KILO_EXPERIMENTAL_MARKDOWN"), - KILO_ENABLE_PARALLEL: truthy("KILO_ENABLE_PARALLEL") || truthy("KILO_EXPERIMENTAL_PARALLEL"), + + KILO_ENABLE_EXA: truthy("KILO_ENABLE_EXA") || KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_EXA"), // kilocode_change + + KILO_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: number("KILO_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS"), // kilocode_change + + KILO_EXPERIMENTAL_OUTPUT_TOKEN_MAX: number("KILO_EXPERIMENTAL_OUTPUT_TOKEN_MAX"), // kilocode_change + + KILO_EXPERIMENTAL_OXFMT: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_OXFMT"), // kilocode_change + + KILO_EXPERIMENTAL_LSP_TY: truthy("KILO_EXPERIMENTAL_LSP_TY"), // kilocode_change + + KILO_EXPERIMENTAL_LSP_TOOL: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_LSP_TOOL"), // kilocode_change + + KILO_EXPERIMENTAL_PLAN_MODE: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_PLAN_MODE"), // kilocode_change + + KILO_EXPERIMENTAL_SCOUT: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_SCOUT"), // kilocode_change + + KILO_EXPERIMENTAL_MARKDOWN: !falsy("KILO_EXPERIMENTAL_MARKDOWN"), // kilocode_change + + KILO_ENABLE_PARALLEL: truthy("KILO_ENABLE_PARALLEL") || truthy("KILO_EXPERIMENTAL_PARALLEL"), // kilocode_change + KILO_MODELS_URL: process.env["KILO_MODELS_URL"], + KILO_MODELS_PATH: process.env["KILO_MODELS_PATH"], - KILO_DISABLE_EMBEDDED_WEB_UI: truthy("KILO_DISABLE_EMBEDDED_WEB_UI"), + + KILO_DISABLE_EMBEDDED_WEB_UI: truthy("KILO_DISABLE_EMBEDDED_WEB_UI"), // kilocode_change + KILO_DB: process.env["KILO_DB"], - KILO_DISABLE_CHANNEL_DB: truthy("KILO_DISABLE_CHANNEL_DB"), - KILO_SKIP_MIGRATIONS: truthy("KILO_SKIP_MIGRATIONS"), - KILO_STRICT_CONFIG_DEPS: truthy("KILO_STRICT_CONFIG_DEPS"), + + KILO_DISABLE_CHANNEL_DB: truthy("KILO_DISABLE_CHANNEL_DB"), // kilocode_change + + KILO_SKIP_MIGRATIONS: truthy("KILO_SKIP_MIGRATIONS"), // kilocode_change + + KILO_STRICT_CONFIG_DEPS: truthy("KILO_STRICT_CONFIG_DEPS"), // kilocode_change KILO_WORKSPACE_ID: process.env["KILO_WORKSPACE_ID"], + KILO_EXPERIMENTAL_WORKSPACES: enabledByExperimental("KILO_EXPERIMENTAL_WORKSPACES"), - KILO_EXPERIMENTAL_EVENT_SYSTEM: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_EVENT_SYSTEM"), - KILO_EXPERIMENTAL_SESSION_SWITCHING: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_SESSION_SWITCHING"), - KILO_EXPERIMENTAL_SESSION_SWITCHER: enabledByExperimental("KILO_EXPERIMENTAL_SESSION_SWITCHER"), + + KILO_EXPERIMENTAL_EVENT_SYSTEM: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_EVENT_SYSTEM"), // kilocode_change + + KILO_EXPERIMENTAL_SESSION_SWITCHING: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_SESSION_SWITCHING"), // kilocode_change + + KILO_EXPERIMENTAL_SESSION_SWITCHER: enabledByExperimental("KILO_EXPERIMENTAL_SESSION_SWITCHER"), // kilocode_change + + KILO_DISABLE_FFF: fff === undefined ? process.platform === "win32" : truthy("KILO_DISABLE_FFF"), // kilocode_change + get KILO_DISABLE_PROJECT_CONFIG() { return truthy("KILO_DISABLE_PROJECT_CONFIG") }, + get KILO_EXPERIMENTAL_REFERENCES() { + return enabledByExperimental("KILO_EXPERIMENTAL_REFERENCES") + }, get KILO_TUI_CONFIG() { return process.env["KILO_TUI_CONFIG"] }, @@ -114,7 +152,9 @@ export const Flag = { get KILO_CLIENT() { return process.env["KILO_CLIENT"] ?? "cli" }, + // kilocode_change start get KILO_SESSION_RETRY_LIMIT() { return number("KILO_SESSION_RETRY_LIMIT") }, + // kilocode_change end } diff --git a/packages/core/src/fs-util.ts b/packages/core/src/fs-util.ts new file mode 100644 index 00000000000..64e352e0bd7 --- /dev/null +++ b/packages/core/src/fs-util.ts @@ -0,0 +1,269 @@ +import { NodeFileSystem } from "@effect/platform-node" +import { decorateFileSystem, ensureDirectory } from "@kilocode/sandbox" // kilocode_change +import { dirname, isAbsolute, join, relative, resolve as pathResolve, sep } from "path" +import { realpathSync } from "fs" +import * as NFS from "fs/promises" +import { lookup } from "mime-types" +import { Context, Effect, FileSystem, Layer, Schema } from "effect" +import type { PlatformError } from "effect/PlatformError" +import { Glob } from "./util/glob" +import { serviceUse } from "./effect/service-use" +import { makeGlobalNode } from "./effect/app-node" +import { filesystem } from "./effect/app-node-platform" + +export namespace FSUtil { + export class FileSystemError extends Schema.TaggedErrorClass()("FileSystemError", { + method: Schema.String, + cause: Schema.optional(Schema.Defect()), + }) { + override get message() { + const detail = this.cause instanceof Error ? this.cause.message : this.cause && String(this.cause) + return `Filesystem operation failed: ${this.method}${detail ? `: ${detail}` : ""}` + } + } + + export type Error = PlatformError | FileSystemError + + export interface DirEntry { + readonly name: string + readonly type: "file" | "directory" | "symlink" | "other" + } + + export interface Interface extends FileSystem.FileSystem { + readonly isDir: (path: string) => Effect.Effect + readonly isFile: (path: string) => Effect.Effect + readonly existsSafe: (path: string) => Effect.Effect + readonly readFileStringSafe: (path: string) => Effect.Effect + readonly readJson: (path: string) => Effect.Effect + readonly writeJson: (path: string, data: unknown, mode?: number) => Effect.Effect + readonly ensureDir: (path: string) => Effect.Effect + readonly writeWithDirs: (path: string, content: string | Uint8Array, mode?: number) => Effect.Effect + readonly readDirectoryEntries: (path: string) => Effect.Effect + readonly resolve: (path: string) => Effect.Effect + readonly findUp: (target: string, start: string, stop?: string) => Effect.Effect + readonly up: (options: { targets: string[]; start: string; stop?: string }) => Effect.Effect + readonly globUp: (pattern: string, start: string, stop?: string) => Effect.Effect + readonly glob: (pattern: string, options?: Glob.Options) => Effect.Effect + readonly globMatch: (pattern: string, filepath: string) => boolean + } + + export class Service extends Context.Service()("@opencode/FileSystem") {} + + export const use = serviceUse(Service) + + const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = decorateFileSystem(yield* FileSystem.FileSystem) // kilocode_change + + const existsSafe = Effect.fn("FileSystem.existsSafe")(function* (path: string) { + return yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false)) + }) + + const readFileStringSafe = Effect.fn("FileSystem.readFileStringSafe")(function* (path: string) { + return yield* fs.readFileString(path).pipe( + Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)), + Effect.catchReason("PlatformError", "PermissionDenied", () => Effect.succeed(undefined)), + ) + }) + + const isDir = Effect.fn("FileSystem.isDir")(function* (path: string) { + const info = yield* fs.stat(path).pipe(Effect.catch(() => Effect.void)) + return info?.type === "Directory" + }) + + const isFile = Effect.fn("FileSystem.isFile")(function* (path: string) { + const info = yield* fs.stat(path).pipe(Effect.catch(() => Effect.void)) + return info?.type === "File" + }) + + const readDirectoryEntries = Effect.fn("FileSystem.readDirectoryEntries")(function* (dirPath: string) { + return yield* Effect.tryPromise({ + try: async () => { + const entries = await NFS.readdir(dirPath, { withFileTypes: true }) + return entries.map( + (e): DirEntry => ({ + name: e.name, + type: e.isDirectory() ? "directory" : e.isSymbolicLink() ? "symlink" : e.isFile() ? "file" : "other", + }), + ) + }, + catch: (cause) => new FileSystemError({ method: "readDirectoryEntries", cause }), + }) + }) + + const resolve = Effect.fn("FileSystem.resolve")(function* (path: string) { + const resolved = pathResolve(windowsPath(path)) + return yield* fs.realPath(resolved).pipe( + Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(resolved)), + Effect.orDie, + ) + }) + + const readJson = Effect.fn("FileSystem.readJson")(function* (path: string) { + const text = yield* fs.readFileString(path) + return yield* Effect.try({ + try: () => JSON.parse(text), + catch: (cause) => new FileSystemError({ method: "readJson", cause }), + }) + }) + + const writeJson = Effect.fn("FileSystem.writeJson")(function* (path: string, data: unknown, mode?: number) { + const content = JSON.stringify(data, null, 2) + yield* fs.writeFileString(path, content) + if (mode) yield* fs.chmod(path, mode) + }) + + const ensureDir = Effect.fn("FileSystem.ensureDir")(function* (path: string) { + yield* ensureDirectory(fs, path) // kilocode_change - mutate through the sandbox-confined filesystem + }) + + const writeWithDirs = Effect.fn("FileSystem.writeWithDirs")(function* ( + path: string, + content: string | Uint8Array, + mode?: number, + ) { + const write = typeof content === "string" ? fs.writeFileString(path, content) : fs.writeFile(path, content) + + yield* write.pipe( + Effect.catchIf( + (e) => e.reason._tag === "NotFound", + () => + Effect.gen(function* () { + yield* ensureDirectory(fs, dirname(path)) // kilocode_change - sandbox-confined mkdir + yield* write + }), + ), + ) + if (mode) yield* fs.chmod(path, mode) + }) + + const glob = Effect.fn("FileSystem.glob")(function* (pattern: string, options?: Glob.Options) { + return yield* Effect.tryPromise({ + try: () => Glob.scan(pattern, options), + catch: (cause) => new FileSystemError({ method: "glob", cause }), + }) + }) + + const findUp = Effect.fn("FileSystem.findUp")(function* (target: string, start: string, stop?: string) { + const result: string[] = [] + let current = start + while (true) { + const search = join(current, target) + if (yield* fs.exists(search)) result.push(search) + if (stop === current) break + const parent = dirname(current) + if (parent === current) break + current = parent + } + return result + }) + + const up = Effect.fn("FileSystem.up")(function* (options: { targets: string[]; start: string; stop?: string }) { + const result: string[] = [] + let current = options.start + while (true) { + for (const target of options.targets) { + const search = join(current, target) + if (yield* fs.exists(search)) result.push(search) + } + if (options.stop === current) break + const parent = dirname(current) + if (parent === current) break + current = parent + } + return result + }) + + const globUp = Effect.fn("FileSystem.globUp")(function* (pattern: string, start: string, stop?: string) { + const result: string[] = [] + let current = start + while (true) { + const matches = yield* glob(pattern, { cwd: current, absolute: true, include: "file", dot: true }).pipe( + Effect.catch(() => Effect.succeed([] as string[])), + ) + result.push(...matches) + if (stop === current) break + const parent = dirname(current) + if (parent === current) break + current = parent + } + return result + }) + + return Service.of({ + ...fs, + existsSafe, + readFileStringSafe, + isDir, + isFile, + readDirectoryEntries, + resolve, + readJson, + writeJson, + ensureDir, + writeWithDirs, + findUp, + up, + globUp, + glob, + globMatch: Glob.match, + }) + }), + ) + + export const node = makeGlobalNode({ service: Service, layer: layer, deps: [filesystem] }) + export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer)) // kilocode_change - legacy Kilo runtime compatibility + + // Pure helpers that don't need Effect (path manipulation, sync operations) + export function mimeType(p: string): string { + return lookup(p) || "application/octet-stream" + } + + export function normalizePath(p: string): string { + if (process.platform !== "win32") return p + const resolved = pathResolve(windowsPath(p)) + try { + return realpathSync.native(resolved) + } catch { + return resolved + } + } + + export function normalizePathPattern(p: string): string { + if (process.platform !== "win32") return p + if (p === "*") return p + const match = p.match(/^(.*)[\\/]\*$/) + if (!match) return normalizePath(p) + const dir = /^[A-Za-z]:$/.test(match[1]) ? match[1] + "\\" : match[1] + return join(normalizePath(dir), "*") + } + + export function resolve(p: string): string { + const resolved = pathResolve(windowsPath(p)) + try { + return normalizePath(realpathSync(resolved)) + } catch (e: any) { + if (e?.code === "ENOENT") return normalizePath(resolved) + throw e + } + } + + export function windowsPath(p: string): string { + if (process.platform !== "win32") return p + return p + .replace(/^\/([a-zA-Z]):(?:[\\/]|$)/, (_, drive) => `${drive.toUpperCase()}:/`) + .replace(/^\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`) + .replace(/^\/cygdrive\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`) + .replace(/^\/mnt\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`) + } + + export function overlaps(a: string, b: string) { + return contains(a, b) || contains(b, a) + } + + export function contains(parent: string, child: string) { + const result = relative(parent, child) + return result === "" || (!isAbsolute(result) && result !== ".." && !result.startsWith(`..${sep}`)) + } +} diff --git a/packages/core/src/git.ts b/packages/core/src/git.ts index a5745233686..19dcde8961a 100644 --- a/packages/core/src/git.ts +++ b/packages/core/src/git.ts @@ -1,46 +1,187 @@ export * as Git from "./git" import path from "path" -import { Context, Effect, Layer } from "effect" +import { randomUUID } from "crypto" +import { Context, Effect, Layer, Schema, Stream } from "effect" import { ChildProcess } from "effect/unstable/process" -import { AbsolutePath } from "./schema" -import { AppFileSystem } from "./filesystem" +import { AbsolutePath, RelativePath } from "./schema" +import { FSUtil } from "./fs-util" import { AppProcess } from "./process" +import { makeGlobalNode } from "./effect/app-node" +import { File } from "./file" +import { KeyedMutex } from "./effect/keyed-mutex" -export interface Repo { - /** - * The root directory of the working tree that contains the input path. - * - * For `/home/me/app/src/file.ts` in a normal clone, this is `/home/me/app`. - * For `/home/me/app-feature/src/file.ts` in a linked worktree, this is - * `/home/me/app-feature`. - */ - readonly directory: AbsolutePath - /** - * The shared Git storage directory used by this repo and any linked worktrees. - * - * For a normal clone at `/home/me/app`, this is usually `/home/me/app/.git`. - * For a linked worktree at `/home/me/app-feature` whose main checkout is - * `/home/me/app`, this is usually `/home/me/app/.git`. - */ - readonly store: AbsolutePath -} +export class Repository extends Schema.Class("Git.Repository")({ + worktree: AbsolutePath, + gitDirectory: AbsolutePath, + commonDirectory: AbsolutePath, +}) {} + +export const ChangeSet = Schema.String.pipe(Schema.brand("Git.ChangeSet")) +export type ChangeSet = typeof ChangeSet.Type + +export const TreeID = Schema.String.pipe(Schema.brand("Git.TreeID")) +export type TreeID = typeof TreeID.Type + +export class OperationError extends Schema.TaggedErrorClass()("Git.OperationError", { + operation: Schema.Literals([ + "clone", + "fetch", + "checkout", + "reset", + "create", + "refresh", + "write_tree", + "list_files", + "diff", + "restore", + ]), + message: Schema.String, + directory: Schema.optional(AbsolutePath), + cause: Schema.optional(Schema.Defect()), +}) {} + +export class Worktree extends Schema.Class("Git.Worktree")({ + directory: AbsolutePath, + kind: Schema.Literals(["main", "linked"]), +}) {} + +export class WorktreeError extends Schema.TaggedErrorClass()("Git.WorktreeError", { + operation: Schema.Literals(["create", "remove", "list"]), + message: Schema.String, + directory: Schema.optional(AbsolutePath), + forceRequired: Schema.optional(Schema.Boolean), + cause: Schema.optional(Schema.Defect()), +}) {} + +export class PatchError extends Schema.TaggedErrorClass()("Git.PatchError", { + operation: Schema.Literals(["capture", "apply", "reset"]), + directory: AbsolutePath, + message: Schema.String, + cause: Schema.optional(Schema.Defect()), +}) {} export interface Interface { - readonly find: (input: AbsolutePath) => Effect.Effect - readonly remote: (repo: Repo, name?: string) => Effect.Effect - readonly roots: (repo: Repo) => Effect.Effect + readonly repo: { + readonly discover: (input: AbsolutePath) => Effect.Effect + readonly clone: (input: { + remote: string + directory: AbsolutePath + branch?: string + depth?: number + }) => Effect.Effect + readonly create: (input: { + worktree: AbsolutePath + gitDirectory: AbsolutePath + seed?: Repository + }) => Effect.Effect + } + readonly remote: { + readonly get: (repository: Repository, name?: string) => Effect.Effect + } + readonly history: { + readonly head: (repository: Repository) => Effect.Effect + readonly branch: (repository: Repository) => Effect.Effect + readonly defaultRemoteBranch: (repository: Repository, remote?: string) => Effect.Effect + readonly rootCommits: (repository: Repository) => Effect.Effect + } + readonly sync: { + readonly fetchRemotes: (repository: Repository, input?: { prune?: boolean }) => Effect.Effect + readonly fetchBranch: ( + repository: Repository, + input: { remote?: string; branch: string; force?: boolean }, + ) => Effect.Effect + readonly checkoutRemoteBranch: ( + repository: Repository, + input: { remote?: string; branch: string; reset?: boolean }, + ) => Effect.Effect + readonly resetHard: (repository: Repository, revision: string) => Effect.Effect + } + readonly change: { + readonly capture: (input: { repository: Repository; path: AbsolutePath }) => Effect.Effect + readonly apply: (input: { + repository: Repository + path: AbsolutePath + changes: ChangeSet + }) => Effect.Effect + readonly discard: (input: { + repository: Repository + path: AbsolutePath + index: "preserve" | "reset" + untracked: "preserve" | "remove" + }) => Effect.Effect + } + readonly worktree: { + readonly create: (input: { + repository: Repository + directory: AbsolutePath + }) => Effect.Effect + readonly remove: (input: { + repository: Repository + directory: AbsolutePath + force: boolean + }) => Effect.Effect + readonly list: (repository: Repository) => Effect.Effect + } + readonly index: { + /** Refresh only the requested project-relative scope, preserving all other entries. */ + readonly refresh: (input: { + repository: Repository + scope: RelativePath + ignores?: Repository + maximumUntrackedFileBytes?: number + }) => Effect.Effect<{ readonly skipped: readonly RelativePath[] }, OperationError> + readonly ignored: (input: { + repository: Repository + paths: readonly RelativePath[] + }) => Effect.Effect, OperationError> + } + readonly tree: { + readonly capture: (input: { + repository: Repository + scopes: readonly RelativePath[] + ignores?: Repository + maximumUntrackedFileBytes?: number + }) => Effect.Effect + readonly write: (repository: Repository) => Effect.Effect + readonly files: (input: { + repository: Repository + from: TreeID + to: TreeID + }) => Effect.Effect + readonly diff: (input: { + repository: Repository + from: TreeID + to: TreeID + context?: number + paths?: readonly RelativePath[] + }) => Effect.Effect + readonly preview: (input: { + repository: Repository + current: TreeID + files: ReadonlyMap + context?: number + }) => Effect.Effect + readonly restore: (input: { + repository: Repository + files: ReadonlyMap + }) => Effect.Effect + readonly checkout: (input: { repository: Repository; tree: TreeID }) => Effect.Effect + } } export class Service extends Context.Service()("@opencode/GitV2") {} -export const layer = Layer.effect( +const layer = Layer.effect( Service, Effect.gen(function* () { - const fs = yield* AppFileSystem.Service + const fs = yield* FSUtil.Service const proc = yield* AppProcess.Service + const locks = KeyedMutex.makeUnsafe() + const locked = (repository: Repository, effect: Effect.Effect) => + locks.withLock(repository.gitDirectory)(effect) - const find = Effect.fn("Git.find")(function* (input: AbsolutePath) { + const discover = Effect.fn("Git.repo.discover")(function* (input: AbsolutePath) { const dotgit = yield* fs.up({ targets: [".git"], start: input }).pipe( Effect.map((matches) => matches[0]), Effect.catch(() => Effect.succeed(undefined)), @@ -50,23 +191,25 @@ export const layer = Layer.effect( const cwd = path.dirname(dotgit) const git = run(cwd, proc) const topLevel = yield* git(["rev-parse", "--show-toplevel"]) + const gitDir = yield* git(["rev-parse", "--git-dir"]) const commonDir = yield* git(["rev-parse", "--git-common-dir"]) - if (commonDir.exitCode !== 0) return undefined + if (gitDir.exitCode !== 0 || commonDir.exitCode !== 0) return undefined - return { - directory: AbsolutePath.make(topLevel.exitCode === 0 ? resolvePath(cwd, topLevel.text) : cwd), - store: AbsolutePath.make(resolvePath(cwd, commonDir.text)), - } satisfies Repo + return new Repository({ + worktree: AbsolutePath.make(topLevel.exitCode === 0 ? resolvePath(cwd, topLevel.text) : cwd), + gitDirectory: AbsolutePath.make(resolvePath(cwd, gitDir.text)), + commonDirectory: AbsolutePath.make(resolvePath(cwd, commonDir.text)), + }) }) - const remote = Effect.fn("Git.remote")(function* (repo: Repo, name = "origin") { - const result = yield* run(repo.directory, proc)(["remote", "get-url", name]) + const remote = Effect.fn("Git.remote.get")(function* (repository: Repository, name = "origin") { + const result = yield* run(repository.worktree, proc)(["remote", "get-url", name]) if (result.exitCode !== 0) return undefined return result.text.trim() || undefined }) - const roots = Effect.fn("Git.roots")(function* (repo: Repo) { - const result = yield* run(repo.directory, proc)(["rev-list", "--max-parents=0", "HEAD"]) + const roots = Effect.fn("Git.history.rootCommits")(function* (repository: Repository) { + const result = yield* run(repository.worktree, proc)(["rev-list", "--max-parents=0", "HEAD"]) if (result.exitCode !== 0) return [] return result.text .split("\n") @@ -75,21 +218,745 @@ export const layer = Layer.effect( .toSorted() }) - return Service.of({ find, remote, roots }) + const head = Effect.fn("Git.history.head")(function* (repository: Repository) { + const result = yield* run(repository.worktree, proc)(["rev-parse", "HEAD"]) + if (result.exitCode !== 0) return undefined + return result.text.trim() || undefined + }) + + const branch = Effect.fn("Git.history.branch")(function* (repository: Repository) { + const result = yield* run(repository.worktree, proc)(["symbolic-ref", "--quiet", "--short", "HEAD"]) + if (result.exitCode !== 0) return undefined + return result.text.trim() || undefined + }) + + const remoteHead = Effect.fn("Git.history.defaultRemoteBranch")(function* ( + repository: Repository, + remoteName = "origin", + ) { + const result = yield* run(repository.worktree, proc)(["symbolic-ref", `refs/remotes/${remoteName}/HEAD`]) + if (result.exitCode !== 0) return undefined + return result.text.trim().replace(new RegExp(`^refs/remotes/${remoteName}/`), "") || undefined + }) + + const operation = Effect.fnUntraced(function* ( + operation: OperationError["operation"], + directory: AbsolutePath, + args: string[], + ) { + const result = yield* execute( + directory, + proc, + )(args).pipe( + Effect.mapError((cause) => new OperationError({ operation, directory, message: cause.message, cause })), + ) + if (result.exitCode === 0) return + return yield* new OperationError({ + operation, + directory, + message: result.stderr.trim() || result.text.trim() || `Git ${operation} failed`, + }) + }) + + const clone = Effect.fn("Git.repo.clone")(function* (input: { + remote: string + directory: AbsolutePath + branch?: string + depth?: number + }) { + yield* operation("clone", AbsolutePath.make(path.dirname(input.directory)), [ + "clone", + "--depth", + String(input.depth ?? 100), + ...(input.branch ? ["--branch", input.branch] : []), + "--", + input.remote, + input.directory, + ]) + const repository = yield* discover(input.directory) + if (repository) return repository + return yield* new OperationError({ + operation: "clone", + directory: input.directory, + message: "Cloned repository could not be opened", + }) + }) + + const fetch = Effect.fn("Git.sync.fetchRemotes")(function* ( + repository: Repository, + input: { prune?: boolean } = {}, + ) { + yield* operation("fetch", repository.worktree, ["fetch", "--all", ...(input.prune === false ? [] : ["--prune"])]) + }) + + const fetchBranch = Effect.fn("Git.sync.fetchBranch")(function* ( + repository: Repository, + input: { remote?: string; branch: string; force?: boolean }, + ) { + const remoteName = input.remote ?? "origin" + const spec = `refs/heads/${input.branch}:refs/remotes/${remoteName}/${input.branch}` + yield* operation("fetch", repository.worktree, ["fetch", remoteName, input.force === false ? spec : `+${spec}`]) + }) + + const checkout = Effect.fn("Git.sync.checkoutRemoteBranch")(function* ( + repository: Repository, + input: { remote?: string; branch: string; reset?: boolean }, + ) { + const remoteName = input.remote ?? "origin" + yield* operation("checkout", repository.worktree, [ + "checkout", + ...(input.reset === false ? [input.branch] : ["-B", input.branch, `${remoteName}/${input.branch}`]), + ]) + }) + + const reset = Effect.fn("Git.sync.resetHard")(function* (repository: Repository, revision: string) { + yield* operation("reset", repository.worktree, ["reset", "--hard", revision]) + }) + + const repositoryArgs = (repository: Repository, args: string[]) => [ + "--git-dir", + repository.gitDirectory, + "--work-tree", + repository.worktree, + ...args, + ] + + const repositoryOperation = Effect.fnUntraced(function* ( + operationName: OperationError["operation"], + repository: Repository, + args: string[], + options?: { stdin?: string; env?: Record }, + ) { + const result = yield* proc + .run( + ChildProcess.make("git", repositoryArgs(repository, args), { + cwd: repository.worktree, + env: options?.env, + extendEnv: true, + }), + { stdin: options?.stdin }, + ) + .pipe( + Effect.mapError( + (cause) => + new OperationError({ + operation: operationName, + directory: repository.worktree, + message: cause.message, + cause, + }), + ), + ) + const text = result.stdout.toString("utf8") + if (result.exitCode === 0) return { text, stderr: result.stderr.toString("utf8") } + return yield* new OperationError({ + operation: operationName, + directory: repository.worktree, + message: result.stderr.toString("utf8").trim() || text.trim() || `Git ${operationName} failed`, + }) + }) + + const create = Effect.fn("Git.repo.create")(function* (input: { + worktree: AbsolutePath + gitDirectory: AbsolutePath + seed?: Repository + }) { + yield* fs.ensureDir(input.gitDirectory).pipe( + Effect.mapError( + (cause) => + new OperationError({ + operation: "create", + directory: input.gitDirectory, + message: "Failed to create Git storage", + cause, + }), + ), + ) + const repository = new Repository({ + worktree: input.worktree, + gitDirectory: input.gitDirectory, + commonDirectory: input.gitDirectory, + }) + yield* repositoryOperation("create", repository, ["init"]) + yield* Effect.forEach( + [ + ["core.autocrlf", "false"], + ["core.longpaths", "true"], + ["core.symlinks", "true"], + ["core.fsmonitor", "false"], + ["feature.manyFiles", "true"], + ["index.version", "4"], + ["index.threads", "true"], + ["core.untrackedCache", "true"], + ], + ([key, value]) => repositoryOperation("create", repository, ["config", key, value]), + { discard: true }, + ) + if (!input.seed) return repository + yield* fs.ensureDir(path.join(input.gitDirectory, "objects", "info")).pipe( + Effect.mapError( + (cause) => + new OperationError({ + operation: "create", + directory: input.gitDirectory, + message: "Failed to configure shared Git objects", + cause, + }), + ), + ) + yield* fs + .writeFileString( + path.join(input.gitDirectory, "objects", "info", "alternates"), + path.join(input.seed.commonDirectory, "objects") + "\n", + ) + .pipe( + Effect.mapError( + (cause) => + new OperationError({ + operation: "create", + directory: input.gitDirectory, + message: "Failed to configure shared Git objects", + cause, + }), + ), + ) + yield* fs + .copyFile(path.join(input.seed.gitDirectory, "index"), path.join(input.gitDirectory, "index")) + .pipe(Effect.catch(() => Effect.void)) + return repository + }) + + const refresh = Effect.fn("Git.index.refresh")(function* (input: { + repository: Repository + scope: RelativePath + ignores?: Repository + maximumUntrackedFileBytes?: number + }) { + const list = (args: string[]) => + repositoryOperation("refresh", input.repository, args).pipe( + Effect.map((result) => result.text.split("\0").filter(Boolean)), + ) + const [tracked, untracked] = yield* Effect.all( + [ + list(["diff-files", "--name-only", "-z", "--", input.scope]), + list(["ls-files", "--others", "--exclude-standard", "-z", "--", input.scope]), + ], + { concurrency: 2 }, + ) + const candidates = Array.from(new Set([...tracked, ...untracked])) + if (!candidates.length) return { skipped: [] } + const ignored = input.ignores + ? new Set( + (yield* repositoryOperation("refresh", input.ignores, ["check-ignore", "--no-index", "--stdin", "-z"], { + stdin: candidates.join("\0") + "\0", + }).pipe(Effect.catch(() => Effect.succeed({ text: "", stderr: "" })))).text + .split("\0") + .filter(Boolean), + ) + : new Set() + const allowed = candidates.filter((item) => !ignored.has(item)) + const maximum = input.maximumUntrackedFileBytes + const skipped = maximum + ? (yield* Effect.forEach( + untracked.filter((item) => allowed.includes(item)), + (item) => + fs.stat(path.join(input.repository.worktree, item)).pipe( + Effect.map((info) => + info.type === "File" && Number(info.size) > maximum ? RelativePath.make(item) : undefined, + ), + Effect.catch(() => Effect.succeed(undefined)), + ), + { concurrency: 8 }, + )).filter((item): item is RelativePath => item !== undefined) + : [] + const stage = allowed.filter((item) => !skipped.includes(RelativePath.make(item))) + const remove = [...ignored, ...skipped] + if (remove.length) + yield* repositoryOperation( + "refresh", + input.repository, + ["rm", "--cached", "-f", "--ignore-unmatch", "--pathspec-from-file=-", "--pathspec-file-nul"], + { stdin: remove.join("\0") + "\0" }, + ) + if (stage.length) + yield* repositoryOperation( + "refresh", + input.repository, + ["add", "--all", "--sparse", "--pathspec-from-file=-", "--pathspec-file-nul"], + { stdin: stage.join("\0") + "\0" }, + ) + return { skipped } + }) + + const ignored = Effect.fn("Git.index.ignored")(function* (input: { + repository: Repository + paths: readonly RelativePath[] + }) { + if (!input.paths.length) return new Set() + const result = yield* proc + .run( + ChildProcess.make("git", repositoryArgs(input.repository, ["check-ignore", "--no-index", "--stdin", "-z"]), { + cwd: input.repository.worktree, + extendEnv: true, + }), + { stdin: input.paths.join("\0") + "\0" }, + ) + .pipe( + Effect.mapError( + (cause) => + new OperationError({ + operation: "list_files", + directory: input.repository.worktree, + message: cause.message, + cause, + }), + ), + ) + if (result.exitCode !== 0 && result.exitCode !== 1) + return yield* new OperationError({ + operation: "list_files", + directory: input.repository.worktree, + message: result.stderr.toString("utf8").trim() || "Failed to check ignored paths", + }) + return new Set( + result.stdout + .toString("utf8") + .split("\0") + .filter(Boolean) + .map((file) => RelativePath.make(file)), + ) + }) + + const writeTree = Effect.fn("Git.tree.write")(function* (repository: Repository) { + return TreeID.make((yield* repositoryOperation("write_tree", repository, ["write-tree"])).text.trim()) + }) + + const captureTree = Effect.fn("Git.tree.capture")( + (input: { + repository: Repository + scopes: readonly RelativePath[] + ignores?: Repository + maximumUntrackedFileBytes?: number + }) => + locked( + input.repository, + Effect.gen(function* () { + yield* Effect.forEach(input.scopes, (scope) => refresh({ ...input, scope }), { discard: true }) + return yield* writeTree(input.repository) + }), + ), + ) + + const treeFiles = Effect.fn("Git.tree.files")(function* (input: { + repository: Repository + from: TreeID + to: TreeID + }) { + return (yield* repositoryOperation("list_files", input.repository, [ + "diff", + "--name-only", + "-z", + input.from, + input.to, + ])).text + .split("\0") + .filter(Boolean) + .map((file) => RelativePath.make(file)) + }) + + const treeDiff = Effect.fn("Git.tree.diff")(function* (input: { + repository: Repository + from: TreeID + to: TreeID + context?: number + paths?: readonly RelativePath[] + }) { + const paths = input.paths ?? (yield* treeFiles(input)) + return yield* Effect.forEach(paths, (file) => + Effect.gen(function* () { + const statusText = (yield* repositoryOperation("diff", input.repository, [ + "diff", + "--name-status", + "--no-renames", + input.from, + input.to, + "--", + file, + ])).text.trim() + const status = statusText.startsWith("A") ? "added" : statusText.startsWith("D") ? "deleted" : "modified" + const stats = (yield* repositoryOperation("diff", input.repository, [ + "diff", + "--numstat", + "--no-renames", + input.from, + input.to, + "--", + file, + ])).text.split("\t") + const binary = stats[0] === "-" || stats[1] === "-" + const patch = binary + ? "" + : (yield* repositoryOperation("diff", input.repository, [ + "diff", + `--unified=${input.context ?? 3}`, + "--no-renames", + input.from, + input.to, + "--", + file, + ])).text + return { + path: file, + status, + additions: binary ? 0 : Number(stats[0] ?? 0), + deletions: binary ? 0 : Number(stats[1] ?? 0), + patch, + } satisfies File.Diff + }), + ) + }) + + const entry = Effect.fnUntraced(function* (repository: Repository, tree: TreeID, file: RelativePath) { + const text = (yield* repositoryOperation("restore", repository, [ + "ls-tree", + "-z", + tree, + "--", + file, + ])).text.replace(/\0$/, "") + if (!text) return + const match = text.match(/^(\d+)\s+\w+\s+([0-9a-f]+)\t/) + if (!match) + return yield* new OperationError({ + operation: "restore", + directory: repository.worktree, + message: `Invalid tree entry for ${file}`, + }) + return { mode: match[1], object: match[2] } + }) + + const preview = Effect.fn("Git.tree.preview")( + (input: { + repository: Repository + current: TreeID + files: ReadonlyMap + context?: number + }) => + locked( + input.repository, + Effect.gen(function* () { + const index = path.join(input.repository.gitDirectory, `preview-${randomUUID()}.index`) + const env = { GIT_INDEX_FILE: index } + return yield* Effect.gen(function* () { + yield* repositoryOperation("diff", input.repository, ["read-tree", input.current], { env }) + yield* Effect.forEach( + input.files, + ([file, tree]) => + Effect.gen(function* () { + const source = yield* entry(input.repository, tree, file) + if (!source) { + yield* repositoryOperation( + "diff", + input.repository, + ["update-index", "--force-remove", "--", file], + { env }, + ) + return + } + yield* repositoryOperation( + "diff", + input.repository, + ["update-index", "--add", "--cacheinfo", source.mode, source.object, file], + { env }, + ) + }), + { discard: true }, + ) + const target = TreeID.make( + (yield* repositoryOperation("diff", input.repository, ["write-tree"], { env })).text.trim(), + ) + return yield* treeDiff({ + repository: input.repository, + from: input.current, + to: target, + context: input.context, + paths: Array.from(input.files.keys()), + }) + }).pipe(Effect.ensuring(fs.remove(index).pipe(Effect.catch(() => Effect.void)))) + }), + ), + ) + + const restore = Effect.fn("Git.tree.restore")( + (input: { repository: Repository; files: ReadonlyMap }) => + locked( + input.repository, + Effect.forEach( + input.files, + ([file, tree]) => + Effect.gen(function* () { + if (yield* entry(input.repository, tree, file)) { + yield* repositoryOperation("restore", input.repository, ["checkout", tree, "--", file]) + return + } + yield* fs.remove(path.join(input.repository.worktree, file), { recursive: true, force: true }).pipe( + Effect.mapError( + (cause) => + new OperationError({ + operation: "restore", + directory: input.repository.worktree, + message: `Failed to remove ${file}`, + cause, + }), + ), + ) + }), + { discard: true }, + ), + ), + ) + + const checkoutTree = Effect.fn("Git.tree.checkout")((input: { repository: Repository; tree: TreeID }) => + locked( + input.repository, + Effect.gen(function* () { + yield* repositoryOperation("restore", input.repository, ["read-tree", input.tree]) + yield* repositoryOperation("restore", input.repository, ["checkout-index", "--all", "--force"]) + }), + ), + ) + + const capture = Effect.fn("Git.change.capture")(function* (input: { repository: Repository; path: AbsolutePath }) { + const scope = path.relative(input.repository.worktree, input.path).replaceAll("\\", "/") || "." + const tracked = yield* execute( + input.repository.worktree, + proc, + )(["diff", "--binary", "HEAD", "--", scope]).pipe( + Effect.mapError( + (cause) => new PatchError({ operation: "capture", directory: input.path, message: cause.message, cause }), + ), + ) + if (tracked.exitCode !== 0) { + return yield* new PatchError({ + operation: "capture", + directory: input.path, + message: tracked.stderr.trim() || tracked.text.trim() || "Failed to capture tracked changes", + }) + } + + const untracked = yield* execute( + input.repository.worktree, + proc, + )(["ls-files", "--others", "--exclude-standard", "-z", "--", scope]).pipe( + Effect.mapError( + (cause) => new PatchError({ operation: "capture", directory: input.path, message: cause.message, cause }), + ), + ) + if (untracked.exitCode !== 0) { + return yield* new PatchError({ + operation: "capture", + directory: input.path, + message: untracked.stderr.trim() || untracked.text.trim() || "Failed to list untracked changes", + }) + } + + const created = yield* Effect.forEach(untracked.text.split("\0").filter(Boolean), (file) => + execute( + input.repository.worktree, + proc, + )(["diff", "--binary", "--no-index", "--", "/dev/null", file]).pipe( + Effect.mapError( + (cause) => new PatchError({ operation: "capture", directory: input.path, message: cause.message, cause }), + ), + Effect.flatMap((result) => + // git diff --no-index returns 1 when differences were found. + result.exitCode === 0 || result.exitCode === 1 + ? Effect.succeed(result.text) + : Effect.fail( + new PatchError({ + operation: "capture", + directory: input.path, + message: + result.stderr.trim() || result.text.trim() || `Failed to capture untracked change: ${file}`, + }), + ), + ), + ), + ) + return ChangeSet.make([tracked.text, ...created].filter(Boolean).join("\n")) + }) + + const apply = Effect.fn("Git.change.apply")(function* (input: { + repository: Repository + path: AbsolutePath + changes: ChangeSet + }) { + const result = yield* proc + .run( + ChildProcess.make("git", ["apply", "-"], { + cwd: input.path, + extendEnv: true, + stdin: Stream.make(new TextEncoder().encode(input.changes)), + }), + ) + .pipe( + Effect.mapError( + (cause) => new PatchError({ operation: "apply", directory: input.path, message: cause.message, cause }), + ), + ) + if (result.exitCode === 0) return + return yield* new PatchError({ + operation: "apply", + directory: input.path, + message: + result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim() || "Failed to apply changes", + }) + }) + + const discard = Effect.fn("Git.change.discard")(function* (input: { + repository: Repository + path: AbsolutePath + index: "preserve" | "reset" + untracked: "preserve" | "remove" + }) { + const scope = path.relative(input.repository.worktree, input.path).replaceAll("\\", "/") || "." + const restore = yield* execute( + input.repository.worktree, + proc, + )(input.index === "reset" ? ["checkout", "HEAD", "--", scope] : ["checkout", "--", scope]).pipe( + Effect.mapError( + (cause) => new PatchError({ operation: "reset", directory: input.path, message: cause.message, cause }), + ), + ) + if (restore.exitCode !== 0) { + return yield* new PatchError({ + operation: "reset", + directory: input.path, + message: restore.stderr.trim() || restore.text.trim() || "Failed to restore tracked changes", + }) + } + if (input.untracked === "preserve") return + const clean = yield* execute( + input.repository.worktree, + proc, + )(["clean", "-fd", "--", scope]).pipe( + Effect.mapError( + (cause) => new PatchError({ operation: "reset", directory: input.path, message: cause.message, cause }), + ), + ) + if (clean.exitCode === 0) return + return yield* new PatchError({ + operation: "reset", + directory: input.path, + message: clean.stderr.trim() || clean.text.trim() || "Failed to clean untracked changes", + }) + }) + + const worktreeRun = Effect.fnUntraced(function* ( + operation: "create" | "remove" | "list", + repository: Repository, + args: string[], + worktreeDirectory?: AbsolutePath, + cwd = repository.worktree, + ) { + const result = yield* proc + .run(ChildProcess.make("git", args, { cwd, extendEnv: true, stdin: "ignore" })) + .pipe( + Effect.mapError( + (cause) => new WorktreeError({ operation, directory: worktreeDirectory, message: cause.message, cause }), + ), + ) + if (result.exitCode === 0) return result.stdout.toString("utf8") + const message = result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim() || "Git failed" + return yield* new WorktreeError({ + operation, + directory: worktreeDirectory, + message, + forceRequired: operation === "remove" && /contains modified or untracked files|is dirty/i.test(message), + }) + }) + + const worktreeCreate = Effect.fn("Git.worktree.create")(function* (input: { + repository: Repository + directory: AbsolutePath + }) { + yield* worktreeRun( + "create", + input.repository, + ["worktree", "add", "--detach", input.directory, "HEAD"], + input.directory, + ) + const repository = yield* discover(input.directory) + if (repository) return repository + return yield* new WorktreeError({ + operation: "create", + directory: input.directory, + message: "Created worktree could not be opened", + }) + }) + + const worktreeRemove = Effect.fn("Git.worktree.remove")(function* (input: { + repository: Repository + directory: AbsolutePath + force: boolean + }) { + yield* worktreeRun( + "remove", + input.repository, + ["worktree", "remove", ...(input.force ? ["--force"] : []), input.directory], + input.directory, + input.repository.commonDirectory, + ) + }) + + const worktreeList = Effect.fn("Git.worktree.list")(function* (repository: Repository) { + return (yield* worktreeRun("list", repository, ["worktree", "list", "--porcelain"])) + .split("\n") + .filter((line) => line.startsWith("worktree ")) + .map( + (line, index) => + new Worktree({ + directory: AbsolutePath.make(resolvePath(repository.worktree, line.slice("worktree ".length).trim())), + kind: index === 0 ? "main" : "linked", + }), + ) + }) + + return Service.of({ + repo: { discover, clone, create }, + remote: { get: remote }, + history: { head, branch, defaultRemoteBranch: remoteHead, rootCommits: roots }, + sync: { fetchRemotes: fetch, fetchBranch, checkoutRemoteBranch: checkout, resetHard: reset }, + change: { capture, apply, discard }, + worktree: { create: worktreeCreate, remove: worktreeRemove, list: worktreeList }, + index: { refresh, ignored }, + tree: { + capture: captureTree, + write: writeTree, + files: treeFiles, + diff: treeDiff, + preview, + restore, + checkout: checkoutTree, + }, + }) }), ) -export const defaultLayer = layer.pipe( - Layer.provide(AppFileSystem.defaultLayer), - Layer.provide(AppProcess.defaultLayer), -) +export const node = makeGlobalNode({ service: Service, layer: layer, deps: [FSUtil.node, AppProcess.node] }) interface Result { readonly exitCode: number readonly text: string + readonly stderr: string } function run(cwd: string, proc: AppProcess.Interface) { + return (args: string[]) => + execute(cwd, proc)(args).pipe(Effect.catch(() => Effect.succeed({ exitCode: 1, text: "", stderr: "" }))) +} + +function execute(cwd: string, proc: AppProcess.Interface) { return (args: string[]) => proc .run( @@ -100,15 +967,21 @@ function run(cwd: string, proc: AppProcess.Interface) { }), ) .pipe( - Effect.map((result) => ({ exitCode: result.exitCode, text: result.stdout.toString("utf8") }) satisfies Result), - Effect.catch(() => Effect.succeed({ exitCode: 1, text: "" } satisfies Result)), + Effect.map( + (result) => + ({ + exitCode: result.exitCode, + text: result.stdout.toString("utf8"), + stderr: result.stderr.toString("utf8"), + }) satisfies Result, + ), ) } function resolvePath(cwd: string, value: string) { const trimmed = value.replace(/[\r\n]+$/, "") if (!trimmed) return cwd - const normalized = AppFileSystem.windowsPath(trimmed) + const normalized = FSUtil.windowsPath(trimmed) if (path.isAbsolute(normalized)) return path.normalize(normalized) return path.resolve(cwd, normalized) } diff --git a/packages/core/src/github-copilot/responses/convert-to-openai-responses-input.ts b/packages/core/src/github-copilot/responses/convert-to-openai-responses-input.ts index 83e46015dd1..1e4f86d9335 100644 --- a/packages/core/src/github-copilot/responses/convert-to-openai-responses-input.ts +++ b/packages/core/src/github-copilot/responses/convert-to-openai-responses-input.ts @@ -86,7 +86,7 @@ export async function convertToOpenAIResponsesInput({ : { image_url: `data:${mediaType};base64,${convertToBase64(part.data)}`, }), - detail: part.providerOptions?.openai?.imageDetail, + detail: part.providerOptions?.copilot?.imageDetail, } } else if (part.mediaType === "application/pdf") { if (part.data instanceof URL) { @@ -127,7 +127,7 @@ export async function convertToOpenAIResponsesInput({ input.push({ role: "assistant", content: [{ type: "output_text", text: part.text }], - id: (part.providerOptions?.openai?.itemId as string) ?? undefined, + id: (part.providerOptions?.copilot?.itemId as string) ?? undefined, }) break } @@ -143,7 +143,7 @@ export async function convertToOpenAIResponsesInput({ input.push({ type: "local_shell_call", call_id: part.toolCallId, - id: (part.providerOptions?.openai?.itemId as string) ?? undefined, + id: (part.providerOptions?.copilot?.itemId as string) ?? undefined, action: { type: "exec", command: parsedInput.action.command, @@ -162,7 +162,7 @@ export async function convertToOpenAIResponsesInput({ call_id: part.toolCallId, name: part.toolName, arguments: JSON.stringify(part.input), - id: (part.providerOptions?.openai?.itemId as string) ?? undefined, + id: (part.providerOptions?.copilot?.itemId as string) ?? undefined, }) break } @@ -275,7 +275,7 @@ export async function convertToOpenAIResponsesInput({ const output = part.output if (output.type === "execution-denied") { - const approvalId = (output.providerOptions?.openai as { approvalId?: string } | undefined)?.approvalId + const approvalId = (output.providerOptions?.copilot as { approvalId?: string } | undefined)?.approvalId if (approvalId) { continue diff --git a/packages/core/src/github-copilot/responses/openai-responses-language-model.ts b/packages/core/src/github-copilot/responses/openai-responses-language-model.ts index 250d1f6f340..8df1dcedadb 100644 --- a/packages/core/src/github-copilot/responses/openai-responses-language-model.ts +++ b/packages/core/src/github-copilot/responses/openai-responses-language-model.ts @@ -525,7 +525,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { type: "reasoning" as const, text: summary.text, providerMetadata: { - openai: { + copilot: { itemId: part.id, reasoningEncryptedContent: part.encrypted_content ?? null, }, @@ -563,7 +563,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { toolName: "local_shell", input: JSON.stringify({ action: part.action } satisfies z.infer), providerMetadata: { - openai: { + copilot: { itemId: part.id, }, }, @@ -574,7 +574,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { case "message": { for (const contentPart of part.content) { - if (options.providerOptions?.openai?.logprobs && contentPart.logprobs) { + if (options.providerOptions?.copilot?.logprobs && contentPart.logprobs) { logprobs.push(contentPart.logprobs) } @@ -582,7 +582,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { type: "text", text: contentPart.text, providerMetadata: { - openai: { + copilot: { itemId: part.id, }, }, @@ -622,7 +622,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { toolName: part.name, input: part.arguments, providerMetadata: { - openai: { + copilot: { itemId: part.id, }, }, @@ -724,15 +724,15 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { } const providerMetadata: SharedV3ProviderMetadata = { - openai: { responseId: response.id }, + copilot: { responseId: response.id }, } if (logprobs.length > 0) { - providerMetadata.openai.logprobs = logprobs + providerMetadata.copilot.logprobs = logprobs } if (typeof response.service_tier === "string") { - providerMetadata.openai.serviceTier = response.service_tier + providerMetadata.copilot.serviceTier = response.service_tier } return { @@ -954,7 +954,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { type: "text-start", id: value.item.id, providerMetadata: { - openai: { + copilot: { itemId: value.item.id, }, }, @@ -971,7 +971,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { type: "reasoning-start", id: `${value.item.id}:0`, providerMetadata: { - openai: { + copilot: { itemId: value.item.id, reasoningEncryptedContent: value.item.encrypted_content ?? null, }, @@ -994,7 +994,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { toolName: value.item.name, input: value.item.arguments, providerMetadata: { - openai: { + copilot: { itemId: value.item.id, }, }, @@ -1103,7 +1103,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { }, } satisfies z.infer), providerMetadata: { - openai: { itemId: value.item.id }, + copilot: { itemId: value.item.id }, }, }) } else if (value.item.type === "message") { @@ -1122,7 +1122,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { type: "reasoning-end", id: `${activeReasoningPart.canonicalId}:${summaryIndex}`, providerMetadata: { - openai: { + copilot: { itemId: activeReasoningPart.canonicalId, reasoningEncryptedContent: value.item.encrypted_content ?? null, }, @@ -1209,7 +1209,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { type: "text-start", id: currentTextId, providerMetadata: { - openai: { itemId: value.item_id }, + copilot: { itemId: value.item_id }, }, }) } @@ -1220,7 +1220,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { delta: value.delta, }) - if (options.providerOptions?.openai?.logprobs && value.logprobs) { + if (options.providerOptions?.copilot?.logprobs && value.logprobs) { logprobs.push(value.logprobs) } } else if (isResponseReasoningSummaryPartAddedChunk(value)) { @@ -1235,7 +1235,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { type: "reasoning-start", id: `${activeItem.canonicalId}:${value.summary_index}`, providerMetadata: { - openai: { + copilot: { itemId: activeItem.canonicalId, reasoningEncryptedContent: activeItem.encryptedContent ?? null, }, @@ -1252,7 +1252,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { id: `${activeItem.canonicalId}:${value.summary_index}`, delta: value.delta, providerMetadata: { - openai: { + copilot: { itemId: activeItem.canonicalId, }, }, @@ -1306,17 +1306,17 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { } const providerMetadata: SharedV3ProviderMetadata = { - openai: { + copilot: { responseId, }, } if (logprobs.length > 0) { - providerMetadata.openai.logprobs = logprobs + providerMetadata.copilot.logprobs = logprobs } if (serviceTier !== undefined) { - providerMetadata.openai.serviceTier = serviceTier + providerMetadata.copilot.serviceTier = serviceTier } controller.enqueue({ diff --git a/packages/core/src/global.ts b/packages/core/src/global.ts index 9c923f826bd..10861364ab4 100644 --- a/packages/core/src/global.ts +++ b/packages/core/src/global.ts @@ -5,8 +5,9 @@ import os from "os" import { Context, Effect, Layer } from "effect" import { Flock } from "./util/flock" import { markNoIndex } from "./kilocode/spotlight" // kilocode_change -import { ensureRealDir } from "./kilocode/global" // kilocode_change +import { ensureRealDir, resolveState } from "./kilocode/global" // kilocode_change import { Flag } from "./flag/flag" +import { makeGlobalNode } from "./effect/app-node" const app = "kilo" // kilocode_change // kilocode_change start @@ -21,7 +22,8 @@ const clean = (p: string | undefined) => p?.replace(/[\r\n]+/g, "") const data = path.join(clean(xdgData)!, app) const cache = path.join(clean(xdgCache)!, app) const config = path.join(clean(xdgConfig)!, app) -const state = path.join(clean(xdgState)!, app) +const preferred = path.join(clean(xdgState)!, app) +const state = await resolveState(preferred, process.env.XDG_STATE_HOME ? undefined : path.join(data, "state")) // kilocode_change end const tmp = path.join(os.tmpdir(), app) @@ -46,7 +48,6 @@ Flock.setGlobal({ state }) await Promise.all([ ensureRealDir(Path.data), // kilocode_change ensureRealDir(Path.config), // kilocode_change - ensureRealDir(Path.state), // kilocode_change ensureRealDir(Path.tmp), // kilocode_change ensureRealDir(Path.log), // kilocode_change ensureRealDir(Path.bin), // kilocode_change @@ -86,12 +87,12 @@ export function make(input: Partial = {}): Interface { } } -export const layer = Layer.effect( +const layer = Layer.effect( Service, Effect.sync(() => Service.of(make())), ) -export const defaultLayer = layer +export const node = makeGlobalNode({ service: Service, layer: layer, deps: [] }) export const layerWith = (input: Partial) => Layer.effect( diff --git a/packages/core/src/id/id.ts b/packages/core/src/id/id.ts new file mode 100644 index 00000000000..be1efc446a6 --- /dev/null +++ b/packages/core/src/id/id.ts @@ -0,0 +1,47 @@ +import { create as createIdentifier } from "@opencode-ai/schema/identifier" + +const prefixes = { + job: "job", + event: "evt", + session: "ses", + message: "msg", + permission: "per", + question: "que", + part: "prt", + pty: "pty", + tool: "tool", + workspace: "wrk", +} as const + +export function ascending(prefix: keyof typeof prefixes, given?: string) { + return generateID(prefix, "ascending", given) +} + +export function descending(prefix: keyof typeof prefixes, given?: string) { + return generateID(prefix, "descending", given) +} + +function generateID(prefix: keyof typeof prefixes, direction: "descending" | "ascending", given?: string): string { + if (!given) { + return create(prefixes[prefix], direction) + } + + if (!given.startsWith(prefixes[prefix])) { + throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`) + } + return given +} + +export function create(prefix: string, direction: "descending" | "ascending", timestamp?: number): string { + return prefix + "_" + createIdentifier(direction === "descending", timestamp) +} + +/** Extract timestamp from an ascending ID. Does not work with descending IDs. */ +export function timestamp(id: string): number { + const prefix = id.split("_")[0] + const hex = id.slice(prefix.length + 1, prefix.length + 13) + const encoded = BigInt("0x" + hex) + return Number(encoded / BigInt(0x1000)) +} + +export * as Identifier from "./id" diff --git a/packages/core/src/image.ts b/packages/core/src/image.ts new file mode 100644 index 00000000000..901adbf5b09 --- /dev/null +++ b/packages/core/src/image.ts @@ -0,0 +1,95 @@ +export * as Image from "./image" + +import { makeLocationNode } from "./effect/app-node" +import { Context, Effect, Layer, Schema } from "effect" +import { Config } from "./config" +import { FileSystem } from "./filesystem" + +export class ResizerUnavailableError extends Schema.TaggedErrorClass()( + "Image.ResizerUnavailableError", + {}, +) {} + +export class DecodeError extends Schema.TaggedErrorClass()("Image.DecodeError", { + resource: Schema.String, +}) { + override get message() { + return `Image could not be decoded: ${this.resource}` + } +} + +// kilocode_change start - report images rejected before native decode allocation +export class PixelLimitError extends Schema.TaggedErrorClass()("Image.PixelLimitError", { + resource: Schema.String, + width: Schema.Number, + height: Schema.Number, + maxDimension: Schema.Number, + maxPixels: Schema.Number, +}) { + override get message() { + return `Image ${this.resource} is ${this.width}x${this.height}, exceeding the safe decode limit of ${this.maxDimension}px per side/${this.maxPixels} pixels` + } +} +// kilocode_change end + +export class SizeError extends Schema.TaggedErrorClass()("Image.SizeError", { + resource: Schema.String, + width: Schema.Number, + height: Schema.Number, + bytes: Schema.Number, + maxWidth: Schema.Number, + maxHeight: Schema.Number, + maxBytes: Schema.Number, +}) { + override get message() { + return `Image ${this.resource} is ${this.width}x${this.height} with base64 size ${this.bytes}, exceeding configured limits ${this.maxWidth}x${this.maxHeight}/${this.maxBytes} bytes` + } +} + +export interface Interface { + readonly normalize: ( + resource: string, + content: FileSystem.Content & { readonly encoding: "base64" }, + ) => Effect.Effect< + FileSystem.Content & { readonly encoding: "base64" }, + ResizerUnavailableError | DecodeError | SizeError | PixelLimitError // kilocode_change + > +} + +export class Service extends Context.Service()("@opencode/Image") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const config = yield* Config.Service + const loadAdapter = yield* Effect.cached( + Effect.tryPromise({ + try: () => import("./image/photon"), + catch: () => new ResizerUnavailableError(), + }).pipe(Effect.flatMap((adapter) => adapter.make)), + ) + const normalize = Effect.fn("Image.normalize")(function* ( + resource: string, + content: FileSystem.Content & { readonly encoding: "base64" }, + ) { + const image = Object.assign( + {}, + ...(yield* config.entries()).flatMap((entry) => + entry.type === "document" && entry.info.attachments?.image ? [entry.info.attachments.image] : [], + ), + ) + const normalize = yield* loadAdapter + return yield* normalize(resource, content, { + autoResize: image.auto_resize ?? true, + maxWidth: image.max_width ?? 2_000, + maxHeight: image.max_height ?? 2_000, + maxBase64Bytes: image.max_base64_bytes ?? 5 * 1024 * 1024, + }) + }) + return Service.of({ normalize }) + }), +) + +export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer)) + +export const node = makeLocationNode({ service: Service, layer, deps: [Config.node] }) diff --git a/packages/core/src/image/photon.ts b/packages/core/src/image/photon.ts new file mode 100644 index 00000000000..a8df2a2b35e --- /dev/null +++ b/packages/core/src/image/photon.ts @@ -0,0 +1,110 @@ +// @ts-ignore Bun's static file import is embedded by `bun build --compile`; some consumers also declare *.wasm. +import photonWasm from "@silvia-odwyer/photon-node/photon_rs_bg.wasm" with { type: "file" } +import { Effect } from "effect" +import path from "node:path" +import { fileURLToPath } from "node:url" +import { FileSystem } from "../filesystem" +import { DecodeError, PixelLimitError, ResizerUnavailableError, SizeError } from "../image" // kilocode_change +import { allowed, dimensions, MAX_DIMENSION, MAX_PIXELS } from "../kilocode/image-size" // kilocode_change + +const JPEG_QUALITIES = [80, 85, 70, 55, 40] + +export const make = Effect.gen(function* () { + ;(globalThis as typeof globalThis & { __OPENCODE_PHOTON_WASM_PATH?: string }).__OPENCODE_PHOTON_WASM_PATH = + path.isAbsolute(photonWasm) ? photonWasm : fileURLToPath(new URL(photonWasm, import.meta.url)) + const loadPhoton = yield* Effect.cached( + Effect.tryPromise({ + try: () => import("@silvia-odwyer/photon-node"), + catch: () => new ResizerUnavailableError(), + }), + ) + return Effect.fn("Image.Photon.normalize")(function* ( + resource: string, + content: FileSystem.Content & { readonly encoding: "base64" }, + limits: { + readonly autoResize: boolean + readonly maxWidth: number + readonly maxHeight: number + readonly maxBase64Bytes: number + }, + ) { + // kilocode_change start - reject decompression bombs before Photon allocates native pixels + const input = Buffer.from(content.content, "base64") + const size = yield* Effect.try({ + try: () => dimensions(input), + catch: () => new DecodeError({ resource }), + }) + if (!allowed(size)) + return yield* new PixelLimitError({ + resource, + width: size.width, + height: size.height, + maxDimension: MAX_DIMENSION, + maxPixels: MAX_PIXELS, + }) + // kilocode_change end + const photon = yield* loadPhoton + const decoded = yield* Effect.try({ + try: () => photon.PhotonImage.new_from_byteslice(input), // kilocode_change + catch: () => new DecodeError({ resource }), + }) + try { + const width = decoded.get_width() + const height = decoded.get_height() + const bytes = Buffer.byteLength(content.content, "utf-8") + if (width <= limits.maxWidth && height <= limits.maxHeight && bytes <= limits.maxBase64Bytes) return content + if (!limits.autoResize) + return yield* new SizeError({ + resource, + width, + height, + bytes, + maxWidth: limits.maxWidth, + maxHeight: limits.maxHeight, + maxBytes: limits.maxBase64Bytes, + }) + const scale = Math.min(1, limits.maxWidth / width, limits.maxHeight / height) + const sizes = Array.from({ length: 32 }).reduce>((acc) => { + const previous = acc.at(-1) ?? { + width: Math.max(1, Math.round(width * scale)), + height: Math.max(1, Math.round(height * scale)), + } + const next = + acc.length === 0 + ? previous + : { + width: previous.width === 1 ? 1 : Math.max(1, Math.floor(previous.width * 0.75)), + height: previous.height === 1 ? 1 : Math.max(1, Math.floor(previous.height * 0.75)), + } + return acc.some((item) => item.width === next.width && item.height === next.height) ? acc : [...acc, next] + }, []) + for (const size of sizes) { + const resized = photon.resize(decoded, size.width, size.height, photon.SamplingFilter.Lanczos3) + try { + const encoders: Array Uint8Array]> = [ + ["image/png", () => resized.get_bytes()], + ...JPEG_QUALITIES.map((quality) => ["image/jpeg", () => resized.get_bytes_jpeg(quality)] as const), + ] + for (const [mime, encode] of encoders) { + const candidate = Buffer.from(encode()).toString("base64") + if (Buffer.byteLength(candidate, "utf-8") <= limits.maxBase64Bytes) + return { ...content, content: candidate, encoding: "base64" as const, mime } + } + } finally { + resized.free() + } + } + return yield* new SizeError({ + resource, + width, + height, + bytes, + maxWidth: limits.maxWidth, + maxHeight: limits.maxHeight, + maxBytes: limits.maxBase64Bytes, + }) + } finally { + decoded.free() + } + }) +}) diff --git a/packages/core/src/instruction-context.ts b/packages/core/src/instruction-context.ts new file mode 100644 index 00000000000..44e0e617182 --- /dev/null +++ b/packages/core/src/instruction-context.ts @@ -0,0 +1,101 @@ +export * as InstructionContext from "./instruction-context" + +import { Array, Effect, Layer, Schema } from "effect" +import { isAbsolute, join, relative, sep } from "path" +import { FSUtil } from "./fs-util" +import { Flag } from "./flag/flag" +import { Global } from "./global" +import { Location } from "./location" +import { AbsolutePath } from "./schema" +import { SystemContext } from "./system-context/index" +import { SystemContextRegistry } from "./system-context/registry" +import { makeLocationNode } from "./effect/app-node" + +class File extends Schema.Class("InstructionContext.File")({ + path: AbsolutePath, + content: Schema.String, +}) {} + +const Files = Schema.Array(File) +const key = SystemContext.Key.make("core/instructions") + +const layer = Layer.effectDiscard( + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const global = yield* Global.Service + const location = yield* Location.Service + const registry = yield* SystemContextRegistry.Service + + const source = (value: ReadonlyArray | SystemContext.Unavailable) => + SystemContext.make({ + key, + codec: Schema.toCodecJson(Files), + load: Effect.succeed(value), + baseline: render, + update: (_previous, current) => + `These instructions replace all previously loaded ambient instructions.\n\n${render(current)}`, + removed: () => "Previously loaded instructions no longer apply.", + }) + + const observe = Effect.fn("InstructionContext.observe")(function* () { + const start = yield* fs.resolve(location.directory) + const stop = yield* fs.resolve(location.project.directory) + const fromProject = relative(stop, start) + const insideProject = + fromProject === "" || (fromProject !== ".." && !fromProject.startsWith(`..${sep}`) && !isAbsolute(fromProject)) + const discovered = new Set( + yield* Effect.forEach( + Flag.KILO_DISABLE_PROJECT_CONFIG || !insideProject + ? [] + : yield* fs.up({ + targets: ["AGENTS.md"], + start, + stop, + }), + fs.resolve, + ), + ) + const paths = Array.dedupe([yield* fs.resolve(join(global.config, "AGENTS.md")), ...discovered]) + const files = yield* Effect.forEach( + paths, + (path) => + fs + .readFileStringSafe(path) + .pipe( + Effect.map((content) => + content === undefined ? undefined : new File({ path: AbsolutePath.make(path), content }), + ), + ), + { concurrency: "unbounded" }, + ) + if (files.some((file, index) => file === undefined && discovered.has(paths[index]))) + return SystemContext.unavailable + return files.filter((file): file is File => file !== undefined) + }) + + yield* registry.register({ + key, + load: observe().pipe( + Effect.map((files) => + files === SystemContext.unavailable + ? source(files) + : files.length === 0 + ? SystemContext.empty + : source(files), + ), + Effect.catch(() => Effect.succeed(source(SystemContext.unavailable))), + Effect.catchDefect(() => Effect.succeed(source(SystemContext.unavailable))), + ), + }) + }), +) + +export const node = makeLocationNode({ + name: "instruction-context", + layer, + deps: [FSUtil.node, Global.node, Location.node, SystemContextRegistry.node], +}) + +function render(files: ReadonlyArray) { + return files.map((file) => `Instructions from: ${file.path}\n${file.content}`).join("\n\n") +} diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts new file mode 100644 index 00000000000..46a08b6d910 --- /dev/null +++ b/packages/core/src/integration.ts @@ -0,0 +1,570 @@ +export * as Integration from "./integration" + +import { makeLocationNode } from "./effect/app-node" +import { + Cause, + Clock, + Context, + Duration, + Effect, + Exit, + Layer, + Schedule, + Schema, + Scope, + SynchronizedRef, + Types, +} from "effect" +import { Integration } from "@opencode-ai/schema/integration" +import { Credential } from "./credential" +import { State } from "./state" +import { EventV2 } from "./event" +import { IntegrationConnection } from "./integration/connection" + +export const ID = Integration.ID +export type ID = Integration.ID + +export const MethodID = Integration.MethodID +export type MethodID = Integration.MethodID + +export const AttemptID = Integration.AttemptID +export type AttemptID = typeof AttemptID.Type + +export const When = Integration.When +export type When = Integration.When + +export const TextPrompt = Integration.TextPrompt +export type TextPrompt = Integration.TextPrompt + +export const SelectPrompt = Integration.SelectPrompt +export type SelectPrompt = Integration.SelectPrompt + +export const Prompt = Integration.Prompt +export type Prompt = Integration.Prompt + +export const OAuthMethod = Integration.OAuthMethod +export type OAuthMethod = Integration.OAuthMethod + +export const KeyMethod = Integration.KeyMethod +export type KeyMethod = Integration.KeyMethod + +export const EnvMethod = Integration.EnvMethod +export type EnvMethod = Integration.EnvMethod + +export const Method = Integration.Method +export type Method = Integration.Method + +export const Info = Integration.Info +export type Info = Integration.Info + +export const Inputs = Integration.Inputs +export type Inputs = Integration.Inputs + +export type OAuthAuthorization = { + readonly url: string + readonly instructions: string +} & ( + | { + readonly mode: "auto" + readonly callback: Effect.Effect + } + | { + readonly mode: "code" + readonly callback: (code: string) => Effect.Effect + } +) + +export interface OAuthImplementation { + readonly integrationID: ID + readonly method: OAuthMethod + readonly authorize: (inputs: Inputs) => Effect.Effect + readonly refresh?: (credential: Credential.OAuth) => Effect.Effect + readonly label?: (credential: Credential.OAuth) => string | undefined +} + +export interface KeyImplementation { + readonly integrationID: ID + readonly method: KeyMethod +} + +export interface EnvImplementation { + readonly integrationID: ID + readonly method: EnvMethod +} + +export type Implementation = OAuthImplementation | KeyImplementation | EnvImplementation + +export const Attempt = Integration.Attempt +export type Attempt = Integration.Attempt + +export const AttemptStatus = Integration.AttemptStatus +export type AttemptStatus = typeof AttemptStatus.Type + +export class CodeRequiredError extends Schema.TaggedErrorClass()("Integration.CodeRequired", { + attemptID: AttemptID, +}) {} + +export class AuthorizationError extends Schema.TaggedErrorClass()("Integration.Authorization", { + cause: Schema.Defect(), +}) {} + +export type Error = CodeRequiredError | AuthorizationError + +export const Event = Integration.Event + +export const Ref = Integration.Ref +export type Ref = Integration.Ref + +type Entry = { + ref: Types.DeepMutable + methods: Types.DeepMutable[] + implementations: Map> +} + +type Data = { + integrations: Map +} + +export type Draft = { + list: () => readonly Ref[] + get: (id: ID) => Ref | undefined + update: (id: ID, update: (integration: Types.DeepMutable) => void) => void + remove: (id: ID) => void + method: { + list: (integrationID: ID) => readonly Method[] + update: (implementation: Implementation) => void + remove: (integrationID: ID, method: Method) => void + } +} + +export interface Interface extends State.Transformable { + /** Registers a scoped transform over the integration registry. */ + /** Returns one integration with its methods and current connections. */ + readonly get: (id: ID) => Effect.Effect + /** Returns all integrations with their methods and current connections. */ + readonly list: () => Effect.Effect + readonly connection: { + /** Returns the active connection for one integration. */ + readonly active: (id: ID) => Effect.Effect + /** Resolves a connection into usable credential material. */ + readonly resolve: ( + connection: IntegrationConnection.Info, + ) => Effect.Effect + /** Runs a key method and stores the resulting credential. */ + readonly key: (input: { + /** Integration receiving the credential. */ + readonly integrationID: ID + /** Secret entered by the user. */ + readonly key: string + /** User-facing label for the stored credential. */ + readonly label?: string + }) => Effect.Effect + /** Starts a stateful OAuth attempt. */ + readonly oauth: (input: { + /** Integration being authenticated. */ + readonly integrationID: ID + /** OAuth method selected by the caller. */ + readonly methodID: MethodID + /** Answers to the method's optional prompts. */ + readonly inputs: Inputs + /** User-facing label for the credential created on completion. */ + readonly label?: string + }) => Effect.Effect + /** Updates a stored credential exposed as a connection. */ + readonly update: ( + credentialID: Credential.ID, + updates: Partial>, + ) => Effect.Effect + /** Removes a stored credential connection. */ + readonly remove: (credentialID: Credential.ID) => Effect.Effect + } + readonly attempt: { + /** Returns the current state of an OAuth attempt. */ + readonly status: (attemptID: AttemptID) => Effect.Effect + /** Completes the attempt and stores its credential. */ + readonly complete: (input: { + /** Opaque handle returned by `oauth`. */ + readonly attemptID: AttemptID + /** Authorization code required by attempts in code mode. */ + readonly code?: string + }) => Effect.Effect + /** Cancels an attempt and releases its resources. */ + readonly cancel: (attemptID: AttemptID) => Effect.Effect + } +} + +export class Service extends Context.Service()("@opencode/v2/Integration") {} + +const attemptLifetime = Duration.toMillis(Duration.minutes(10)) +const terminalRetention = Duration.toMillis(Duration.minutes(1)) +const scrubInterval = Duration.seconds(30) +const settlementTimeout = Duration.seconds(30) // kilocode_change - bound retained OAuth attempt secrets + +type AttemptTime = { created: number; expires: number } +type PendingAttempt = { + status: "pending" + completing: boolean + settling: boolean // kilocode_change - cancellation and expiry cannot overtake credential persistence + authorization: OAuthAuthorization + integrationID: ID + methodID: MethodID + label?: string + scope: Scope.Closeable + time: AttemptTime +} +type TerminalAttempt = { + status: "complete" | "failed" | "expired" + message?: string + removeAt: number + time: AttemptTime +} +type AttemptEntry = PendingAttempt | TerminalAttempt + +export const locationLayer = Layer.effect( + Service, + Effect.gen(function* () { + const credentials = yield* Credential.Service + const events = yield* EventV2.Service + const scope = yield* Scope.Scope + const attempts = SynchronizedRef.makeUnsafe(new Map()) + const state = State.create({ + initial: () => ({ integrations: new Map() }), + draft: (draft) => ({ + list: () => Array.from(draft.integrations.values(), (entry) => entry.ref) as Ref[], + get: (id) => draft.integrations.get(id)?.ref as Ref | undefined, + update: (id, update) => { + const current = draft.integrations.get(id) ?? { + ref: { id, name: id }, + methods: [], + implementations: new Map(), + } + if (!draft.integrations.has(id)) draft.integrations.set(id, current) + update(current.ref) + current.ref.id = id + }, + remove: (id) => draft.integrations.delete(id), + method: { + list: (integrationID) => (draft.integrations.get(integrationID)?.methods as Method[] | undefined) ?? [], + update: (implementation) => { + const current = draft.integrations.get(implementation.integrationID) ?? { + ref: { + id: implementation.integrationID, + name: implementation.integrationID, + }, + methods: [], + implementations: new Map>(), + } + if (!draft.integrations.has(implementation.integrationID)) { + draft.integrations.set(implementation.integrationID, current) + } + const index = current.methods.findIndex((method) => { + if (method.type !== implementation.method.type) return false + if (method.type !== "oauth" || implementation.method.type !== "oauth") return true + return method.id === implementation.method.id + }) + if (index === -1) current.methods.push(implementation.method as Types.DeepMutable) + else current.methods[index] = implementation.method as Types.DeepMutable + if (implementation.method.type === "oauth") { + current.implementations.set( + implementation.method.id, + implementation as Types.DeepMutable, + ) + } + }, + remove: (integrationID, method) => { + const current = draft.integrations.get(integrationID) + if (!current) return + const index = current.methods.findIndex((candidate) => { + if (candidate.type !== method.type) return false + if (candidate.type !== "oauth" || method.type !== "oauth") return true + return candidate.id === method.id + }) + if (index !== -1) current.methods.splice(index, 1) + if (method.type === "oauth") current.implementations.delete(method.id) + }, + }, + }), + finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid), + }) + + const resolveConnections = (entry: Entry | undefined, saved: readonly Credential.Info[]) => { + const credentials = saved + .map((credential) => ({ + type: "credential" as const, + id: credential.id, + label: credential.label, + })) + .toReversed() + const env = (entry?.methods ?? []) + .filter((method) => method.type === "env") + .flatMap((method) => method.names.filter((name) => process.env[name])) + .map((name) => ({ type: "env" as const, name })) + return [...credentials, ...env] + } + + const project = (entry: Entry, connections: IntegrationConnection.Info[]) => + new Info({ + id: entry.ref.id, + name: entry.ref.name, + methods: entry.methods, + connections, + }) + + const authorize = (effect: Effect.Effect) => + effect.pipe(Effect.mapError((cause) => new AuthorizationError({ cause }))) + + const close = (attemptScope: Scope.Closeable) => + Scope.close(attemptScope, Exit.void).pipe(Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid) + + const message = (cause: Cause.Cause) => { + const error = Cause.squash(cause) + return error instanceof Error ? error.message : String(error) + } + + // kilocode_change start - persist before exposing completion and make settlement atomic with cancellation + const settle = Effect.fnUntraced(function* ( + attemptID: AttemptID, + exit: Exit.Exit, + owned = false, + ) { + return yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const pending = yield* SynchronizedRef.modify(attempts, (current) => { + const attempt = current.get(attemptID) + if (!attempt || attempt.status !== "pending") return [undefined, current] + if (owned) return attempt.settling ? [attempt, current] : [undefined, current] + if (attempt.settling) return [undefined, current] + return [attempt, new Map(current).set(attemptID, { ...attempt, settling: true })] + }) + if (!pending) return + const settled = Exit.isSuccess(exit) + ? yield* restore( + credentials + .create({ + integrationID: pending.integrationID, + label: pending.label, + value: + exit.value.type === "oauth" + ? { ...exit.value, methodID: pending.methodID } // kilocode_change - OAuth is a struct, not a class + : exit.value, + }) + .pipe( + Effect.timeout(settlementTimeout), + Effect.mapError((cause) => new AuthorizationError({ cause })), + ), + ).pipe(Effect.asVoid, Effect.exit) + : Exit.failCause(exit.cause) + const now = yield* Clock.currentTimeMillis + const result = yield* SynchronizedRef.modify(attempts, (current) => { + const attempt = current.get(attemptID) + if (!attempt || attempt.status !== "pending") return [undefined, current] + const terminal: TerminalAttempt = Exit.isSuccess(settled) + ? { status: "complete", time: attempt.time, removeAt: now + terminalRetention } + : { + status: "failed", + message: message(settled.cause), + time: attempt.time, + removeAt: now + terminalRetention, + } + return [attempt, new Map(current).set(attemptID, terminal)] + }) + if (!result) return settled + if (Exit.isSuccess(settled)) { + yield* events.publish(Event.ConnectionUpdated, { integrationID: result.integrationID }) + yield* events.publish(Event.Updated, {}) + } + yield* close(result.scope) + return settled + }), + ) + }) + // kilocode_change end + + const scrub = Effect.fnUntraced(function* () { + const now = yield* Clock.currentTimeMillis + const expired = yield* SynchronizedRef.modify(attempts, (current) => { + const next = new Map(current) + const scopes: Scope.Closeable[] = [] + for (const [id, attempt] of current) { + if (attempt.status === "pending" && !attempt.settling && attempt.time.expires <= now) { // kilocode_change + scopes.push(attempt.scope) + next.set(id, { status: "expired", time: attempt.time, removeAt: now + terminalRetention }) + continue + } + if (attempt.status !== "pending" && attempt.removeAt <= now) next.delete(id) + } + return [scopes, next] + }) + yield* Effect.forEach(expired, close, { discard: true }) + }) + + yield* scrub().pipe(Effect.repeat(Schedule.spaced(scrubInterval)), Effect.forkIn(scope)) + + return Service.of({ + transform: state.transform, + reload: state.reload, + get: Effect.fn("Integration.get")(function* (id) { + const entry = state.get().integrations.get(id) + if (!entry) return undefined + return project(entry, resolveConnections(entry, yield* credentials.list(id))) + }), + list: Effect.fn("Integration.list")(function* () { + const saved = Map.groupBy(yield* credentials.all(), (credential) => credential.integrationID) + return Array.from(state.get().integrations.values(), (entry) => + project(entry, resolveConnections(entry, saved.get(entry.ref.id) ?? [])), + ).toSorted((a, b) => a.name.localeCompare(b.name)) + }), + connection: { + active: Effect.fn("Integration.connection.active")(function* (id) { + const entry = state.get().integrations.get(id) + return resolveConnections(entry, yield* credentials.list(id))[0] + }), + resolve: Effect.fn("Integration.connection.resolve")(function* (connection) { + if (connection.type === "env") { + const key = process.env[connection.name] + return key ? Credential.Key.make({ type: "key", key }) : undefined + } + const credential = yield* credentials.get(connection.id) + if (!credential) return undefined + if (credential.value.type === "key") return credential.value + const implementation = state + .get() + .integrations.get(credential.integrationID) + ?.implementations.get(credential.value.methodID) + if (!implementation?.refresh) return credential.value + const now = yield* Clock.currentTimeMillis + if (credential.value.expires > now + Duration.toMillis(Duration.minutes(5))) return credential.value + const value = yield* authorize(implementation.refresh(credential.value)) + yield* credentials.update(credential.id, { value }) + return value + }), + key: Effect.fn("Integration.connection.key")(function* (input) { + const method = state + .get() + .integrations.get(input.integrationID) + ?.methods.some((method) => method.type === "key") + if (!method) return yield* Effect.die(`Key method not found: ${input.integrationID}`) + yield* credentials.create({ + integrationID: input.integrationID, + label: input.label, + value: Credential.Key.make({ type: "key", key: input.key }), + }) + yield* events.publish(Event.ConnectionUpdated, { integrationID: input.integrationID }) + yield* events.publish(Event.Updated, {}) + }), + oauth: Effect.fn("Integration.connection.oauth")(function* (input) { + const method = state.get().integrations.get(input.integrationID)?.implementations.get(input.methodID) + if (!method) { + return yield* Effect.die(`OAuth method not found: ${input.integrationID}/${input.methodID}`) + } + const attemptScope = yield* Scope.fork(scope) + const authorization = yield* authorize(method.authorize(input.inputs)).pipe( + Scope.provide(attemptScope), + Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(attemptScope, exit) : Effect.void)), + ) + const id = AttemptID.create() + const created = yield* Clock.currentTimeMillis + const time = { created, expires: created + attemptLifetime } + yield* SynchronizedRef.update(attempts, (current) => + new Map(current).set(id, { + status: "pending", + completing: authorization.mode === "auto", + settling: false, // kilocode_change + authorization, + integrationID: input.integrationID, + methodID: input.methodID, + label: input.label, + scope: attemptScope, + time, + }), + ) + if (authorization.mode === "auto") { + // kilocode_change start - settle persistence atomically with cancellation + yield* authorize(authorization.callback).pipe( + Effect.exit, + Effect.flatMap((exit) => settle(id, exit)), + Effect.forkIn(attemptScope, { startImmediately: true }), + ) + // kilocode_change end + } + return new Attempt({ + attemptID: id, + url: authorization.url, + instructions: authorization.instructions, + mode: authorization.mode, + time, + }) + }), + update: Effect.fn("Integration.connection.update")(function* (credentialID, updates) { + const credential = yield* credentials.get(credentialID) + yield* credentials.update(credentialID, updates) + if (credential) { + yield* events.publish(Event.ConnectionUpdated, { integrationID: credential.integrationID }) + } + yield* events.publish(Event.Updated, {}) + }), + remove: Effect.fn("Integration.connection.remove")(function* (credentialID) { + const credential = yield* credentials.get(credentialID) + yield* credentials.remove(credentialID) + if (credential) { + yield* events.publish(Event.ConnectionUpdated, { integrationID: credential.integrationID }) + } + yield* events.publish(Event.Updated, {}) + }), + }, + attempt: { + status: Effect.fn("Integration.attempt.status")(function* (attemptID) { + const attempt = (yield* SynchronizedRef.get(attempts)).get(attemptID) + if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${attemptID}`) + if (attempt.status === "failed") { + return { status: attempt.status, message: attempt.message ?? "Authorization failed", time: attempt.time } + } + return { status: attempt.status, time: attempt.time } + }), + complete: Effect.fn("Integration.attempt.complete")(function* (input) { + const attempt = yield* SynchronizedRef.modify(attempts, (current) => { + const match = current.get(input.attemptID) + if (!match || match.status !== "pending" || match.completing) return [match, current] + if (match.authorization.mode === "code" && input.code === undefined) return [match, current] + return [match, new Map(current).set(input.attemptID, { ...match, completing: true, settling: true })] // kilocode_change + }) + if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${input.attemptID}`) + if (attempt.status !== "pending") return + if (attempt.authorization.mode === "code" && input.code === undefined) { + return yield* new CodeRequiredError({ attemptID: input.attemptID }) + } + if (attempt.completing) return yield* Effect.die(`OAuth attempt already completing: ${input.attemptID}`) + const callback = + attempt.authorization.mode === "auto" + ? attempt.authorization.callback + : attempt.authorization.callback(input.code as string) + // kilocode_change start - an interrupted or timed-out callback still settles and releases its attempt. + return yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const exit = yield* restore(authorize(callback)).pipe( + Effect.timeout(settlementTimeout), + Effect.mapError((cause) => new AuthorizationError({ cause })), + Effect.exit, + ) + const settled = yield* settle(input.attemptID, exit, true) + if (settled && Exit.isFailure(settled)) return yield* settled + }), + ) + // kilocode_change end + }), + cancel: Effect.fn("Integration.attempt.cancel")(function* (attemptID) { + const attempt = yield* SynchronizedRef.modify(attempts, (current) => { + const match = current.get(attemptID) + if (!match || match.status !== "pending" || match.settling) return [undefined, current] // kilocode_change + const next = new Map(current) + next.delete(attemptID) + return [match, next] + }) + if (attempt) yield* Scope.close(attempt.scope, Exit.void) + }), + }, + }) + }), +) + +export const node = makeLocationNode({ service: Service, layer: locationLayer, deps: [Credential.node, EventV2.node] }) diff --git a/packages/core/src/integration/connection.ts b/packages/core/src/integration/connection.ts new file mode 100644 index 00000000000..91ab7e9e404 --- /dev/null +++ b/packages/core/src/integration/connection.ts @@ -0,0 +1,12 @@ +export * as IntegrationConnection from "./connection" + +import { Connection } from "@opencode-ai/schema/connection" + +export const CredentialInfo = Connection.CredentialInfo +export type CredentialInfo = Connection.CredentialInfo + +export const EnvInfo = Connection.EnvInfo +export type EnvInfo = Connection.EnvInfo + +export const Info = Connection.Info +export type Info = Connection.Info diff --git a/packages/core/src/kilocode/cost/max-cost-nudge.ts b/packages/core/src/kilocode/cost/max-cost-nudge.ts new file mode 100644 index 00000000000..b276689b433 --- /dev/null +++ b/packages/core/src/kilocode/cost/max-cost-nudge.ts @@ -0,0 +1,151 @@ +/** + * Soft per-session max-cost nudge. + * + * Alert (not hard-stop) the moment a session's cumulative cost crosses a + * whole-dollar threshold. The alert is non-blocking: the session keeps running + * while it is shown. Continue dismisses it (won't nag again for that limit); + * Stop is the surface's cue to abort. + * + * Cost signal: `SessionTable.cost` is written via direct SQL during message-part + * projection, so `session.updated` does NOT fire on cost change. The reliable + * signal is the per-assistant-message `cost`, summed here into a session total. + */ + +export type MaxCostChoice = "continue" | "stop" + +// Minimal shape of a message needed to aggregate session cost. +export interface MaxCostMessage { + id: string + sessionID: string + role?: string + cost?: number +} + +export class MaxCostNudge { + readonly #msgs = new Map() + readonly #totals = new Map() + readonly #floors = new Map() + readonly #alerted = new Map>() // sid -> limit values shown this run + readonly #acked = new Map>() // sid -> limit values continued past + + #limit: number | undefined + + // `> 0` rounds up to whole dollars; everything else disables (undefined). + static normalizeLimit(value: number | undefined | null): number | undefined { + if (value == null || !Number.isFinite(value) || value <= 0) return undefined + return Math.ceil(value) + } + + // Format a cost as `$X.XX`, with 4 decimals below $1. + static formatCost(value: number): string { + return `$${value.toFixed(value < 1 ? 4 : 2)}` + } + + setLimit(value: number | undefined | null): void { + this.#limit = MaxCostNudge.normalizeLimit(value) + } + + get limit(): number | undefined { + return this.#limit + } + + // Rebuild a session's total from a full message snapshot (seed on load). + resetMessageCosts(sid: string, messages: MaxCostMessage[]): number { + this.#dropMessages(sid) + let total = 0 + for (const msg of messages) { + if (msg.sessionID !== sid || msg.role !== "assistant" || !Number.isFinite(msg.cost)) continue + const cost = msg.cost ?? 0 + this.#msgs.set(msg.id, { sid, cost }) + total += cost + } + this.#totals.set(sid, total) + return this.sessionCost(sid) + } + + // Floor the session total with a direct cost signal (e.g. session.cost). Monotonic. + setSessionCost(sid: string, value: number): number { + if (Number.isFinite(value)) this.#floors.set(sid, Math.max(this.#floors.get(sid) ?? 0, value)) + return this.sessionCost(sid) + } + + // Record an assistant message cost (message.updated). Returns the session total. + updateMessageCost(sid: string, id: string, role: string | undefined, value: number | undefined): number { + if (role === "assistant") { + const prev = this.#msgs.get(id) + if (Number.isFinite(value)) { + if (prev && prev.sid !== sid) { + this.#totals.set(prev.sid, Math.max(0, (this.#totals.get(prev.sid) ?? 0) - prev.cost)) + } + const before = prev?.sid === sid ? prev.cost : 0 + const cost = value! + this.#msgs.set(id, { sid, cost }) + this.#totals.set(sid, Math.max(0, (this.#totals.get(sid) ?? 0) - before + cost)) + } else if (prev) { + // value became non-finite — drop the stale contribution + this.#totals.set(prev.sid, Math.max(0, (this.#totals.get(prev.sid) ?? 0) - prev.cost)) + this.#msgs.delete(id) + } + } + return this.sessionCost(sid) + } + + // Drop a message's contribution (message.removed). + removeMessageCost(id: string): void { + const prev = this.#msgs.get(id) + if (!prev) return + this.#msgs.delete(id) + this.#totals.set(prev.sid, Math.max(0, (this.#totals.get(prev.sid) ?? 0) - prev.cost)) + } + + sessionCost(sid: string): number { + return Math.max(this.#totals.get(sid) ?? 0, this.#floors.get(sid) ?? 0) + } + + /** + * Decide whether to alert for `sid` now. Returns the limit + cost to show + * once per run, or undefined (below limit, already acknowledged, or already + * showing). Re-arm with {@link rearm} when the session runs again. + */ + check(sid: string): { limit: number; cost: number } | undefined { + const limit = this.#limit + if (limit === undefined) return undefined + const cost = this.sessionCost(sid) + if (cost < limit || this.#acked.get(sid)?.has(limit) || this.#alerted.get(sid)?.has(limit)) return undefined + this.#remember(this.#alerted, sid, limit) + return { limit, cost } + } + + // Apply the user's choice. Continue suppresses re-alerts for the current limit. + resolve(sid: string, choice: MaxCostChoice, limit = this.#limit): void { + if (choice === "continue" && limit !== undefined) this.#remember(this.#acked, sid, limit) + } + + // Re-arm alerts for a session that started running again. + rearm(sid: string): void { + this.#alerted.delete(sid) + } + + // Forget all state for a deleted session. + onSessionDeleted(sid: string): void { + this.#dropMessages(sid) + this.#totals.delete(sid) + this.#floors.delete(sid) + this.#alerted.delete(sid) + this.#acked.delete(sid) + } + + // Drop every message contribution belonging to a session. + #dropMessages(sid: string): void { + for (const [id, msg] of this.#msgs) { + if (msg.sid === sid) this.#msgs.delete(id) + } + } + + // Record a limit value seen for a session. + #remember(map: Map>, sid: string, limit: number): void { + const seen = map.get(sid) + if (seen) seen.add(limit) + else map.set(sid, new Set([limit])) + } +} diff --git a/packages/core/src/kilocode/credential-migration.ts b/packages/core/src/kilocode/credential-migration.ts new file mode 100644 index 00000000000..590643ceb67 --- /dev/null +++ b/packages/core/src/kilocode/credential-migration.ts @@ -0,0 +1,49 @@ +import { Option, Schema } from "effect" +import { NonNegativeInt } from "../schema" + +const OAuth = Schema.Struct({ + type: Schema.Literal("oauth"), + refresh: Schema.String, + access: Schema.String, + expires: NonNegativeInt, + accountId: Schema.optional(Schema.String), + enterpriseUrl: Schema.optional(Schema.String), +}) + +const Key = Schema.Struct({ + type: Schema.Literal("api"), + key: Schema.String, + metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), +}) + +const Account = Schema.Struct({ + id: Schema.String, + serviceID: Schema.String, + description: Schema.String, + credential: Schema.Union([OAuth, Key]), +}) + +const Store = Schema.Struct({ + version: Schema.Literal(2), + accounts: Schema.Record(Schema.String, Account), + active: Schema.Record(Schema.String, Schema.String), +}) + +export function parse(input: unknown) { + const decoded = Schema.decodeUnknownOption(Store)(input) + if (Option.isNone(decoded)) return [] + const first = new Set() + return Object.values(decoded.value.accounts).map((account) => { + const fallback = !first.has(account.serviceID) + first.add(account.serviceID) + return { + id: account.id, + connectorID: account.serviceID, + label: account.description, + credential: account.credential, + active: decoded.value.active[account.serviceID] + ? decoded.value.active[account.serviceID] === account.id + : fallback, + } + }) +} diff --git a/packages/core/src/kilocode/database-compat.ts b/packages/core/src/kilocode/database-compat.ts new file mode 100644 index 00000000000..733b2eece88 --- /dev/null +++ b/packages/core/src/kilocode/database-compat.ts @@ -0,0 +1,32 @@ +import { Effect } from "effect" +import type { Database } from "../database/database" + +type Db = Database.Interface["db"] + +export function ensure(db: Db) { + const load = db.all<{ name: string }>("PRAGMA table_info('session_context_epoch')") + const ready = (rows: { name: string }[]) => { + const names = new Set(rows.map((row) => row.name)) + return ["agent", "replacement_seq", "revision"].every((name) => names.has(name)) + } + return load.pipe( + Effect.flatMap((rows) => { + if (ready(rows)) return Effect.void + return db.transaction( + (tx) => + Effect.gen(function* () { + const current = yield* tx.all<{ name: string }>("PRAGMA table_info('session_context_epoch')") + const names = new Set(current.map((row) => row.name)) + + if (!names.has("agent")) + yield* tx.run("ALTER TABLE `session_context_epoch` ADD `agent` text DEFAULT 'build' NOT NULL") + if (!names.has("replacement_seq")) + yield* tx.run("ALTER TABLE `session_context_epoch` ADD `replacement_seq` integer") + if (!names.has("revision")) + yield* tx.run("ALTER TABLE `session_context_epoch` ADD `revision` integer DEFAULT 0 NOT NULL") + }), + { behavior: "immediate" }, + ) + }), + ) +} diff --git a/packages/core/src/kilocode/db-preflight.ts b/packages/core/src/kilocode/db-preflight.ts new file mode 100644 index 00000000000..4f211980804 --- /dev/null +++ b/packages/core/src/kilocode/db-preflight.ts @@ -0,0 +1,68 @@ +export * as DbPreflight from "./db-preflight" + +import { accessSync, chmodSync, constants, statSync } from "fs" +import path from "path" +import { Global } from "../global" +import { Log } from "../util/log" + +const log = Log.create({ service: "db-preflight" }) + +function writable(target: string) { + try { + accessSync(target, constants.W_OK) + return true + } catch { + return false + } +} + +function exists(target: string) { + try { + statSync(target) + return true + } catch { + return false + } +} + +// Startup runs `PRAGMA wal_checkpoint(PASSIVE)`, which must write the database and its +// WAL sidecars. A stray read-only file otherwise kills the process deep inside Effect +// with an opaque "attempt to write a readonly database". +export function assertWritable(filename: string, trusted: string = Global.Path.data) { + if (!filename || filename === ":memory:" || filename.startsWith("file:")) return + const dir = path.dirname(filename) + const owned = path.resolve(dir) === path.resolve(trusted) + let missing = false + for (const file of [filename, `${filename}-wal`, `${filename}-shm`]) { + if (!exists(file)) { + missing = true + continue + } + if (writable(file)) continue + let cause: unknown + if (owned) { + // chmod only succeeds for files the current user owns, which is exactly the safe repair scope + try { + chmodSync(file, statSync(file).mode | 0o600) + } catch (err) { + cause = err + } + if (writable(file)) { + // visible trail: if files keep losing their write bit, something outside kilo is doing it + log.warn("repaired read-only database file", { file }) + continue + } + } + throw new Error( + `Database file is not writable: ${file}. Fix its permissions (chmod u+w "${file}") or point KILO_DB at a writable location.`, + cause === undefined ? undefined : { cause }, + ) + } + if (missing && !writable(dir)) { + if (!exists(dir)) + throw new Error(`Database directory does not exist: ${dir}. Create it or point KILO_DB at an existing location.`) + throw new Error( + `Database directory is not writable: ${dir}. SQLite must create WAL files next to the database. Fix its permissions or point KILO_DB at a writable location.`, + ) + } +} diff --git a/packages/core/src/kilocode/event-storage.ts b/packages/core/src/kilocode/event-storage.ts new file mode 100644 index 00000000000..44b83af7214 --- /dev/null +++ b/packages/core/src/kilocode/event-storage.ts @@ -0,0 +1,30 @@ +// kilocode_change start - released readers persist tool content in the pre-1.17.13 shapes +// +// Kilo has shipped durable rows whose `content` uses `{type:"media"}` or `{type:"file",source}`. +// Before v1.17.13 the codec lived on the event schema, but upstream moved session events onto the +// wire (`SessionEvent.Durable` backs `/api/session/{id}/history`), so a schema-level transform now +// forks the generated OpenAPI surface into duplicate variants. Keyed on event type and applied only +// where rows enter and leave SQL, so the wire contract stays upstream's. +import { Schema } from "effect" +import { StoredToolContent } from "@opencode-ai/llm" + +const decodeContent = Schema.decodeUnknownSync(Schema.Array(StoredToolContent)) +const encodeContent = Schema.encodeUnknownSync(Schema.Array(StoredToolContent)) + +/** Durable event types carrying tool `content`. Unversioned, matching `Definition["type"]`. */ +const CONTENT_TYPES = new Set(["session.next.tool.progress", "session.next.tool.success"]) + +const mapContent = (type: string, data: unknown, convert: (content: readonly unknown[]) => unknown) => { + if (!CONTENT_TYPES.has(type)) return data + if (typeof data !== "object" || data === null) return data + const content = (data as { readonly content?: unknown }).content + if (!Array.isArray(content)) return data + return { ...(data as Record), content: convert(content) } +} + +/** Released or current persisted `data` -> the shape `definition.data` expects. */ +export const decode = (type: string, data: unknown) => mapContent(type, data, (content) => decodeContent(content)) + +/** Encoded `definition.data` -> the persisted shape released readers still parse. */ +export const encode = (type: string, data: unknown) => mapContent(type, data, (content) => encodeContent(content)) +// kilocode_change end diff --git a/packages/core/src/kilocode/exit-code.ts b/packages/core/src/kilocode/exit-code.ts new file mode 100644 index 00000000000..4a4408a7b4a --- /dev/null +++ b/packages/core/src/kilocode/exit-code.ts @@ -0,0 +1,16 @@ +import { constants } from "node:os" +import * as Effect from "effect/Effect" +import * as Predicate from "effect/Predicate" +import { ExitCode } from "effect/unstable/process/ChildProcessSpawner" + +// A process terminated by a signal produces a null exit code (see the "exit" +// event on node:child_process). Report the conventional 128 + signum code +// (e.g. 139 for SIGSEGV) instead of failing, which left consumers like the +// bash tool waiting on a numeric code that never arrived. +export const settle = ([code, signal]: readonly [code: number | null, signal: NodeJS.Signals | null]) => { + if (Predicate.isNotNull(code)) return Effect.succeed(ExitCode(code)) + if (Predicate.isNotNull(signal) && signal in constants.signals) { + return Effect.succeed(ExitCode(128 + constants.signals[signal])) + } + return Effect.succeed(ExitCode(1)) +} diff --git a/packages/core/src/kilocode/fff.ts b/packages/core/src/kilocode/fff.ts new file mode 100644 index 00000000000..f217a417605 --- /dev/null +++ b/packages/core/src/kilocode/fff.ts @@ -0,0 +1,9 @@ +import os from "os" +import path from "path" + +export function scanning(directory: string) { + return { + enableFsRootScanning: directory === path.parse(directory).root, + enableHomeDirScanning: directory === os.homedir(), + } +} diff --git a/packages/core/src/kilocode/global.ts b/packages/core/src/kilocode/global.ts index b57d06f7ae9..a98d815896d 100644 --- a/packages/core/src/kilocode/global.ts +++ b/packages/core/src/kilocode/global.ts @@ -1,4 +1,6 @@ import fs from "fs/promises" +import path from "path" +import { randomUUID } from "crypto" /** * Like `fs.mkdir({ recursive: true })` but also repairs broken symlinks and @@ -21,3 +23,50 @@ export async function ensureRealDir(p: string) { await fs.mkdir(p, { recursive: true }) } } + +async function writable(p: string) { + const probe = path.join(p, `.kilo-write-${process.pid}-${randomUUID()}`) + await fs.writeFile(probe, "", { flag: "wx", mode: 0o600 }) + await fs.unlink(probe) +} + +async function ready(p: string) { + await ensureRealDir(p) + await writable(p) +} + +export async function resolveState(p: string, fallback?: string) { + const sticky = + fallback === undefined + ? false + : await fs.stat(fallback).then( + (stat) => + stat.isDirectory() && + writable(fallback).then( + () => true, + () => false, + ), + () => false, + ) + if (sticky && fallback !== undefined) return fallback + + const err = await ready(p).then( + () => undefined, + (err: unknown) => err, + ) + if (err === undefined) return p + if (fallback === undefined) throw err + + const failed = await ready(fallback).then( + () => undefined, + (err: unknown) => err, + ) + if (failed !== undefined) { + throw new AggregateError([err, failed], `Cannot use state directory "${p}" or fallback "${fallback}"`) + } + + const msg = err instanceof Error ? err.message : "Unknown error" + // Logging is not initialized until Global.Path.log exists. + console.warn(`[kilo] Cannot use state directory "${p}"; using "${fallback}" instead: ${msg}`) + return fallback +} diff --git a/packages/core/src/kilocode/image-size.ts b/packages/core/src/kilocode/image-size.ts new file mode 100644 index 00000000000..84cbc21d410 --- /dev/null +++ b/packages/core/src/kilocode/image-size.ts @@ -0,0 +1,73 @@ +export const MAX_DIMENSION = 16_384 +export const MAX_PIXELS = 25_000_000 + +export function dimensions(input: Buffer) { + const png = input.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) + if (png) { + if (input.length < 24 || input.toString("ascii", 12, 16) !== "IHDR") throw new TypeError("invalid PNG") + return { width: input.readUInt32BE(16), height: input.readUInt32BE(20) } + } + + const gif = input.toString("ascii", 0, 6) + if (gif === "GIF87a" || gif === "GIF89a") { + if (input.length < 10) throw new TypeError("invalid GIF") + return { width: input.readUInt16LE(6), height: input.readUInt16LE(8) } + } + + if (input[0] === 0xff && input[1] === 0xd8) { + for (let offset = 2; offset < input.length; ) { + if (input[offset] !== 0xff) throw new TypeError("invalid JPEG") + while (input[offset] === 0xff) offset++ + const marker = input[offset++] + if (marker === undefined || marker === 0xd9 || marker === 0xda) break + if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd8)) continue + if (offset + 2 > input.length) throw new TypeError("invalid JPEG") + const length = input.readUInt16BE(offset) + if (length < 2 || offset + length > input.length) throw new TypeError("invalid JPEG") + const frame = + (marker >= 0xc0 && marker <= 0xc3) || + (marker >= 0xc5 && marker <= 0xc7) || + (marker >= 0xc9 && marker <= 0xcb) || + (marker >= 0xcd && marker <= 0xcf) + if (frame) { + if (length < 7) throw new TypeError("invalid JPEG") + return { width: input.readUInt16BE(offset + 5), height: input.readUInt16BE(offset + 3) } + } + offset += length + } + throw new TypeError("invalid JPEG") + } + + if (input.toString("ascii", 0, 4) === "RIFF" && input.toString("ascii", 8, 12) === "WEBP") { + const chunk = input.toString("ascii", 12, 16) + if (chunk === "VP8X" && input.length >= 30) { + return { width: input.readUIntLE(24, 3) + 1, height: input.readUIntLE(27, 3) + 1 } + } + if (chunk === "VP8L" && input.length >= 25 && input[20] === 0x2f) { + const b1 = input[21] ?? 0 + const b2 = input[22] ?? 0 + const b3 = input[23] ?? 0 + const b4 = input[24] ?? 0 + return { + width: 1 + b1 + ((b2 & 0x3f) << 8), + height: 1 + ((b2 & 0xc0) >> 6) + (b3 << 2) + ((b4 & 0x0f) << 10), + } + } + if (chunk === "VP8 " && input.length >= 30 && input[23] === 0x9d && input[24] === 0x01 && input[25] === 0x2a) { + return { width: input.readUInt16LE(26) & 0x3fff, height: input.readUInt16LE(28) & 0x3fff } + } + throw new TypeError("invalid WebP") + } + + throw new TypeError("unsupported image") +} + +export function allowed(size: { width: number; height: number }) { + return ( + size.width > 0 && + size.height > 0 && + size.width <= MAX_DIMENSION && + size.height <= MAX_DIMENSION && + size.width * size.height <= MAX_PIXELS + ) +} diff --git a/packages/core/src/kilocode/oauth/page.ts b/packages/core/src/kilocode/oauth/page.ts new file mode 100644 index 00000000000..c1566919301 --- /dev/null +++ b/packages/core/src/kilocode/oauth/page.ts @@ -0,0 +1,19 @@ +export * as KiloOauthCallbackPage from "./page" + +import { OauthCallbackPage, type CallbackPageOptions } from "../../oauth/page" + +const KILO_MARK = ` + + ` + +function brand(page: string) { + return page.replace(//, KILO_MARK).replaceAll("OpenCode", "Kilo") +} + +export function success(options?: CallbackPageOptions) { + return brand(OauthCallbackPage.success(options)) +} + +export function error(detail: string, options?: CallbackPageOptions) { + return brand(OauthCallbackPage.error(detail, options)) +} diff --git a/packages/core/src/kilocode/powershell.ts b/packages/core/src/kilocode/powershell.ts new file mode 100644 index 00000000000..866f215e043 --- /dev/null +++ b/packages/core/src/kilocode/powershell.ts @@ -0,0 +1,126 @@ +export function args(command: string) { + return ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script(command)] +} + +const setup = `[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false); +[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); +$OutputEncoding = [Console]::OutputEncoding; +` + +function script(command: string) { + const pos = prologue(command) + const head = command.slice(0, pos) + const body = command.slice(pos) + const gap = head && !/[;\r\n]\s*$/.test(head) ? "\n" : "" + return `${head}${gap}${setup}${body}` +} + +function prologue(command: string) { + const pos = scan(command, 0) + const attr = attrs(command, pos) + const body = command.slice(attr) + const match = /^param\s*\(/i.exec(body) + if (!match) return pos + + const start = attr + match[0].lastIndexOf("(") + const end = block(command, start, "(", ")") + if (end === undefined) return pos + return end +} + +function attrs(command: string, start: number) { + let pos = start + while (pos < command.length) { + const next = scan(command, pos) + if (command[next] !== "[") return next + const end = block(command, next, "[", "]") + if (end === undefined) return start + pos = end + } + return pos +} + +function scan(command: string, start: number) { + let pos = start + while (pos < command.length) { + const next = trivia(command, pos) + if (next !== pos) { + pos = next + continue + } + const end = line(command, pos) + const value = command.slice(pos, end) + if (/^using\s+(?:assembly|module|namespace|type)\b/i.test(value)) { + pos = end + continue + } + return pos + } + return pos +} + +function trivia(command: string, start: number) { + let pos = start + while (pos < command.length) { + while (/\s/.test(command[pos] ?? "")) pos++ + if (command[pos] === "#") { + pos = line(command, pos) + continue + } + if (command.startsWith("<#", pos)) { + const end = command.indexOf("#>", pos + 2) + if (end === -1) return command.length + pos = end + 2 + continue + } + return pos + } + return pos +} + +function line(command: string, start: number) { + const index = command.indexOf("\n", start) + if (index === -1) return command.length + return index + 1 +} + +function block(command: string, start: number, open: string, close: string) { + let depth = 0 + let quote: string | undefined + for (let pos = start; pos < command.length; pos++) { + const char = command[pos] + if (quote) { + if (quote === "'" && char === "'" && command[pos + 1] === "'") { + pos++ + continue + } + if (quote === '"' && char === "`") { + pos++ + continue + } + if (char === quote) quote = undefined + continue + } + if (char === "'" || char === '"') { + quote = char + continue + } + if (command.startsWith("<#", pos)) { + const end = command.indexOf("#>", pos + 2) + if (end === -1) return + pos = end + 1 + continue + } + if (char === "#") { + pos = line(command, pos) - 1 + continue + } + if (char === open) depth++ + if (char === close) { + depth-- + if (depth === 0) return pos + 1 + } + } +} + +export const PowerShell = { args } diff --git a/packages/core/src/kilocode/provider-usage.ts b/packages/core/src/kilocode/provider-usage.ts new file mode 100644 index 00000000000..a27f4a3bd68 --- /dev/null +++ b/packages/core/src/kilocode/provider-usage.ts @@ -0,0 +1,395 @@ +export * as ProviderUsage from "./provider-usage" + +import { Context, Effect, Layer, Schema } from "effect" +import { createHash } from "node:crypto" +import { ProviderUsage as Contract } from "@opencode-ai/schema/kilocode/provider-usage" +import { Catalog } from "../catalog" +import { makeGlobalNode, makeLocationNode } from "../effect/app-node" +import { Integration } from "../integration" +import { PluginV2 } from "../plugin" +import { ProviderV2 } from "../provider" +import * as Cloud from "./provider-usage/cloud" +import { bindings, direct, type Candidate } from "./provider-usage/minimax/usage" + +const successTtl = 60_000 +const errorTtl = 10_000 +const readyPlugin = PluginV2.ID.make("config-provider") + +interface AdapterContext { + candidates: readonly Candidate[] + failedCandidates: readonly Candidate["providerID"][] + cloud: (() => Promise) | undefined + token: string | undefined + cloudIdentity: string | undefined + cloudReliable: boolean + fetch: typeof fetch + usage: typeof Cloud.fetchCodingPlanUsage + identityCurrent(identity: string): boolean + source(id: string, load: () => Promise, identity?: string): Promise + preserve(prefix: string, identity?: string): Contract.UsageSnapshot[] + prune(prefix: string, keep: string[]): void +} + +interface AdapterResult { + items: ReadonlyArray +} + +interface Adapter { + cachePrefixes: readonly string[] + cloudScoped?: boolean + run(ctx: AdapterContext): Promise +} + +const managed: Adapter = { + cachePrefixes: ["kilo-managed:"], + cloudScoped: true, + async run(ctx) { + if (!ctx.cloud || !ctx.token || !ctx.cloudIdentity) { + return { items: ctx.cloudReliable ? [] : ctx.preserve("kilo-managed:") } + } + const state = await ctx.cloud() + if (!ctx.identityCurrent(ctx.cloudIdentity)) return { items: [] } + if (!state.plans.ok || !state.byok.ok) return { items: ctx.preserve("kilo-managed:", ctx.cloudIdentity) } + const token = ctx.token + const identity = ctx.cloudIdentity + const detected = Cloud.plans(state) + const ids = detected.map((subscription) => `kilo-managed:${subscription.id}`) + ctx.prune("kilo-managed:", ids) + return { + items: await Promise.all( + detected.map((subscription) => + ctx.source(`kilo-managed:${subscription.id}`, () => Cloud.managed(token, subscription, ctx.usage), identity), + ), + ), + } + }, +} + +const minimax: Adapter = { + cachePrefixes: ["minimax-direct-"], + async run(ctx) { + const items = await direct(ctx.candidates, ctx.fetch, ctx.source) + // A candidate is either live or failed, never both, so the id sets are disjoint. + const stale = ctx.failedCandidates.flatMap((id) => ctx.preserve(`minimax-direct-${bindings[id].region}`)) + const merged = [...items, ...stale] + ctx.prune( + "minimax-direct-", + merged.map((item) => item.id), + ) + return { items: merged } + }, +} + +const registry: readonly Adapter[] = [managed, minimax] + +export class ServiceError extends Schema.TaggedErrorClass()("ProviderUsageServiceError", { + message: Schema.String, +}) {} + +interface SourceCell { + identity?: string + value?: Contract.UsageSnapshot + expires: number + updatedAt?: string + inflight?: Promise +} + +interface CloudCell { + value?: Cloud.CloudState + expires: number + updatedAt?: string + inflight?: Promise +} + +interface State { + sources: Map + cloud: CloudCell + cloudIdentity?: string +} + +function fingerprint(value: string) { + return createHash("sha256").update(value).digest("hex") +} + +function scopeCloudCache(state: State, token: string | undefined) { + const identity = token ? fingerprint(token) : undefined + if (state.cloudIdentity === identity) return identity + state.cloudIdentity = identity + state.cloud = { expires: 0 } + prune(state, "kilo-managed:", []) + return identity +} + +function stale(next: Contract.UsageSnapshot, previous: Contract.UsageSnapshot | undefined) { + if (next.fetchState !== "unavailable" && next.fetchState !== "error") return next + if (!previous || (previous.fetchState !== "ready" && previous.fetchState !== "stale")) return next + return { + ...previous, + fetchState: "stale" as const, + planState: next.planState, + routingState: next.routingState, + managementUrl: next.managementUrl, + error: next.error, + } +} + +function source( + state: State, + id: string, + force: boolean, + load: () => Promise, + identity?: string, +) { + const existing = state.sources.get(id) + const cell: SourceCell = existing && existing.identity === identity ? existing : { expires: 0, identity } + state.sources.set(id, cell) + if (!force && cell.value && cell.expires > Date.now()) return Promise.resolve(cell.value) + if (cell.inflight) return cell.inflight + + const task = load() + .then((item) => { + const value = stale(item, cell.value) + if (state.sources.get(id) !== cell) return value + cell.value = value + cell.updatedAt = new Date().toISOString() + cell.expires = Date.now() + (value.fetchState === "ready" ? successTtl : errorTtl) + return value + }) + .finally(() => { + cell.inflight = undefined + }) + cell.inflight = task + return task +} + +function preserve(state: State, prefix: string, identity?: string) { + const items: Contract.UsageSnapshot[] = [] + for (const [id, cell] of state.sources) { + if (!id.startsWith(prefix) || (identity !== undefined && cell.identity !== identity) || !cell.value) continue + const loaded = cell.value.fetchState === "ready" || cell.value.fetchState === "stale" + const value = loaded + ? { + ...cell.value, + fetchState: "stale" as const, + error: { + code: "source_refresh_unavailable", + message: "The latest usage could not be loaded.", + retryable: true, + }, + } + : cell.value + cell.value = value + cell.updatedAt = new Date().toISOString() + cell.expires = Date.now() + errorTtl + items.push(value) + } + return items +} + +function prune(state: State, prefix: string, keep: string[]) { + const ids = new Set(keep) + for (const id of state.sources.keys()) { + if (!id.startsWith(prefix) || ids.has(id)) continue + state.sources.delete(id) + } +} + +function cloud(state: State, token: string, identity: string, force: boolean, transport: TransportInterface) { + if (state.cloudIdentity !== identity) return Cloud.load(token, transport) + const cell = state.cloud + if (!force && cell.value && cell.expires > Date.now()) return Promise.resolve(cell.value) + if (cell.inflight) return cell.inflight + + const task = Cloud.load(token, transport) + .then((value) => { + if (state.cloudIdentity !== identity) return value + const failed = Object.values(value).some((result) => !result.ok) + const previous = cell.value + if (failed && previous) { + cell.expires = Date.now() + errorTtl + return previous + } + cell.value = value + cell.updatedAt = new Date().toISOString() + cell.expires = Date.now() + (failed ? errorTtl : successTtl) + return value + }) + .finally(() => { + cell.inflight = undefined + }) + cell.inflight = task + return task +} + +export interface Interface { + readonly get: () => Effect.Effect + readonly refresh: () => Effect.Effect +} + +export class Service extends Context.Service()("@kilocode/ProviderUsage") {} + +export interface TransportInterface { + readonly fetch: typeof fetch + readonly plans: typeof Cloud.fetchCodingPlanSubscriptions + readonly byok: typeof Cloud.fetchByokEntries + readonly usage: typeof Cloud.fetchCodingPlanUsage +} + +export class Transport extends Context.Service()("@kilocode/ProviderUsageTransport") {} + +const transportLayer = Layer.succeed(Transport, { + fetch, + plans: Cloud.fetchCodingPlanSubscriptions, + byok: Cloud.fetchByokEntries, + usage: Cloud.fetchCodingPlanUsage, +}) + +export const transportNode = makeGlobalNode({ service: Transport, layer: transportLayer, deps: [] }) + +const credential = Effect.fn("ProviderUsage.credential")(function* ( + integrations: Integration.Interface, + id: Integration.ID, +) { + const connection = yield* integrations.connection.active(id) + if (!connection) return undefined + return yield* integrations.connection + .resolve(connection) + .pipe(Effect.mapError(() => new ServiceError({ message: `Unable to resolve provider credential: ${id}` }))) +}) + +const resolve = Effect.fn("ProviderUsage.resolveCredential")(function* ( + integrations: Integration.Interface, + id: Integration.ID, +) { + return yield* credential(integrations, id).pipe( + Effect.map((value) => ({ ok: true as const, value })), + Effect.catch(() => Effect.succeed({ ok: false as const })), + ) +}) + +function configuredKey(provider: ProviderV2.Info) { + const header = Object.entries(provider.request.headers).find(([key]) => key.toLowerCase() === "x-api-key")?.[1] + const value = provider.request.body.apiKey ?? provider.api.settings?.apiKey ?? header + return typeof value === "string" ? value : undefined +} + +function nonempty(value: unknown) { + if (typeof value !== "string") return undefined + const text = value.trim() + return text || undefined +} + +const inputs = Effect.fn("ProviderUsage.inputs")(function* ( + catalog: Catalog.Interface, + integrations: Integration.Interface, +) { + const providers = yield* catalog.provider.all() + const byID = new Map(providers.map((provider) => [provider.id, provider])) + const failedCandidates: Candidate["providerID"][] = [] + const candidates = yield* Effect.forEach(Object.keys(bindings) as (keyof typeof bindings)[], (providerID) => + Effect.gen(function* () { + const provider = byID.get(ProviderV2.ID.make(providerID)) + if (!provider || provider.disabled) return undefined + const resolved = yield* resolve(integrations, provider.integrationID ?? Integration.ID.make(provider.id)) + if (!resolved.ok) failedCandidates.push(providerID) + const value = resolved.ok + ? resolved.value?.type === "key" + ? resolved.value.key + : configuredKey(provider) + : undefined + if (typeof value !== "string" || !value.trim().startsWith("sk-cp")) return undefined + return { providerID, label: provider.name, key: value.trim() } satisfies Candidate + }), + ) + const kilo = yield* resolve(integrations, Integration.ID.make("kilo")) + const kiloProvider = byID.get(ProviderV2.ID.kilo) + const configuredOrg = nonempty(process.env.KILO_ORG_ID) ?? nonempty(kiloProvider?.request.body.kilocodeOrganizationId) + const organization = + configuredOrg !== undefined || + (kilo.ok && kilo.value?.type === "oauth" && nonempty(kilo.value.metadata?.accountID) !== undefined) + const cloudReliable = organization || kilo.ok + const token = + kilo.ok && kilo.value?.type === "oauth" && !organization && kilo.value.access ? kilo.value.access : undefined + return { + candidates: candidates.filter((item): item is Candidate => item !== undefined), + failedCandidates, + token, + cloudReliable, + } +}) + +function makeService( + catalog: Catalog.Interface, + integrations: Integration.Interface, + transport: TransportInterface, + ready: Effect.Effect, +) { + const state: State = { sources: new Map(), cloud: { expires: 0 } } + + const evaluate = Effect.fn("ProviderUsage.evaluate")(function* (force: boolean) { + yield* ready + const current = yield* inputs(catalog, integrations) + const cloudIdentity = current.cloudReliable ? scopeCloudCache(state, current.token) : state.cloudIdentity + const ctx: AdapterContext = { + candidates: current.candidates, + failedCandidates: current.failedCandidates, + cloud: + current.token && cloudIdentity + ? () => cloud(state, current.token!, cloudIdentity, force, transport) + : undefined, + token: current.token, + cloudIdentity, + cloudReliable: current.cloudReliable, + fetch: transport.fetch, + usage: transport.usage, + identityCurrent: (identity) => state.cloudIdentity === identity, + source: (id, load, identity) => source(state, id, force, load, identity), + preserve: (prefix, identity) => preserve(state, prefix, identity), + prune: (prefix, keep) => prune(state, prefix, keep), + } + const results = yield* Effect.promise(() => + Promise.all( + registry.map((adapter) => + // Adapters are expected to be total (they absorb their own failures into + // unavailable/stale snapshots). This catch is the containment boundary so a + // faulty future adapter degrades to stale output instead of failing the endpoint. + adapter.run(ctx).catch( + (): AdapterResult => ({ + items: adapter.cachePrefixes.flatMap((prefix) => + ctx.preserve(prefix, adapter.cloudScoped ? ctx.cloudIdentity : undefined), + ), + }), + ), + ), + ), + ) + const stamps = [state.cloud.updatedAt, ...state.sources.values().map((cell) => cell.updatedAt)].filter( + (value): value is string => value !== undefined, + ) + return { + items: results.flatMap((result) => result.items), + generatedAt: stamps.toSorted().at(-1) ?? new Date().toISOString(), + } satisfies Contract.Info + }) + + return Service.of({ + get: () => evaluate(false), + refresh: () => evaluate(true), + }) +} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const plugins = yield* PluginV2.Service + return makeService(yield* Catalog.Service, yield* Integration.Service, yield* Transport, plugins.wait(readyPlugin)) + }), +) + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [Catalog.node, Integration.node, PluginV2.node, transportNode], +}) + +export { Contract as Schema } diff --git a/packages/core/src/kilocode/provider-usage/cloud.ts b/packages/core/src/kilocode/provider-usage/cloud.ts new file mode 100644 index 00000000000..74464ba45ec --- /dev/null +++ b/packages/core/src/kilocode/provider-usage/cloud.ts @@ -0,0 +1,138 @@ +import { + fetchByokEntries, + fetchCodingPlanSubscriptions, + fetchCodingPlanUsage, + type ByokEntry, + type CodingPlanQuotaWindow, + type CodingPlanSubscription, +} from "@kilocode/kilo-gateway" +import type { ProviderUsage } from "@opencode-ai/schema/kilocode/provider-usage" + +export { fetchByokEntries, fetchCodingPlanSubscriptions, fetchCodingPlanUsage } + +export interface CloudState { + plans: Result + byok: Result +} + +type Result = { ok: true; value: T } | { ok: false } + +const safe = async (promise: Promise): Promise> => + promise.then( + (value) => ({ ok: true, value }), + () => ({ ok: false }), + ) + +export async function load( + token: string, + transport: { + plans: typeof fetchCodingPlanSubscriptions + byok: typeof fetchByokEntries + } = { plans: fetchCodingPlanSubscriptions, byok: fetchByokEntries }, +): Promise { + const [plans, byok] = await Promise.all([safe(transport.plans(token)), safe(transport.byok(token))]) + return { plans, byok } +} + +function base() { + if (!process.env.KILO_API_URL) return "https://app.kilo.ai" + try { + return new URL(process.env.KILO_API_URL).origin + } catch { + return "https://app.kilo.ai" + } +} + +const error = (code: string, message: string) => ({ code, message, retryable: true }) + +function installed(subscription: CodingPlanSubscription, state: Result) { + if (!state.ok || !subscription.canQueryUsage || !subscription.hasInstalledByokKey) return false + return state.value.some( + (item) => + item.provider_id === subscription.providerId && item.management_source === "coding_plan" && item.is_enabled, + ) +} + +export function plans(state: CloudState) { + if (!state.plans.ok) return [] + return state.plans.value + .filter((item) => (item.status === "active" || item.status === "past_due") && installed(item, state.byok)) + .sort((a, b) => a.id.localeCompare(b.id)) +} + +function durationMs(period: CodingPlanQuotaWindow["period"]) { + const multipliers = { + hour: 60 * 60 * 1000, + day: 24 * 60 * 60 * 1000, + week: 7 * 24 * 60 * 60 * 1000, + } as const + if (period.unit === "month") return undefined + return period.value * multipliers[period.unit] +} + +function window(subscriptionId: string, value: CodingPlanQuotaWindow): ProviderUsage.UsageWindow { + const remaining = value.remainingPercent + const duration = durationMs(value.period) + return { + id: `${subscriptionId}:${value.id}`, + resource: "subscription", + unit: "percent", + orientation: "remaining_percent", + used: Math.max(0, 100 - remaining), + remaining, + limit: 100, + period: value.period, + ...(duration !== undefined ? { durationMs: duration } : {}), + resetAt: value.resetsAt, + state: remaining <= 0 ? "exhausted" : "active", + } +} + +export async function managed( + token: string, + subscription: CodingPlanSubscription, + usage: typeof fetchCodingPlanUsage = fetchCodingPlanUsage, +): Promise { + const fetchedAt = new Date().toISOString() + const planState = subscription.cancelAtPeriodEnd + ? "canceling" + : subscription.status === "past_due" + ? "past_due" + : "active" + const id = `kilo-managed:${subscription.id}` + const managementUrl = `${base()}/subscriptions/coding-plans/${subscription.id}` + + return usage(token, subscription.id) + .then((usage) => { + const windows = usage.subscription.windows.map((item) => window(usage.subscription.id, item)) + return { + id, + providerID: usage.subscription.providerId, + sourceKind: "kilo_managed", + providerLabel: usage.subscription.providerName, + planLabel: usage.subscription.planName, + sourceLabel: "via Kilo", + fetchState: "ready", + planState, + routingState: "active", + fetchedAt: usage.fetchedAt, + managementUrl, + windows, + } satisfies ProviderUsage.UsageSnapshot + }) + .catch(() => ({ + id, + providerID: subscription.providerId, + sourceKind: "kilo_managed", + providerLabel: subscription.providerName, + planLabel: subscription.planName, + sourceLabel: "via Kilo", + fetchState: "unavailable", + planState, + routingState: "active", + fetchedAt, + managementUrl, + windows: [], + error: error("managed_subscription_unavailable", "Usage unavailable."), + })) +} diff --git a/packages/core/src/kilocode/provider-usage/minimax/native.ts b/packages/core/src/kilocode/provider-usage/minimax/native.ts new file mode 100644 index 00000000000..0d93b264706 --- /dev/null +++ b/packages/core/src/kilocode/provider-usage/minimax/native.ts @@ -0,0 +1,34 @@ +import { Schema } from "effect" + +const IntegerField = Schema.Int +// Matches the cloud schema: remaining percent is a 0-100 share of the base quota; boosts scale it separately. +const PercentField = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(100)) + +export const ModelRemains = Schema.Struct({ + model_name: Schema.String, + current_interval_total_count: Schema.optional(IntegerField), + current_interval_usage_count: Schema.optional(IntegerField), + start_time: Schema.optional(IntegerField), + end_time: Schema.optional(IntegerField), + remains_time: Schema.optional(IntegerField), + interval_boost_permille: Schema.optional(IntegerField), + current_interval_remaining_percent: Schema.optional(PercentField), + current_interval_status: Schema.optional(IntegerField), + current_weekly_total_count: Schema.optional(IntegerField), + current_weekly_usage_count: Schema.optional(IntegerField), + weekly_start_time: Schema.optional(IntegerField), + weekly_end_time: Schema.optional(IntegerField), + weekly_remains_time: Schema.optional(IntegerField), + weekly_boost_permille: Schema.optional(IntegerField), + current_weekly_remaining_percent: Schema.optional(PercentField), + current_weekly_status: Schema.optional(IntegerField), +}).annotate({ identifier: "MiniMaxModelRemains" }) +export type ModelRemains = typeof ModelRemains.Type + +export const Native = Schema.Struct({ + base_resp: Schema.Struct({ status_code: IntegerField }), + model_remains: Schema.Array(ModelRemains), +}).annotate({ identifier: "MiniMaxNativeUsage" }) +export type Native = typeof Native.Type + +export const decode = Schema.decodeUnknownSync(Native) diff --git a/packages/core/src/kilocode/provider-usage/minimax/usage.ts b/packages/core/src/kilocode/provider-usage/minimax/usage.ts new file mode 100644 index 00000000000..dac61af1117 --- /dev/null +++ b/packages/core/src/kilocode/provider-usage/minimax/usage.ts @@ -0,0 +1,284 @@ +import { createHash } from "node:crypto" +import { decode, type ModelRemains, type Native } from "./native" +import type { ProviderUsage } from "@opencode-ai/schema/kilocode/provider-usage" + +export const bindings = { + "minimax-coding-plan": { + region: "global", + url: "https://api.minimax.io/v1/token_plan/remains", + manage: "https://platform.minimax.io/subscribe/token-plan", + }, + "minimax-cn-coding-plan": { + region: "china", + url: "https://api.minimaxi.com/v1/token_plan/remains", + manage: "https://platform.minimaxi.com/subscribe/token-plan", + }, +} as const + +type ProviderID = keyof typeof bindings + +const timeout = 5_000 +const limit = 64 * 1024 + +class MiniMaxUsageError extends Error { + constructor(readonly code: "network" | "http" | "too_large" | "invalid" | "application") { + super("MiniMax usage is temporarily unavailable.") + this.name = "MiniMaxUsageError" + } +} + +async function text(response: Response) { + const declared = Number(response.headers.get("content-length")) + if (Number.isFinite(declared) && declared > limit) { + response.body?.cancel().catch(() => undefined) + throw new MiniMaxUsageError("too_large") + } + + if (!response.body) { + const value = await response.arrayBuffer() + if (value.byteLength > limit) throw new MiniMaxUsageError("too_large") + return new TextDecoder().decode(value) + } + + const reader = response.body.getReader() + const chunks: Uint8Array[] = [] + let size = 0 + while (true) { + const chunk = await reader.read() + if (chunk.done) break + if (!chunk.value) continue + size += chunk.value.byteLength + if (size > limit) { + await reader.cancel().catch(() => undefined) + throw new MiniMaxUsageError("too_large") + } + chunks.push(chunk.value) + } + + const value = new Uint8Array(size) + let offset = 0 + for (const chunk of chunks) { + value.set(chunk, offset) + offset += chunk.byteLength + } + return new TextDecoder().decode(value) +} + +export async function query(providerID: ProviderID, key: string, fetcher: typeof fetch = fetch): Promise { + const response = await fetcher(bindings[providerID].url, { + method: "GET", + headers: { + Accept: "application/json", + Authorization: `Bearer ${key}`, + }, + cache: "no-store", + redirect: "error", + signal: AbortSignal.timeout(timeout), + }).catch(() => { + throw new MiniMaxUsageError("network") + }) + if (!response.ok) { + response.body?.cancel().catch(() => undefined) + throw new MiniMaxUsageError("http") + } + + const body = await text(response) + const native = (() => { + try { + return decode(JSON.parse(body)) + } catch { + throw new MiniMaxUsageError("invalid") + } + })() + if (native.base_resp.status_code !== 0) throw new MiniMaxUsageError("application") + return native +} + +function reset(end: number | undefined, remains: number | undefined, fetchedAt: string) { + if (end !== undefined && end > 0) return new Date(end).toISOString() + if (remains !== undefined && remains > 0) return new Date(Date.parse(fetchedAt) + remains).toISOString() + return undefined +} + +function duration(start: number | undefined, end: number | undefined) { + if (start === undefined || end === undefined || end <= start) return undefined + return end - start +} + +const hourMs = 60 * 60 * 1000 +const dayMs = 24 * hourMs +const weekMs = 7 * dayMs + +function cadence(kind: "interval" | "weekly", span: number | undefined): ProviderUsage.UsagePeriod | undefined { + if (kind === "weekly") return { unit: "week", value: 1 } + if (span === undefined) return undefined + if (span % weekMs === 0) return { unit: "week", value: span / weekMs } + if (span % dayMs === 0) return { unit: "day", value: span / dayMs } + if (span % hourMs === 0) return { unit: "hour", value: span / hourMs } + return undefined +} + +function window( + row: ModelRemains, + kind: "interval" | "weekly", + fetchedAt: string, +): ProviderUsage.UsageWindow | undefined { + const weekly = kind === "weekly" + const percent = weekly ? row.current_weekly_remaining_percent : row.current_interval_remaining_percent + const status = weekly ? row.current_weekly_status : row.current_interval_status + const total = weekly ? row.current_weekly_total_count : row.current_interval_total_count + const count = weekly ? row.current_weekly_usage_count : row.current_interval_usage_count + const start = weekly ? row.weekly_start_time : row.start_time + const end = weekly ? row.weekly_end_time : row.end_time + const remains = weekly ? row.weekly_remains_time : row.remains_time + const boost = weekly ? row.weekly_boost_permille : row.interval_boost_permille + const span = duration(start, end) + const base = { + id: `${row.model_name}-${kind}`, + resource: row.model_name, + period: cadence(kind, span), + durationMs: span, + resetAt: reset(end, remains, fetchedAt), + } + + if (status === 3) return { ...base, unit: "unknown", orientation: "amount", state: "not_in_plan" } + + if (percent !== undefined) { + const factor = boost !== undefined && boost > 0 ? boost / 1000 : 1 + const cap = 100 * factor + // The status flag is authoritative: an exhausted window has zero remaining even when the percent field lags. + const remaining = status === 2 ? 0 : percent * factor + return { + ...base, + unit: factor === 1 ? "percent" : "standard_units", + orientation: factor === 1 ? "remaining_percent" : "amount", + used: Math.max(0, cap - remaining), + remaining, + limit: cap, + state: status === 2 || remaining <= 0 ? "exhausted" : "active", + } + } + + if (total !== undefined && total > 0 && count !== undefined && count >= 0) { + // Despite the name, MiniMax's *_usage_count fields report the remaining quota, not the consumed amount. + const remaining = status === 2 ? 0 : count + return { + ...base, + unit: "count", + orientation: "count", + used: Math.max(0, total - remaining), + remaining, + limit: total, + state: status === 2 || remaining === 0 ? "exhausted" : "active", + } + } + + if (status === undefined && total === undefined && count === undefined) return undefined + return { + ...base, + unit: "unknown", + orientation: "amount", + state: status === 2 ? "exhausted" : "unknown", + } +} + +export function normalize( + native: Native, + input: { + id: string + providerID: string + sourceLabel: string + managementUrl: string + fetchedAt: string + }, +): ProviderUsage.UsageSnapshot { + const windows = native.model_remains + .filter((row) => row.model_name !== "video") + .flatMap((row) => + (["interval", "weekly"] as const).flatMap((kind) => { + const value = window(row, kind, input.fetchedAt) + return value ? [value] : [] + }), + ) + + return { + id: input.id, + providerID: input.providerID, + sourceKind: "direct", + providerLabel: "MiniMax", + planLabel: "MiniMax Token Plan", + sourceLabel: input.sourceLabel, + fetchState: "ready", + planState: "active", + routingState: "not_applicable", + fetchedAt: input.fetchedAt, + managementUrl: input.managementUrl, + windows, + } +} + +const unavailable = ( + id: string, + providerID: string, + label: string, + managementUrl: string, +): ProviderUsage.UsageSnapshot => ({ + id, + providerID, + sourceKind: "direct", + providerLabel: "MiniMax", + planLabel: "MiniMax Token Plan", + sourceLabel: label, + fetchState: "unavailable", + planState: "unknown", + routingState: "not_applicable", + managementUrl, + windows: [], + error: { code: "direct_minimax_unavailable", message: "Usage unavailable.", retryable: true }, +}) + +export interface Candidate { + providerID: ProviderID + label: string + key: string +} + +export async function direct( + candidates: readonly Candidate[], + fetcher: typeof fetch = fetch, + cached: ( + id: string, + load: () => Promise, + identity?: string, + ) => Promise = (_id, load) => load(), +) { + // Each configured provider is an independent plan with a stable per-region + // cache cell, even when providers share a credential. + return Promise.all( + candidates + .filter((candidate) => candidate.key.startsWith("sk-cp")) + .map((candidate) => { + const binding = bindings[candidate.providerID] + const id = `minimax-direct-${binding.region}` + // The key fingerprint scopes the cache cell to the configured credential, so + // swapping keys never reuses the previous account's quota via TTL or stale fallback. + const identity = createHash("sha256").update(candidate.key).digest("hex") + return cached( + id, + () => + query(candidate.providerID, candidate.key, fetcher) + .then((native) => + normalize(native, { + id, + providerID: candidate.providerID, + sourceLabel: candidate.label, + managementUrl: binding.manage, + fetchedAt: new Date().toISOString(), + }), + ) + .catch(() => unavailable(id, candidate.providerID, candidate.label, binding.manage)), + identity, + ) + }), + ) +} diff --git a/packages/core/src/kilocode/pty-self-command.ts b/packages/core/src/kilocode/pty-self-command.ts new file mode 100644 index 00000000000..360e094bda7 --- /dev/null +++ b/packages/core/src/kilocode/pty-self-command.ts @@ -0,0 +1,61 @@ +import path from "path" + +type Input = { + command?: string + args?: string[] + cwd?: string +} + +type Command = { + command: string + args: string[] + cwd?: string +} + +const names = new Set(["kilo", "kilocode"]) +const self = command() + +function clean(input: string[]) { + return input.filter((arg, index) => { + if (arg === "--cwd") return false + if (input[index - 1] === "--cwd") return false + if (arg.startsWith("--cwd=")) return false + return true + }) +} + +function full(input: string, cwd: string) { + if (path.isAbsolute(input)) return input + return path.resolve(cwd, input) +} + +export function command( + proc = { argv: process.argv, execArgv: process.execArgv, execPath: process.execPath, cwd: process.cwd() }, +): Command { + const script = proc.argv[1] + const bundled = script?.startsWith("/$bunfs/") || (script ? /^[A-Za-z]:[\\/]~BUN[\\/]/.test(script) : false) + if (script && !bundled && /\.(ts|js|mjs|cjs)$/.test(script)) { + const file = full(script, proc.cwd) + const dir = path.dirname(file) + const root = path.basename(dir) === "src" ? path.dirname(dir) : proc.cwd + return { command: full(proc.execPath, proc.cwd), args: [...clean(proc.execArgv), file], cwd: root } + } + return { command: full(proc.execPath, proc.cwd), args: [] } +} + +export function resolve(input: Input, cmd = self): Input { + if (!input.command || !names.has(input.command)) return input + const args = input.args ?? [] + const project = cmd.cwd && args.length === 0 && input.cwd ? [input.cwd] : [] + return { + ...input, + command: cmd.command, + args: [...cmd.args, ...project, ...args], + cwd: cmd.cwd ?? input.cwd, + } +} + +export const KiloPtySelfCommand = { + command, + resolve, +} diff --git a/packages/core/src/kilocode/pty/registry.ts b/packages/core/src/kilocode/pty/registry.ts new file mode 100644 index 00000000000..08ea77e5213 --- /dev/null +++ b/packages/core/src/kilocode/pty/registry.ts @@ -0,0 +1,264 @@ +import type { Disp, Proc } from "#pty" +import path from "node:path" +import { Log } from "../../util/log" +import type { Location } from "../../location" +import type { Info } from "../../pty" +import type { PtyID } from "../../pty/schema" +import { KiloPtyTermination } from "./termination" + +const log = Log.create({ service: "pty.registry" }) + +export type Subscriber = { + readonly onData: (chunk: string) => void + readonly onEnd: (event: { exitCode?: number }) => void + active: boolean + detached: boolean + pending: string[] + end?: { exitCode?: number } +} + +export type Active = { + info: Info + location: Location.Ref + process: Proc + buffer: string + bufferCursor: number + cursor: number + subscribers: Map + listeners: Disp[] + stopping: boolean + terminated: boolean + closing?: Promise +} + +export const sessions = new Map() +const exited = new Map() +const pending = new Map() +const blocked = new Map() +const removing = new Set() +const waiters = new Set<() => void>() +const directoryTasks = new Map>() +let next = 0 +let closing = false +let shutdownTask: Promise | undefined +let owners = 0 + +export function sameLocation(a: Location.Ref, b: Location.Ref) { + return sameDirectory(a.directory, b.directory) && a.workspaceID === b.workspaceID +} + +export function sameDirectory(a: string, b: string) { + const left = directoryKey(a) + const right = directoryKey(b) + return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right +} + +function directoryKey(value: string) { + return path.resolve(value) +} + +function locationKey(location: Location.Ref) { + const directory = directoryKey(location.directory) + return `${location.workspaceID ?? ""}\u0000${process.platform === "win32" ? directory.toLowerCase() : directory}` +} + +function matchesDirectory(location: Location.Ref, target: Location.Ref) { + return sameDirectory(location.directory, target.directory) && location.workspaceID === target.workspaceID +} + +function wake() { + const current = [...waiters] + waiters.clear() + for (const resolve of current) resolve() +} + +function waitFor(check: () => boolean): Promise { + if (check()) return Promise.resolve() + return new Promise((resolve) => waiters.add(resolve)).then(() => waitFor(check)) +} + +export function beginCreate(location: Location.Ref) { + if (closing) throw new Error("PTY registry is shutting down") + for (const target of blocked.values()) { + if (matchesDirectory(location, target)) throw new Error("PTY directory is being removed") + } + const id = ++next + pending.set(id, location) + let released = false + return () => { + if (released) return + released = true + pending.delete(id) + wake() + } +} + +export function claimRemoval(id: PtyID) { + if (removing.has(id)) return false + removing.add(id) + return true +} + +export function releaseRemoval(id: PtyID) { + removing.delete(id) +} + +export async function acquireOwner() { + if (shutdownTask) await shutdownTask.catch(() => undefined) + owners++ + let released = false + return async () => { + if (released) return + released = true + owners-- + if (owners === 0) await shutdown() + } +} + +export function hasDirectory(location: Location.Ref) { + return Array.from(sessions.values()).some((session) => matchesDirectory(session.location, location)) +} + +export function markExited(session: Active) { + const key = locationKey(session.location) + const order = exited.get(key) ?? [] + if (!exited.has(key)) exited.set(key, order) + order.push(session.info.id) +} + +export function oldestExited(location: Location.Ref) { + return exited.get(locationKey(location))?.[0] +} + +export function exitedCount(location: Location.Ref) { + return exited.get(locationKey(location))?.length ?? 0 +} + +export function removeExitedID(location: Location.Ref, id: PtyID) { + const key = locationKey(location) + const order = exited.get(key) + if (!order) return + const index = order.indexOf(id) + if (index !== -1) order.splice(index, 1) + if (order.length === 0) exited.delete(key) +} + +export function removeExited(session: Active) { + removeExitedID(session.location, session.info.id) +} + +function notifyEnd(session: Active, event: { exitCode?: number }) { + for (const subscriber of session.subscribers.values()) { + if (!subscriber.active) { + subscriber.end = event + continue + } + try { + subscriber.onEnd(event) + } catch (error) { + log.debug("PTY subscriber end callback failed", { error, id: session.info.id }) + } + } + session.subscribers.clear() +} + +export function teardown(session: Active) { + if (session.closing) return session.closing + const task = (async () => { + session.stopping = true + try { + if (!session.terminated && session.info.status !== "exited") { + await KiloPtyTermination.terminate(session.process) + } + session.terminated = true + for (const listener of session.listeners) listener.dispose() + session.listeners.length = 0 + notifyEnd(session, session.info.status === "exited" ? { exitCode: session.info.exitCode } : {}) + } catch (error) { + session.stopping = false + throw error + } + })() + session.closing = task + void task.catch(() => { + if (session.closing === task) session.closing = undefined + }) + return task +} + +async function teardownMany(owned: Active[]) { + const results = await Promise.allSettled(owned.map(teardown)) + const failed: unknown[] = [] + for (let index = 0; index < owned.length; index++) { + const session = owned[index] + const result = results[index] + if (!session || !result) continue + if (result.status === "rejected") { + failed.push(result.reason) + continue + } + if (sessions.get(session.info.id) !== session) continue + sessions.delete(session.info.id) + removeExited(session) + } + if (failed.length > 0) throw new AggregateError(failed, "Failed to tear down one or more PTYs") +} + +export function shutdown() { + if (shutdownTask) return shutdownTask + const task = (async () => { + closing = true + let success = false + try { + await waitFor(() => pending.size === 0) + await teardownMany(Array.from(sessions.values())) + success = true + } finally { + closing = false + wake() + } + })() + shutdownTask = task + void task.then( + () => { + if (shutdownTask === task) shutdownTask = undefined + }, + () => { + if (shutdownTask === task) shutdownTask = undefined + }, + ) + return task +} + +async function terminateDirectoryOnce(target: Location.Ref) { + const key = locationKey(target) + blocked.set(key, target) + let success = false + try { + await waitFor(() => ![...pending.values()].some((location) => matchesDirectory(location, target))) + const owned = Array.from(sessions.values()).filter((session) => matchesDirectory(session.location, target)) + await teardownMany(owned) + success = true + } finally { + if (success) blocked.delete(key) + wake() + } +} + +export function terminateDirectory(target: Location.Ref) { + const key = locationKey(target) + const current = directoryTasks.get(key) + if (current) return current + const task = terminateDirectoryOnce(target) + directoryTasks.set(key, task) + void task.then( + () => { + if (directoryTasks.get(key) === task) directoryTasks.delete(key) + }, + (error) => { + log.warn("failed to tear down PTY for directory", { error, target }) + if (directoryTasks.get(key) === task) directoryTasks.delete(key) + }, + ) + return task +} diff --git a/packages/core/src/kilocode/pty/termination.ts b/packages/core/src/kilocode/pty/termination.ts new file mode 100644 index 00000000000..33b29936d20 --- /dev/null +++ b/packages/core/src/kilocode/pty/termination.ts @@ -0,0 +1,182 @@ +import { spawn } from "child_process" +import { setTimeout as sleep } from "node:timers/promises" +import type { Proc } from "../../pty/pty" +import { Log } from "../../util/log" + +const log = Log.create({ service: "pty.termination" }) +const GRACE_MS = 200 +const SPAWN_TIMEOUT_MS = 5_000 + +export type Process = Pick + +export type Runtime = { + readonly platform: NodeJS.Platform + readonly taskkill: ( + file: string, + args: string[], + opts: { stdio: "ignore"; windowsHide: true; timeout: number }, + ) => Promise + readonly tree: () => Promise> + readonly alive: (pid: number) => boolean + readonly signal: (pid: number, signal: "SIGTERM" | "SIGKILL") => void + readonly sleep: (ms: number) => Promise +} + +const runtime: Runtime = { + platform: process.platform, + taskkill, + tree, + alive: (pid) => { + try { + process.kill(pid, 0) + return true + } catch { + return false + } + }, + signal: (pid, signal) => process.kill(pid, signal), + sleep, +} + +function direct(proc: Process, signal?: "SIGTERM" | "SIGKILL") { + try { + proc.kill(signal) + } catch (err) { + log.warn("failed to kill PTY directly", { err, pid: proc.pid, signal }) + } +} + +function descendants(root: number, rows: Array<{ pid: number; parent: number }>) { + const children = new Map() + for (const row of rows) { + const list = children.get(row.parent) ?? [] + list.push(row.pid) + children.set(row.parent, list) + } + const seen = new Set() + const collect = (pid: number): number[] => { + const result: number[] = [] + for (const child of children.get(pid) ?? []) { + if (seen.has(child)) continue + seen.add(child) + result.push(...collect(child), child) + } + return result + } + return collect(root) +} + +async function family(root: number, input: Runtime) { + const rows = await input.tree() + return [...descendants(root, rows), root] +} + +function signal(proc: Process, pids: number[], value: "SIGTERM" | "SIGKILL", input: Runtime) { + for (const pid of pids) { + let sent = false + for (const target of [-pid, pid]) { + try { + input.signal(target, value) + sent = true + } catch (err) { + log.debug("failed to signal PTY process", { err, pid: target, signal: value }) + } + } + if (pid === proc.pid && !sent) direct(proc, value) + } +} + +async function tree(file: string = "ps", args: string[] = ["-axo", "pid=,ppid="]) { + return await new Promise>((resolve, reject) => { + try { + const child = spawn(file, args, { + stdio: ["ignore", "pipe", "ignore"], + windowsHide: true, + timeout: SPAWN_TIMEOUT_MS, + killSignal: "SIGKILL", + }) + const chunks: Buffer[] = [] + child.stdout?.on("data", (chunk: Buffer) => chunks.push(chunk)) + child.once("error", reject) + child.once("close", (code) => { + if (code !== 0) return reject(new Error(`process tree command exited with ${code}`)) + const rows = Buffer.concat(chunks) + .toString("utf8") + .trim() + .split("\n") + .filter(Boolean) + .map((line) => line.trim().split(/\s+/).map(Number)) + .filter(([pid, parent]) => Number.isSafeInteger(pid) && Number.isSafeInteger(parent)) + .map(([pid, parent]) => ({ pid: pid!, parent: parent! })) + resolve(rows) + }) + } catch (error) { + reject(error) + } + }) +} + +async function taskkill(file: string, args: string[], opts: { stdio: "ignore"; windowsHide: true; timeout: number }) { + return await new Promise((resolve) => { + try { + const child = spawn(file, args, opts) + child.once("exit", (code) => resolve(code === 0)) + child.once("error", (err) => { + log.warn("taskkill failed", { err }) + resolve(false) + }) + } catch (err) { + log.warn("failed to start taskkill", { err }) + resolve(false) + } + }) +} + +export async function terminate(proc: Process, input: Runtime = runtime): Promise { + const state = { exited: false } + const listener = proc.onExit(() => { + state.exited = true + }) + try { + if (!proc.pid) { + direct(proc) + if (!state.exited) await input.sleep(GRACE_MS) + await verify(proc, state.exited, input) + return + } + + if (input.platform === "win32") { + const killed = await input.taskkill("taskkill", ["/pid", String(proc.pid), "/f", "/t"], { + stdio: "ignore", + windowsHide: true, + timeout: SPAWN_TIMEOUT_MS, + }) + if ((!killed || input.alive(proc.pid)) && !state.exited) direct(proc) + if (!state.exited) await input.sleep(GRACE_MS) + await verify(proc, state.exited, input) + return + } + + const initial = await family(proc.pid, input) + signal(proc, initial, "SIGTERM", input) + await input.sleep(GRACE_MS) + const remaining = new Set(initial.filter(input.alive)) + if (input.alive(proc.pid)) for (const pid of await family(proc.pid, input)) remaining.add(pid) + if (remaining.size > 0) { + signal(proc, [...remaining], "SIGKILL", input) + await input.sleep(GRACE_MS) + } + await verify(proc, state.exited, input) + } finally { + listener.dispose() + } +} + +async function verify(proc: Process, exited: boolean, input: Runtime) { + if (!proc.pid) return + const live = (await family(proc.pid, input)).filter((pid) => input.alive(pid) && !(pid === proc.pid && exited)) + if (!exited && input.alive(proc.pid) && !live.includes(proc.pid)) live.push(proc.pid) + if (live.length > 0) throw new Error(`PTY process tree is still alive: ${live.join(", ")}`) +} + +export * as KiloPtyTermination from "./termination" diff --git a/packages/core/src/kilocode/ripgrep-grep.ts b/packages/core/src/kilocode/ripgrep-grep.ts new file mode 100644 index 00000000000..cac163c7f5d --- /dev/null +++ b/packages/core/src/kilocode/ripgrep-grep.ts @@ -0,0 +1,50 @@ +import type { Match } from "@opencode-ai/schema/filesystem" + +export interface Options { + readonly context?: number + readonly literal?: boolean + readonly ignoreCase?: boolean +} + +export type GrepMatch = Match & { + readonly context: boolean + readonly textTruncated: boolean +} + +export const flags = (input: Options) => [ + ...(input.literal ? ["--fixed-strings"] : []), + ...(input.ignoreCase ? ["--ignore-case"] : []), + ...(input.context ? [`--context=${input.context}`] : []), +] + +export const stop = (limit: number) => { + let matches = 0 + return (row: { readonly context: boolean }) => !row.context && ++matches > limit +} + +export const select = < + A extends { + readonly context: boolean + readonly path: { readonly text: string } + readonly line_number: number + }, +>( + input: { readonly limit: number; readonly context?: number }, + items: readonly A[], +) => { + let count = 0 + const overflow = items.findIndex((row) => !row.context && ++count > input.limit) + const selected = items.slice(0, overflow === -1 ? items.length : overflow) + const matches = selected.filter((row) => !row.context) + return selected.filter( + (row) => + !row.context || + matches.some( + (match) => + match.path.text === row.path.text && Math.abs(match.line_number - row.line_number) <= (input.context ?? 0), + ), + ) +} + +export const decorate = (match: Match, context: boolean, textTruncated: boolean): GrepMatch => + Object.assign(match, { context, textTruncated }) diff --git a/packages/core/src/kilocode/search-target.ts b/packages/core/src/kilocode/search-target.ts new file mode 100644 index 00000000000..8ef90b679a9 --- /dev/null +++ b/packages/core/src/kilocode/search-target.ts @@ -0,0 +1,39 @@ +import path from "path" +import { Effect, Option } from "effect" +import { FSUtil } from "../fs-util" +import { ToolOutputStore } from "../tool-output-store" + +export interface Target { + readonly path: string + readonly type: "file" | "directory" + readonly dev: number + readonly ino: number +} + +export const inspect = Effect.fn("SearchTarget.inspect")(function* (fs: FSUtil.Interface, input: string) { + const target = yield* fs.realPath(input) + const info = yield* fs.stat(target) + if ((yield* fs.realPath(input)) !== target) + return yield* Effect.fail(new Error("Search target changed during inspection")) + const type = info.type === "File" ? "file" : info.type === "Directory" ? "directory" : undefined + const ino = Option.getOrUndefined(info.ino) + if (!type || ino === undefined) return yield* Effect.fail(new Error("Search target identity is unavailable")) + return { path: target, type, dev: info.dev, ino } satisfies Target +}) + +export const validate = Effect.fn("SearchTarget.validate")(function* (fs: FSUtil.Interface, target: Target) { + const info = yield* fs.stat(target.path) + const type = info.type === "File" ? "file" : info.type === "Directory" ? "directory" : undefined + if (type === target.type && info.dev === target.dev && Option.getOrUndefined(info.ino) === target.ino) return + yield* Effect.fail(new Error("Search target changed after approval")) +}) + +export const managed = Effect.fn("SearchTarget.managed")(function* ( + fs: FSUtil.Interface, + data: string, + target: Target, +) { + if (target.type !== "file" || !path.basename(target.path).startsWith("tool_")) return false + const directory = yield* fs.realPath(path.join(data, ToolOutputStore.MANAGED_DIRECTORY)) + return path.dirname(target.path) === directory +}) diff --git a/packages/core/src/kilocode/session-message.ts b/packages/core/src/kilocode/session-message.ts new file mode 100644 index 00000000000..74f89af193a --- /dev/null +++ b/packages/core/src/kilocode/session-message.ts @@ -0,0 +1,51 @@ +import { StoredToolContent } from "@opencode-ai/llm" +import { Schema } from "effect" + +const decode = Schema.decodeUnknownSync(StoredToolContent) +const encodeContent = Schema.encodeUnknownSync(StoredToolContent) + +function record(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + +export function normalize(value: unknown): unknown { + if (!record(value)) return value + // New readers recover the canonical summary while old readers receive recent context inline. + if (value.type === "compaction" && typeof value.kilo_summary === "string") { + return { ...value, summary: value.kilo_summary } + } + if (value.type !== "assistant" || !Array.isArray(value.content)) return value + return { + ...value, + content: value.content.map((item) => { + if (!record(item) || item.type !== "tool" || !record(item.state)) return item + const status = item.state.status + if (status !== "running" && status !== "completed" && status !== "error") return item + if (!Array.isArray(item.state.content)) return item + return { ...item, state: { ...item.state, content: item.state.content.map((entry) => decode(entry)) } } + }), + } +} + +export function encode(value: unknown): unknown { + if (!record(value)) return value + // Preserve current semantics while making released compaction rows self-contained. + if (value.type === "compaction" && typeof value.summary === "string" && typeof value.recent === "string") { + return { + ...value, + summary: [value.summary, value.recent ? `Recent context:\n${value.recent}` : ""].filter(Boolean).join("\n\n"), + kilo_summary: value.summary, + } + } + if (value.type !== "assistant" || !Array.isArray(value.content)) return value + return { + ...value, + content: value.content.map((item) => { + if (!record(item) || item.type !== "tool" || !record(item.state)) return item + const status = item.state.status + if (status !== "running" && status !== "completed" && status !== "error") return item + if (!Array.isArray(item.state.content)) return item + return { ...item, state: { ...item.state, content: item.state.content.map((entry) => encodeContent(entry)) } } + }), + } +} diff --git a/packages/core/src/kilocode/session/prompt-promoted.ts b/packages/core/src/kilocode/session/prompt-promoted.ts new file mode 100644 index 00000000000..38a75b00698 --- /dev/null +++ b/packages/core/src/kilocode/session/prompt-promoted.ts @@ -0,0 +1,42 @@ +import { PromptPromoted } from "@opencode-ai/schema/kilocode/durable-event" +import { Effect } from "effect" +import { Database } from "../../database/database" +import { SessionEvent } from "../../session/event" +import { SessionInput } from "../../session/input" + +export const definition = PromptPromoted + +export function project( + db: Database.Interface["db"], + event: typeof PromptPromoted.Type, + commit: (event: typeof SessionEvent.Prompted.Type) => Effect.Effect, +) { + return Effect.gen(function* () { + if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence") + const admitted = yield* SessionInput.find(db, event.data.messageID) + if (!admitted) return yield* Effect.die(new SessionInput.LifecycleConflict({ id: event.data.messageID })) + const prompted = SessionEvent.Prompted.make({ + id: event.id, + type: SessionEvent.Prompted.type, + durable: event.durable, + location: event.location, + metadata: event.metadata, + data: { + sessionID: event.data.sessionID, + messageID: event.data.messageID, + timestamp: event.data.timeCreated, + prompt: event.data.prompt, + delivery: admitted.delivery, + }, + }) + yield* SessionInput.projectPrompted(db, { + id: prompted.data.messageID, + sessionID: prompted.data.sessionID, + prompt: prompted.data.prompt, + delivery: prompted.data.delivery, + timeCreated: prompted.data.timestamp, + promotedSeq: event.durable.seq, + }) + yield* commit(prompted) + }) +} diff --git a/packages/core/src/kilocode/session/recall-part-index.ts b/packages/core/src/kilocode/session/recall-part-index.ts new file mode 100644 index 00000000000..08762280e70 --- /dev/null +++ b/packages/core/src/kilocode/session/recall-part-index.ts @@ -0,0 +1,25 @@ +import { sql } from "drizzle-orm" +import { index, type AnySQLiteColumn } from "drizzle-orm/sqlite-core" + +export namespace RecallPartIndex { + export const createSql = `CREATE INDEX IF NOT EXISTS \`recall_part_search_idx\` ON \`part\` (\`session_id\`,\`id\`,\`message_id\`,json_extract("data", '$.type'),CASE WHEN json_extract("data", '$.type') = 'text' THEN coalesce(json_extract("data", '$.text'), '') WHEN json_extract("data", '$.type') = 'file' THEN trim(coalesce(json_extract("data", '$.filename'), '') || ' ' || CASE WHEN coalesce(json_extract("data", '$.url'), '') NOT LIKE 'data:%' THEN coalesce(json_extract("data", '$.url'), '') ELSE '' END || ' ' || coalesce(json_extract("data", '$.source.path'), '') || ' ' || coalesce(json_extract("data", '$.source.name'), '') || ' ' || CASE WHEN coalesce(json_extract("data", '$.source.uri'), '') NOT LIKE 'data:%' THEN coalesce(json_extract("data", '$.source.uri'), '') ELSE '' END || ' ' || coalesce(json_extract("data", '$.source.clientName'), '')) ELSE coalesce(json_extract("data", '$.state.error'), '') END) WHERE json_valid("part"."data") AND ((json_extract("part"."data", '$.type') = 'text' AND coalesce(json_extract("part"."data", '$.synthetic'), 0) = 0 AND coalesce(json_extract("part"."data", '$.ignored'), 0) = 0) OR json_extract("part"."data", '$.type') = 'file' OR (json_extract("part"."data", '$.type') = 'tool' AND json_extract("part"."data", '$.state.status') = 'error'));` + + export function make(table: { + session_id: AnySQLiteColumn + id: AnySQLiteColumn + message_id: AnySQLiteColumn + data: AnySQLiteColumn + }) { + return index("recall_part_search_idx") + .on( + table.session_id, + table.id, + table.message_id, + sql`json_extract(${table.data}, '$.type')`, + sql`CASE WHEN json_extract(${table.data}, '$.type') = 'text' THEN coalesce(json_extract(${table.data}, '$.text'), '') WHEN json_extract(${table.data}, '$.type') = 'file' THEN trim(coalesce(json_extract(${table.data}, '$.filename'), '') || ' ' || CASE WHEN coalesce(json_extract(${table.data}, '$.url'), '') NOT LIKE 'data:%' THEN coalesce(json_extract(${table.data}, '$.url'), '') ELSE '' END || ' ' || coalesce(json_extract(${table.data}, '$.source.path'), '') || ' ' || coalesce(json_extract(${table.data}, '$.source.name'), '') || ' ' || CASE WHEN coalesce(json_extract(${table.data}, '$.source.uri'), '') NOT LIKE 'data:%' THEN coalesce(json_extract(${table.data}, '$.source.uri'), '') ELSE '' END || ' ' || coalesce(json_extract(${table.data}, '$.source.clientName'), '')) ELSE coalesce(json_extract(${table.data}, '$.state.error'), '') END`, + ) + .where( + sql`json_valid(${table.data}) AND ((json_extract(${table.data}, '$.type') = 'text' AND coalesce(json_extract(${table.data}, '$.synthetic'), 0) = 0 AND coalesce(json_extract(${table.data}, '$.ignored'), 0) = 0) OR json_extract(${table.data}, '$.type') = 'file' OR (json_extract(${table.data}, '$.type') = 'tool' AND json_extract(${table.data}, '$.state.status') = 'error'))`, + ) + } +} diff --git a/packages/core/src/kilocode/spawn-exit.ts b/packages/core/src/kilocode/spawn-exit.ts new file mode 100644 index 00000000000..3bd4c0f88a9 --- /dev/null +++ b/packages/core/src/kilocode/spawn-exit.ts @@ -0,0 +1,14 @@ +import type { ChildProcess } from "effect/unstable/process" + +const commands = new WeakSet() + +export function attach(command: ChildProcess.StandardCommand) { + commands.add(command) + return command +} + +export function take(command: ChildProcess.StandardCommand) { + const found = commands.has(command) + commands.delete(command) + return found +} diff --git a/packages/core/src/kilocode/spawn-validation.ts b/packages/core/src/kilocode/spawn-validation.ts new file mode 100644 index 00000000000..cb540deb81d --- /dev/null +++ b/packages/core/src/kilocode/spawn-validation.ts @@ -0,0 +1,15 @@ +import type { Effect } from "effect" +import type { ChildProcess } from "effect/unstable/process" + +const effects = new WeakMap>() + +export function attach(command: ChildProcess.StandardCommand, effect: Effect.Effect) { + effects.set(command, effect) + return command +} + +export function take(command: ChildProcess.StandardCommand) { + const effect = effects.get(command) + effects.delete(command) + return effect +} diff --git a/packages/core/src/kilocode/sqlite-error.ts b/packages/core/src/kilocode/sqlite-error.ts new file mode 100644 index 00000000000..60f52b531c0 --- /dev/null +++ b/packages/core/src/kilocode/sqlite-error.ts @@ -0,0 +1,10 @@ +import { Cause, Option } from "effect" +import { isSqlError } from "effect/unstable/sql/SqlError" + +export function isBusy(error: unknown): boolean { + if (isSqlError(error)) return error.reason._tag === "LockTimeoutError" + if (typeof error !== "object" || error === null || !("cause" in error) || error.cause === error) return false + if (!Cause.isCause(error.cause)) return isBusy(error.cause) + const failure = Cause.findErrorOption(error.cause) + return Option.isSome(failure) && isBusy(failure.value) +} diff --git a/packages/core/src/kilocode/stdio-tap.ts b/packages/core/src/kilocode/stdio-tap.ts new file mode 100644 index 00000000000..0bfe8011919 --- /dev/null +++ b/packages/core/src/kilocode/stdio-tap.ts @@ -0,0 +1,34 @@ +import type * as NodeChildProcess from "node:child_process" +import { PassThrough, type Readable } from "node:stream" + +// Bun's child_process drops buffered stdio data once the child emits "close", so +// stream readers that attach lazily (a tick or more after spawn) lose the output of +// fast-exiting processes entirely. To retain it, stdout/stderr are piped into +// PassThroughs synchronously at spawn time, before yielding to the event loop. +// PassThrough backpressure (default highWaterMark) keeps unconsumed output bounded. + +const map = new WeakMap() + +const wrap = (src: Readable | null) => { + if (!src) return null + const out = new PassThrough() + // A destroy(err) before the lazy consumer attaches would otherwise emit an + // unhandled "error" event and crash the process (a hazard that also existed + // when readers attached lazily to the raw stdio streams). Consumers attached + // by then still get the error via their own listeners; in the rare pre-attach + // window the error is dropped instead of crashing the CLI. + out.on("error", () => {}) + src.on("error", (err) => out.destroy(err instanceof Error ? err : new Error(String(err)))) + src.pipe(out) + return out +} + +/** Tap a freshly spawned process. Must be called in the same tick as spawn. */ +export function tap(proc: NodeChildProcess.ChildProcess) { + map.set(proc, { stdout: wrap(proc.stdout), stderr: wrap(proc.stderr) }) +} + +/** The tapped stream for a process, falling back to the raw stdio stream. */ +export function tapped(proc: NodeChildProcess.ChildProcess, fd: "stdout" | "stderr") { + return map.get(proc)?.[fd] ?? proc[fd]! +} diff --git a/packages/core/src/location-layer.ts b/packages/core/src/location-layer.ts deleted file mode 100644 index 67293f7c5f9..00000000000 --- a/packages/core/src/location-layer.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Layer, LayerMap } from "effect" -import { Location } from "./location" -import { Catalog } from "./catalog" -import { PluginBoot } from "./plugin/boot" -import { Policy } from "./policy" -import { Config } from "./config" - -export class LocationServiceMap extends LayerMap.Service()("@opencode/example/LocationServiceMap", { - lookup: (ref: Location.Ref) => { - const result = Layer.mergeAll(Catalog.defaultLayer, PluginBoot.defaultLayer, Config.defaultLayer).pipe( - Layer.provideMerge(Policy.defaultLayer), - Layer.provideMerge(Location.defaultLayer(ref)), - ) - return result - }, - idleTimeToLive: "60 minutes", - dependencies: [], -}) {} diff --git a/packages/core/src/location-mutation.ts b/packages/core/src/location-mutation.ts new file mode 100644 index 00000000000..5f410b95b1b --- /dev/null +++ b/packages/core/src/location-mutation.ts @@ -0,0 +1,162 @@ +export * as LocationMutation from "./location-mutation" + +import { makeLocationNode } from "./effect/app-node" +import path from "path" +import { Context, Effect, Layer, Schema } from "effect" +import { FSUtil } from "./fs-util" +import { Location } from "./location" + +export const Kind = Schema.Literals(["file", "directory"]) +export type Kind = typeof Kind.Type + +/** + * Mutation paths do not accept project references. Relative paths must stay + * inside the active Location. Absolute paths outside it require separate + * `external_directory` approval. + */ +export const ResolveInput = Schema.Struct({ + path: Schema.String, + /** Selects the external approval boundary; it does not validate the target type. */ + kind: Kind.pipe(Schema.optional), +}) +export type ResolveInput = typeof ResolveInput.Type + +export class PathError extends Schema.TaggedErrorClass()("LocationMutation.PathError", { + path: Schema.String, + reason: Schema.Literals(["relative_escape", "location_escape", "non_directory_ancestor"]), +}) {} + +export interface ExternalDirectoryAuthorization { + readonly action: "external_directory" + /** Canonical existing directory used as the external approval boundary. */ + readonly directory: string + /** `external_directory` permission resource. */ + readonly resource: string + readonly save: string +} + +export const externalDirectoryPermission = (input: ExternalDirectoryAuthorization) => ({ + action: input.action, + resources: [input.resource], + save: [input.save], +}) + +export interface Target { + /** Canonical existing path, or missing path below a canonical directory. */ + readonly canonical: string + /** Permission resource: Location-relative for internal paths, canonical for external paths. */ + readonly resource: string + readonly externalDirectory?: ExternalDirectoryAuthorization +} + +export interface Interface { + /** + * Resolve a path and derive its permission resources. Relative paths must + * stay inside the Location. Absolute paths outside it require separate + * `external_directory` approval. This does not approve the mutation. + */ + readonly resolve: (input: ResolveInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/LocationMutation") {} + +interface ResolvedPath { + readonly canonical: string + readonly type?: + | "File" + | "Directory" + | "SymbolicLink" + | "BlockDevice" + | "CharacterDevice" + | "FIFO" + | "Socket" + | "Unknown" + readonly directory: string +} + +const slash = (value: string) => value.replaceAll("\\", "/") + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const location = yield* Location.Service + const locationRoot = yield* fs.realPath(location.directory) + + function notFound(effect: Effect.Effect) { + return effect.pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined))) + } + + const resolvePath = Effect.fnUntraced(function* (absolute: string) { + const existing = yield* notFound(fs.realPath(absolute)) + if (existing !== undefined) { + const info = yield* fs.stat(existing) + return { + canonical: existing, + type: info.type, + directory: info.type === "Directory" ? existing : path.dirname(existing), + } satisfies ResolvedPath + } + + let anchor = path.dirname(absolute) + while (true) { + const canonical = yield* notFound(fs.realPath(anchor)) + if (canonical !== undefined) { + const info = yield* fs.stat(canonical) + if (info.type !== "Directory") { + return yield* new PathError({ path: absolute, reason: "non_directory_ancestor" }) + } + return { + canonical: path.resolve(canonical, path.relative(anchor, absolute)), + directory: canonical, + } satisfies ResolvedPath + } + const parent = path.dirname(anchor) + if (parent === anchor) return yield* new PathError({ path: absolute, reason: "non_directory_ancestor" }) + anchor = parent + } + }) + + const resolve = Effect.fn("LocationMutation.resolve")(function* (input: ResolveInput) { + const relative = !path.isAbsolute(input.path) + const absolute = path.resolve(location.directory, input.path) + const lexicallyInternal = FSUtil.contains(location.directory, absolute) + if (relative && !lexicallyInternal) return yield* new PathError({ path: input.path, reason: "relative_escape" }) + + const resolved = yield* resolvePath(absolute) + if (lexicallyInternal && !FSUtil.contains(locationRoot, resolved.canonical)) { + return yield* new PathError({ path: input.path, reason: "location_escape" }) + } + + const external = !lexicallyInternal + const resource = external + ? slash(resolved.canonical) + : slash(path.relative(locationRoot, resolved.canonical) || ".") + const externalDirectory = + input.kind === "directory" && resolved.type === "Directory" ? resolved.canonical : resolved.directory + const externalResource = slash(path.join(externalDirectory, "*")) + return { + canonical: resolved.canonical, + resource, + externalDirectory: external + ? { + action: "external_directory", + directory: externalDirectory, + resource: externalResource, + save: externalResource, + } + : undefined, + } satisfies Target + }) + + return Service.of({ resolve }) + }), +) + +export const locationLayer = layer + +export const node = makeLocationNode({ + service: Service, + layer: layer.pipe(Layer.orDie), + deps: [FSUtil.node, Location.node], +}) diff --git a/packages/core/src/location-service-map.ts b/packages/core/src/location-service-map.ts new file mode 100644 index 00000000000..1a6c26737dd --- /dev/null +++ b/packages/core/src/location-service-map.ts @@ -0,0 +1,18 @@ +import { Context, Effect, Layer, LayerMap } from "effect" +import { LayerNode } from "./effect/layer-node" +import { Node } from "./effect/app-node" +import { Location } from "./location" +import type { LocationError, LocationServices } from "./location-services" + +export class Service extends Context.Service< + Service, + LayerMap.LayerMap +>()("@opencode/example/LocationServiceMap") { + static get(ref: Location.Ref) { + return Layer.unwrap(Effect.map(Service, (locations) => locations.get(ref))) + } +} + +export const node = LayerNode.unbound(Service, Node.tags.values.global) + +export * as LocationServiceMap from "./location-service-map" diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts new file mode 100644 index 00000000000..2256ccd6bfc --- /dev/null +++ b/packages/core/src/location-services.ts @@ -0,0 +1,118 @@ +import { Duration, Effect, Layer, LayerMap } from "effect" // kilocode_change +import { AgentV2 } from "./agent" +import { AISDK } from "./aisdk" +import { Catalog } from "./catalog" +import { CommandV2 } from "./command" +import { Config } from "./config" +import { LayerNode } from "./effect/layer-node" +import { Node } from "./effect/app-node" +import { FileMutation } from "./file-mutation" +import { FileSystem } from "./filesystem" +import { FileSystemSearch } from "./filesystem/search" +import { Watcher } from "./filesystem/watcher" +import { Image } from "./image" +import { Integration } from "./integration" +import { ProviderUsage } from "./kilocode/provider-usage" // kilocode_change +import { Location } from "./location" +import { LocationMutation } from "./location-mutation" +import { LocationServiceMap } from "./location-service-map" +import { PermissionV2 } from "./permission" +import { PluginV2 } from "./plugin" +import { PluginInternal } from "./plugin/internal" +import { Policy } from "./policy" +import { ProjectCopy } from "./project/copy" +import { Pty } from "./pty" +import { QuestionV2 } from "./question" +import { Reference } from "./reference" +import { ReferenceGuidance } from "./reference/guidance" +import * as SessionRunnerLLM from "./session/runner/llm" +import { SessionRunnerModel } from "./session/runner/model" +import { SessionTodo } from "./session/todo" +import { SkillV2 } from "./skill" +import { SkillGuidance } from "./skill/guidance" +import { Snapshot } from "./snapshot" +import { SystemContextBuiltIns } from "./system-context/builtins" +import { SystemContextRegistry } from "./system-context/registry" +import { BuiltInTools } from "./tool/builtins" +import { ReadToolFileSystem } from "./tool/read-filesystem" +import { ToolRegistry } from "./tool/registry" +import { ToolOutputStore } from "./tool-output-store" + +export { LocationServiceMap } from "./location-service-map" + +export const locationServices = LayerNode.group([ + Location.node, + Policy.node, + Config.node, + AgentV2.node, + CommandV2.node, + Reference.node, + Integration.node, + Catalog.node, + ProviderUsage.node, // kilocode_change + AISDK.node, + PluginV2.node, + PluginInternal.node, + ProjectCopy.node, + ProjectCopy.refreshNode, + FileSystemSearch.node, + FileSystem.node, + Watcher.node, + Pty.node, + SkillV2.node, + SystemContextRegistry.node, + SystemContextBuiltIns.node, + LocationMutation.node, + FileMutation.node, + PermissionV2.node, + ToolOutputStore.node, + ToolRegistry.node, + ToolRegistry.toolsNode, + Image.node, + SkillGuidance.node, + ReferenceGuidance.node, + SessionTodo.node, + QuestionV2.node, + ReadToolFileSystem.node, + BuiltInTools.node, + SessionRunnerModel.node, + Snapshot.node, + SessionRunnerLLM.node, +]) + +export type LocationServices = LayerNode.Output +export type LocationError = LayerNode.Error + +export function buildLocationServiceMap( + replacements: LayerNode.Replacements = [], + options: { readonly idleTimeToLive?: Duration.Input } = {}, // kilocode_change +): Layer.Layer { + return Layer.effect( + LocationServiceMap.Service, + LayerMap.make( + (ref: Location.Ref) => { + const allReplacements = replacements.concat([[Location.node, Location.boundNode(ref)]]) + // Apply replacements during hoist, not afterward: replacements can + // introduce new tagged dependencies (Location.boundNode depends on + // Project), and the hoist walk is the only pass that can still slice + // those back out. + const location = LayerNode.hoist(locationServices, Node.tags.values.global, allReplacements) + + return LayerNode.compile(location.node).pipe( + Layer.fresh, + Layer.tap(() => + Effect.logInfo("booting location services", { + directory: ref.directory, + workspaceID: ref.workspaceID, + }), + ), + Layer.provide(LayerNode.compile(location.hoisted)), + ) + }, + { idleTimeToLive: options.idleTimeToLive ?? "60 minutes" }, // kilocode_change + ), + ) +} + +// This is temporary for backwards compatibility +export const locationServiceMapLayer = buildLocationServiceMap() diff --git a/packages/core/src/location.ts b/packages/core/src/location.ts index 68c9a8f791e..8228b8599ea 100644 --- a/packages/core/src/location.ts +++ b/packages/core/src/location.ts @@ -1,28 +1,22 @@ -import { Context, Effect, Layer, Schema } from "effect" +import { Context, Effect, Layer } from "effect" +import { Info, Ref, response } from "@opencode-ai/schema/location" import { Project } from "./project" -import { AbsolutePath } from "./schema" +import { LayerNode } from "./effect/layer-node" +import { makeLocationNode, tags } from "./effect/app-node" export * as Location from "./location" -export const Ref = Schema.Struct({ - directory: AbsolutePath, - workspaceID: Schema.optional(Schema.String), -}).annotate({ identifier: "Location.Ref" }) -export type Ref = typeof Ref.Type +export { Info, Ref, response } -export interface Interface { - readonly directory: AbsolutePath - readonly workspaceID?: string - readonly project: { - readonly id: Project.ID - readonly directory: AbsolutePath - } +export interface Interface extends Info { readonly vcs?: Project.Vcs } export class Service extends Context.Service()("@opencode/Location") {} -export const layer = (ref: Ref) => +export const node = LayerNode.unbound(Service, tags.values.location) + +const layer = (ref: Ref) => Layer.effect( Service, Effect.gen(function* () { @@ -37,4 +31,9 @@ export const layer = (ref: Ref) => }), ) -export const defaultLayer = (ref: Ref) => layer(ref).pipe(Layer.provide(Project.defaultLayer)) +export const boundNode = (ref: Ref) => + makeLocationNode({ + service: Service, + layer: layer(ref), + deps: [Project.node], + }) diff --git a/packages/core/src/markdown.d.ts b/packages/core/src/markdown.d.ts new file mode 100644 index 00000000000..eb3e3b92d66 --- /dev/null +++ b/packages/core/src/markdown.d.ts @@ -0,0 +1,4 @@ +declare module "*.md" { + const content: string + export default content +} diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index 8cf02ddfe05..52fff98733b 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -1,108 +1,33 @@ -import { DateTime, Schema } from "effect" -import { DateTimeUtcFromMillis } from "effect/Schema" +import { Types } from "effect" +import { Model } from "@opencode-ai/schema/model" import { ProviderV2 } from "./provider" -export const ID = Schema.String.pipe(Schema.brand("ModelV2.ID")) +export const ID = Model.ID export type ID = typeof ID.Type -export const VariantID = Schema.String.pipe(Schema.brand("VariantID")) +export const VariantID = Model.VariantID export type VariantID = typeof VariantID.Type // Grouping of models, eg claude opus, claude sonnet -export const Family = Schema.String.pipe(Schema.brand("Family")) -export type Family = typeof Family.Type +export const Family = Model.Family +export type Family = Model.Family -export const Capabilities = Schema.Struct({ - tools: Schema.Boolean, - // mime patterns, image, audio, video/*, text/* - input: Schema.String.pipe(Schema.Array), - output: Schema.String.pipe(Schema.Array), -}) -export type Capabilities = typeof Capabilities.Type +export const Capabilities = Model.Capabilities +export type Capabilities = Model.Capabilities -export const Cost = Schema.Struct({ - tier: Schema.Struct({ - type: Schema.Literal("context"), - size: Schema.Int, - }).pipe(Schema.optional), - input: Schema.Finite, - output: Schema.Finite, - cache: Schema.Struct({ - read: Schema.Finite, - write: Schema.Finite, - }), -}) +export const Cost = Model.Cost -export const Ref = Schema.Struct({ - id: ID, - providerID: ProviderV2.ID, - variant: VariantID.pipe(Schema.optional), -}) +export const Ref = Model.Ref export type Ref = typeof Ref.Type -export class Info extends Schema.Class("ModelV2.Info")({ - id: ID, - apiID: ID, - providerID: ProviderV2.ID, - family: Family.pipe(Schema.optional), - name: Schema.String, - endpoint: ProviderV2.Endpoint, - capabilities: Capabilities, - options: Schema.Struct({ - ...ProviderV2.Options.fields, - variant: Schema.String.pipe(Schema.optional), - }), - variants: Schema.Struct({ - id: VariantID, - ...ProviderV2.Options.fields, - }).pipe(Schema.Array), - time: Schema.Struct({ - released: DateTimeUtcFromMillis, - }), - cost: Cost.pipe(Schema.Array), - status: Schema.Literals(["alpha", "beta", "deprecated", "active"]), - enabled: Schema.Boolean, - limit: Schema.Struct({ - context: Schema.Int, - input: Schema.Int.pipe(Schema.optional), - output: Schema.Int, - }), -}) { - static empty(providerID: ProviderV2.ID, modelID: ID) { - return new Info({ - id: modelID, - apiID: modelID, - providerID, - name: modelID, - endpoint: { - type: "unknown", - }, - capabilities: { - tools: false, - input: [], - output: [], - }, - options: { - headers: {}, - body: {}, - aisdk: { - provider: {}, - request: {}, - }, - }, - variants: [], - time: { - released: DateTime.makeUnsafe(0), - }, - cost: [], - status: "active", - enabled: true, - limit: { - context: 0, - output: 0, - }, - }) - } +export const Api = Model.Api +export type Api = Model.Api + +export const Info = Model.Info +export type Info = Model.Info + +export type MutableInfo = Omit, "api"> & { + api: ProviderV2.MutableApi } export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } { diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index fec7e001c28..2a46d342f02 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -1,18 +1,27 @@ import path from "path" -import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect" +import { Context, Duration, Effect, Layer, Logger, Option, Schedule, Schema } from "effect" // kilocode_change import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" +import { ModelsDev } from "@opencode-ai/schema/models-dev" import { Global } from "./global" import { Flag } from "./flag/flag" import { Flock } from "./util/flock" import { Hash } from "./util/hash" -import { AppFileSystem } from "./filesystem" +import { FSUtil } from "./fs-util" import { InstallationChannel, InstallationVersion } from "./installation/version" import * as ModelsRefresh from "./kilocode/models-refresh" // kilocode_change import { EventV2 } from "./event" +import { makeGlobalNode } from "./effect/app-node" +import { httpClient } from "./effect/app-node-platform" +import { Observability } from "./observability" // kilocode_change export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"]) export type CatalogModelStatus = typeof CatalogModelStatus.Type +const InterleavedField = Schema.Union([ + Schema.Literals(["reasoning", "reasoning_content", "reasoning_text"]), + Schema.String, +]) + const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.KILO_CLIENT}` const CostTier = Schema.Struct({ @@ -42,6 +51,21 @@ const Cost = Schema.Struct({ ), }) +const ReasoningOption = Schema.Union([ + Schema.Struct({ + type: Schema.Literal("effort"), + values: Schema.Array(Schema.NullOr(Schema.String)), + }), + Schema.Struct({ + type: Schema.Literal("toggle"), + }), + Schema.Struct({ + type: Schema.Literal("budget_tokens"), + min: Schema.optional(Schema.Finite), + max: Schema.optional(Schema.Finite), + }), +]) + export const Model = Schema.Struct({ id: Schema.String, name: Schema.String, @@ -51,11 +75,13 @@ export const Model = Schema.Struct({ reasoning: Schema.Boolean, temperature: Schema.Boolean, tool_call: Schema.Boolean, + reasoning_options: Schema.optional(Schema.Array(ReasoningOption)), interleaved: Schema.optional( Schema.Union([ - Schema.Literal(true), + Schema.Boolean, + InterleavedField, Schema.Struct({ - field: Schema.Literals(["reasoning_content", "reasoning_details"]), + field: InterleavedField, }), ]), ), @@ -115,12 +141,7 @@ export const Provider = Schema.Struct({ export type Provider = Schema.Schema.Type -export const Event = { - Refreshed: EventV2.define({ - type: "models-dev.refreshed", - schema: {}, - }), -} +export const Event = ModelsDev.Event declare const KILO_MODELS_DEV: Record | undefined @@ -131,11 +152,12 @@ export interface Interface { export class Service extends Context.Service()("@opencode/ModelsDev") {} -export const layer = Layer.effect( +const layer = Layer.effect( Service, Effect.gen(function* () { - const fs = yield* AppFileSystem.Service + const fs = yield* FSUtil.Service const events = yield* EventV2.Service + const loggers = yield* Effect.service(Logger.CurrentLoggers) // kilocode_change const http = HttpClient.filterStatusOk( (yield* HttpClient.HttpClient).pipe( HttpClient.retryTransient({ @@ -146,10 +168,10 @@ export const layer = Layer.effect( ), ) - const source = Flag.KILO_MODELS_URL || "https://models.dev" + const source = Flag.KILO_MODELS_URL || "https://models.dev" // kilocode_change const filepath = path.join( Global.Path.cache, - source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`, + source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`, // kilocode_change ) const ttl = Duration.minutes(5) const lockKey = `models-dev:${filepath}` @@ -171,7 +193,12 @@ export const layer = Layer.effect( }) const loadFromDisk = fs.readJson(Flag.KILO_MODELS_PATH ?? filepath).pipe( - Effect.catch(() => Effect.succeed(undefined)), + Effect.catch((error) => { + if (Flag.KILO_MODELS_PATH === undefined && error._tag === "FileSystemError" && error.method === "readJson") { + return fs.remove(filepath, { force: true }).pipe(Effect.ignore, Effect.as(undefined)) + } + return Effect.succeed(undefined) + }), Effect.map((v) => v as Record | undefined), ) @@ -179,7 +206,16 @@ export const layer = Layer.effect( const fetchAndWrite = Effect.fn("ModelsDev.fetchAndWrite")(function* () { const text = yield* fetchApi() - yield* fs.writeWithDirs(filepath, text) + const tempfile = `${filepath}.${process.pid}.${Date.now()}.tmp` + yield* fs.writeWithDirs(tempfile, text).pipe( + Effect.andThen(fs.rename(tempfile, filepath)), + Effect.catch((error) => + Effect.gen(function* () { + yield* fs.remove(tempfile, { force: true }).pipe(Effect.ignore) + return yield* Effect.fail(error) + }), + ), + ) return text }) @@ -190,13 +226,19 @@ export const layer = Layer.effect( if (snapshot) return snapshot if (Flag.KILO_DISABLE_MODELS_FETCH) return {} // Flock is cross-process: concurrent opencode CLIs can race on this cache file. - const text = yield* Effect.scoped( + return yield* Effect.scoped( Effect.gen(function* () { yield* Flock.effect(lockKey) - return yield* fetchAndWrite() + // kilocode_change start - re-read under the lock: a concurrent refresh + // may already have recovered the corrupted cache while we waited, and + // fetching again here would duplicate the network call. + const rechecked = yield* loadFromDisk + if (rechecked) return rechecked + // kilocode_change end + const text = yield* fetchAndWrite() + return JSON.parse(text) as Record }), ) - return JSON.parse(text) as Record }).pipe(Effect.withSpan("ModelsDev.populate"), Effect.orDie) const [cachedGet, invalidate] = yield* Effect.cachedInvalidateWithTTL(populate, Duration.infinity) @@ -217,10 +259,9 @@ export const layer = Layer.effect( yield* events.publish(Event.Refreshed, {}) }), ).pipe( - Effect.tapCause((cause) => - Effect.logError("Failed to fetch models.dev").pipe(Effect.annotateLogs("cause", cause)), - ), + Effect.tapCause((cause) => Effect.logError("Failed to fetch models.dev", { cause: cause })), Effect.ignore, + Effect.provideService(Logger.CurrentLoggers, loggers), // kilocode_change ) }) @@ -233,10 +274,12 @@ export const layer = Layer.effect( }), ) -export const defaultLayer = layer.pipe( - Layer.provide(FetchHttpClient.layer), - Layer.provide(AppFileSystem.defaultLayer), - Layer.provide(EventV2.defaultLayer), -) +// kilocode_change start - capture file/OTLP loggers before the refresh fork +export const node = makeGlobalNode({ + service: Service, + layer: layer, + deps: [FSUtil.node, EventV2.node, httpClient, Observability.node], +}) +// kilocode_change end export * as ModelsDev from "./models-dev" diff --git a/packages/core/src/npm.ts b/packages/core/src/npm.ts index 8dac8faf012..30e12cff120 100644 --- a/packages/core/src/npm.ts +++ b/packages/core/src/npm.ts @@ -4,21 +4,24 @@ import path from "path" import npa from "npm-package-arg" import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect" import { NodeFileSystem } from "@effect/platform-node" -import { AppFileSystem } from "./filesystem" +import { FSUtil } from "./fs-util" import { Global } from "./global" import { EffectFlock } from "./util/effect-flock" +import { makeGlobalNode } from "./effect/app-node" +import { filesystem } from "./effect/app-node-platform" +import { LayerNode } from "./effect/layer-node" import { makeRuntime } from "./effect/runtime" import { NpmConfig } from "./npm-config" export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", { add: Schema.Array(Schema.String).pipe(Schema.optional), dir: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }) {} export interface EntryPoint { readonly directory: string - readonly entrypoint: Option.Option + readonly entrypoint?: string } export interface Interface { @@ -32,7 +35,7 @@ export interface Interface { }[] }, ) => Effect.Effect - readonly which: (pkg: string, bin?: string) => Effect.Effect> + readonly which: (pkg: string, bin?: string) => Effect.Effect } export class Service extends Context.Service()("@opencode/Npm") {} @@ -45,12 +48,11 @@ export function sanitize(pkg: string) { } const resolveEntryPoint = (name: string, dir: string): EntryPoint => { - let entrypoint: Option.Option + let entrypoint: string | undefined try { - const resolved = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir) - entrypoint = Option.some(resolved) + entrypoint = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir) } catch { - entrypoint = Option.none() + entrypoint = undefined } return { directory: dir, @@ -67,10 +69,10 @@ interface ArboristTree { edgesOut: Map } -export const layer = Layer.effect( +const layer = Layer.effect( Service, Effect.gen(function* () { - const afs = yield* AppFileSystem.Service + const afs = yield* FSUtil.Service const global = yield* Global.Service const fs = yield* FileSystem.FileSystem const flock = yield* EffectFlock.Service @@ -128,7 +130,7 @@ export const layer = Layer.effect( const first = tree.edgesOut.values().next().value?.to if (!first) { const result = resolveEntryPoint(name, path.join(dir, "node_modules", name)) - if (Option.isSome(result.entrypoint)) return result + if (result.entrypoint) return result return yield* new InstallFailedError({ add: [pkg], dir }) } return resolveEntryPoint(first.name, first.path) @@ -217,22 +219,24 @@ export const layer = Layer.effect( return Option.some(files[0]) }) - return yield* Effect.gen(function* () { - const bin = yield* pick() - if (Option.isSome(bin)) { - return Option.some(path.join(binDir, bin.value)) - } + return Option.getOrUndefined( + yield* Effect.gen(function* () { + const bin = yield* pick() + if (Option.isSome(bin)) { + return Option.some(path.join(binDir, bin.value)) + } - yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {})) + yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {})) - yield* add(pkg) + yield* add(pkg) - const resolved = yield* pick() - if (Option.isNone(resolved)) return Option.none() - return Option.some(path.join(binDir, resolved.value)) - }).pipe( - Effect.scoped, - Effect.orElseSucceed(() => Option.none()), + const resolved = yield* pick() + if (Option.isNone(resolved)) return Option.none() + return Option.some(path.join(binDir, resolved.value)) + }).pipe( + Effect.scoped, + Effect.orElseSucceed(() => Option.none()), + ), ) }) @@ -244,28 +248,22 @@ export const layer = Layer.effect( }), ) -export const defaultLayer = layer.pipe( - Layer.provide(EffectFlock.layer), - Layer.provide(AppFileSystem.layer), - Layer.provide(Global.layer), - Layer.provide(NodeFileSystem.layer), -) +export const node = makeGlobalNode({ + service: Service, + layer: layer, + deps: [FSUtil.node, Global.node, filesystem, EffectFlock.node], +}) -const { runPromise } = makeRuntime(Service, defaultLayer) +const { runPromise } = makeRuntime(Service, LayerNode.compile(node)) export async function install(...args: Parameters) { return runPromise((svc) => svc.install(...args)) } export async function add(...args: Parameters) { - const entry = await runPromise((svc) => svc.add(...args)) - return { - directory: entry.directory, - entrypoint: Option.getOrUndefined(entry.entrypoint), - } + return runPromise((svc) => svc.add(...args)) } export async function which(...args: Parameters) { - const resolved = await runPromise((svc) => svc.which(...args)) - return Option.getOrUndefined(resolved) + return runPromise((svc) => svc.which(...args)) } diff --git a/packages/core/src/oauth/page.ts b/packages/core/src/oauth/page.ts new file mode 100644 index 00000000000..5d3e29f67bd --- /dev/null +++ b/packages/core/src/oauth/page.ts @@ -0,0 +1,276 @@ +// Branded HTML pages for local OAuth callback servers. +// +// These are served by the loopback HTTP servers that finish an OAuth exchange +// (MCP, Codex/ChatGPT, xAI, Snowflake, DigitalOcean, ...). The functions return +// a fully self-contained HTML string with no external assets, so they work +// offline and drop into any transport (`res.end(...)`, Effect `response.end`, +// etc.). +// +// The visual language mirrors the OpenCode app: the design tokens are a curated +// subset of the OC-2 semantic tokens in `packages/ui/src/styles/theme.css`, and +// the wordmark is the same geometry as `packages/ui/src/components/logo.tsx`. +// Keep this file in sync with those sources when the brand changes. + +export interface CallbackPageOptions { + /** Friendly integration name shown as a subtitle, e.g. "xAI", "Snowflake", "MCP". */ + provider?: string + /** Attempt to close the window shortly after success. Defaults to true. */ + autoClose?: boolean +} + +export function success(options?: CallbackPageOptions) { + const provider = options?.provider + return renderDocument({ + title: "Authorization successful", + body: renderCard({ + status: "success", + headline: "Authorization successful", + message: provider ? `OpenCode is now connected to ${escapeHtml(provider)}.` : "OpenCode is now authorized.", + footnote: "You can close this window.", + }), + script: options?.autoClose === false ? undefined : AUTO_CLOSE_SCRIPT, + }) +} + +export function error(detail: string, options?: CallbackPageOptions) { + const provider = options?.provider + return renderDocument({ + title: "Authorization failed", + body: renderCard({ + status: "error", + headline: "Authorization failed", + message: provider + ? `OpenCode couldn't finish connecting to ${escapeHtml(provider)}.` + : "OpenCode couldn't complete authorization.", + detail, + footnote: "Close this window and try again from OpenCode.", + }), + }) +} + +export interface BootstrapOptions { + /** Same-origin path the in-browser script POSTs the parsed callback to. */ + tokenPath: string + provider?: string +} + +// For flows where the credential arrives in the URL fragment (implicit grant), +// the browser must relay it back to the loopback server. This renders a pending +// page whose script reads the fragment, POSTs it to `tokenPath`, then resolves +// to the success or error state in place. +export function bootstrap(options: BootstrapOptions) { + return renderDocument({ + title: "Finishing sign-in", + body: renderCard({ + status: "pending", + headline: "Finishing sign-in", + message: options.provider + ? `Completing your ${escapeHtml(options.provider)} authorization.` + : "Completing authorization.", + footnote: "You can close this window once sign-in finishes.", + }), + script: bootstrapScript(options), + }) +} + +export * as OauthCallbackPage from "./page" + +type Status = "pending" | "success" | "error" + +function renderCard(input: { status: Status; headline: string; message: string; detail?: string; footnote: string }) { + const detail = input.detail?.trim() + return `
+
${WORDMARK}
+ +

${escapeHtml(input.headline)}

+

${input.message}

+
${detail ? escapeHtml(detail) : ""}
+

${escapeHtml(input.footnote)}

+ ` +} + +function renderDocument(input: { title: string; body: string; script?: string }) { + return ` + + + + + + ${escapeHtml(input.title)} · OpenCode + + + + ${input.body}${input.script ? `\n ` : ""} + +` +} + +const AUTO_CLOSE_SCRIPT = `setTimeout(function(){try{window.close()}catch(e){}},2500)` + +function bootstrapScript(options: BootstrapOptions) { + return `var PROVIDER=${scriptString(options.provider ?? "")}; +var TOKEN_URL=new URL(${scriptString(options.tokenPath)},window.location.origin).href; +(function(){ + var card=document.getElementById("oc-card"),headline=document.getElementById("oc-headline"),message=document.getElementById("oc-message"),detail=document.getElementById("oc-detail"),footnote=document.getElementById("oc-footnote"); + function fail(text){card.dataset.status="error";headline.textContent="Authorization failed";message.textContent=PROVIDER?("OpenCode couldn't finish connecting to "+PROVIDER+"."):"OpenCode couldn't complete authorization.";if(text){detail.textContent=text;detail.hidden=false}footnote.textContent="Close this window and try again from OpenCode."} + function ok(){card.dataset.status="success";headline.textContent="Authorization successful";message.textContent=PROVIDER?("OpenCode is now connected to "+PROVIDER+"."):"OpenCode is now authorized.";detail.hidden=true;footnote.textContent="You can close this window.";setTimeout(function(){try{window.close()}catch(e){}},2500)} + try{ + var hash=new URLSearchParams((window.location.hash||"").slice(1)); + var search=new URLSearchParams(window.location.search||""); + var err=hash.get("error")||search.get("error"); + var errDescription=hash.get("error_description")||search.get("error_description"); + var body=err?{error:err,error_description:errDescription||""}:{access_token:hash.get("access_token")||"",expires_in:hash.get("expires_in")||"0",state:hash.get("state")||""}; + fetch(TOKEN_URL,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)}).then(function(res){ + if(!res.ok)return res.text().catch(function(){return""}).then(function(t){throw new Error(t||("callback failed ("+res.status+")"))}); + if(err){fail(errDescription||err);return} + ok(); + }).catch(function(e){fail(String(e&&e.message?e.message:e))}); + }catch(e){fail(String(e&&e.message?e.message:e))} +})()` +} + +function scriptString(value: string) { + return JSON.stringify(value).replaceAll("<", "\\u003c") +} + +function escapeHtml(value: string) { + return value + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'") +} + +// Curated subset of OC-2 tokens (packages/ui/src/styles/theme.css). Default is +// light; dark applies via prefers-color-scheme. The [data-theme] selectors let a +// host force a scheme without changing the default. +const LIGHT_VARS = ` + --oc-bg: #f8f8f8; + --oc-card: #fcfcfc; + --oc-text-strong: #171717; + --oc-text-base: #6f6f6f; + --oc-text-weak: #8f8f8f; + --oc-border-weak: #e5e5e5; + --oc-icon-strong: #171717; + --oc-icon-base: #8f8f8f; + --oc-icon-weak: #dbdbdb; + --oc-success: #2dba26; + --oc-error: #ed4831; + --oc-detail-bg: #fff8f6; + --oc-detail-border: #fdc3b7; + --oc-shadow: 0 16px 48px -6px rgba(0,0,0,.10), 0 6px 12px -2px rgba(0,0,0,.05), 0 1px 2px rgba(0,0,0,.06);` + +const DARK_VARS = ` + --oc-bg: #101010; + --oc-card: #161616; + --oc-text-strong: rgba(255,255,255,.936); + --oc-text-base: rgba(255,255,255,.618); + --oc-text-weak: rgba(255,255,255,.422); + --oc-border-weak: #282828; + --oc-icon-strong: #ededed; + --oc-icon-base: #7e7e7e; + --oc-icon-weak: #343434; + --oc-success: #12c905; + --oc-error: #fc533a; + --oc-detail-bg: #28110c; + --oc-detail-border: #6a1206; + --oc-shadow: 0 16px 48px -6px rgba(0,0,0,.55), 0 6px 12px -2px rgba(0,0,0,.35), 0 1px 2px rgba(0,0,0,.4);` + +const STYLES = ` + :root { color-scheme: light dark;${LIGHT_VARS} + --oc-font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --oc-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + } + @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) {${DARK_VARS} } } + :root[data-theme="dark"] {${DARK_VARS} } + :root[data-theme="light"] {${LIGHT_VARS} } + + * { box-sizing: border-box; } + html, body { margin: 0; height: 100%; } + body { + min-height: 100vh; + display: grid; + place-items: center; + padding: 24px; + background: var(--oc-bg); + color: var(--oc-text-base); + font-family: var(--oc-font-sans); + line-height: 1.5; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + } + .card { + width: min(100%, 28rem); + padding: 2.25rem 2rem 1.75rem; + background: var(--oc-card); + border: 1px solid var(--oc-border-weak); + border-radius: 14px; + box-shadow: var(--oc-shadow); + text-align: center; + } + .brand { display: flex; justify-content: center; margin-bottom: 1.75rem; } + .brand svg { height: 19px; width: auto; } + .status { display: flex; justify-content: center; margin-bottom: 1.125rem; } + .icon { display: none; line-height: 0; } + .icon svg { display: block; } + .card[data-status="pending"] .icon-pending, + .card[data-status="success"] .icon-success, + .card[data-status="error"] .icon-error { display: block; } + .icon-success { color: var(--oc-success); } + .icon-error { color: var(--oc-error); } + .icon-pending { color: var(--oc-text-weak); } + .headline { margin: 0; font-size: 1.1875rem; font-weight: 500; line-height: 1.3; letter-spacing: -0.012em; color: var(--oc-text-strong); } + .message { margin: 0.5rem 0 0; font-size: 0.9375rem; color: var(--oc-text-base); } + .detail { + margin: 1.25rem 0 0; + padding: 0.75rem 0.875rem; + text-align: left; + font-family: var(--oc-font-mono); + font-size: 0.8125rem; + line-height: 1.55; + color: var(--oc-text-strong); + background: var(--oc-detail-bg); + border: 1px solid var(--oc-detail-border); + border-radius: 8px; + white-space: pre-wrap; + word-break: break-word; + max-height: 9.5rem; + overflow: auto; + } + .detail[hidden] { display: none; } + .footnote { margin: 1.5rem 0 0; font-size: 0.8125rem; color: var(--oc-text-weak); } + .spinner { animation: oc-spin 0.8s linear infinite; transform-origin: center; } + @keyframes oc-spin { to { transform: rotate(360deg); } } + @media (prefers-reduced-motion: reduce) { .spinner { animation: none; } } +` + +// OpenCode wordmark — same path geometry as packages/ui/src/components/logo.tsx (Logo). +const WORDMARK = ` + + + + + + + + + + + + + + + + + ` + +const ICON_CHECK = `` + +const ICON_CROSS = `` + +const ICON_SPINNER = `` diff --git a/packages/core/src/observability.ts b/packages/core/src/observability.ts new file mode 100644 index 00000000000..22285974d83 --- /dev/null +++ b/packages/core/src/observability.ts @@ -0,0 +1,24 @@ +export * as Observability from "./observability" + +import { NodeFileSystem } from "@effect/platform-node" +import { LayerNode } from "./effect/layer-node" +import { Effect, Layer, Logger, References } from "effect" +import { FetchHttpClient } from "effect/unstable/http" +import { OtlpSerialization } from "effect/unstable/observability" +import { Logging } from "./observability/logging" +import { Otlp } from "./observability/otlp" + +export const layer = Layer.unwrap( + Effect.gen(function* () { + const logs = Logger.layer([...Logging.loggers(), ...Otlp.loggers()], { mergeWithExisting: false }).pipe( + Layer.provide(NodeFileSystem.layer), + Layer.provide(OtlpSerialization.layerJson), + Layer.provide(FetchHttpClient.layer), + Layer.orDie, + Layer.merge(Layer.succeed(References.MinimumLogLevel, Logging.minimumLogLevel())), + ) + return Layer.merge(logs, yield* Effect.promise(Otlp.tracingLayer)) + }), +) + +export const node = LayerNode.make({ name: "observability", layer, deps: [] }) diff --git a/packages/core/src/observability/logging.ts b/packages/core/src/observability/logging.ts new file mode 100644 index 00000000000..e8576fcd9f2 --- /dev/null +++ b/packages/core/src/observability/logging.ts @@ -0,0 +1,71 @@ +import { Formatter, Logger, type LogLevel } from "effect" +import path from "path" +import { Global } from "../global" +import { runID } from "./shared" + +function formatter(id: string = runID) { + return Logger.map(Logger.formatStructured, (output) => { + const messages = Array.isArray(output.message) ? output.message : [output.message] + return [ + ["timestamp", output.timestamp], + ["level", output.level], + ["run", id], + ...messages.flatMap((value) => (plain(value) ? flatten(value) : [["message", value] as const])), + ...(output.cause === undefined ? [] : [["cause", output.cause] as const]), + ...flatten(output.spans), + ...flatten(output.annotations), + ] + .map(([key, value]) => `${key}=${format(value)}`) + .join(" ") + }) +} + +function flatten( + input: Record, + prefix = "", + seen = new WeakSet(), +): Array { + if (seen.has(input)) return [[prefix, "[Circular]"]] + seen.add(input) + const entries = Object.entries(input) + if (entries.length === 0 && prefix) return [[prefix, input]] + return entries.flatMap(([key, value]) => { + const path = prefix ? `${prefix}.${key}` : key + return plain(value) ? flatten(value, path, seen) : [[path, value] as const] + }) +} + +function plain(input: unknown): input is Record { + if (input === null || typeof input !== "object" || Array.isArray(input)) return false + const prototype = Object.getPrototypeOf(input) + return prototype === Object.prototype || prototype === null +} + +function format(input: unknown) { + const value = typeof input === "string" ? input : Formatter.format(input) + return /^[^\s="\\]+$/.test(value) ? value : JSON.stringify(value) +} + +export function fileLogger(file = path.join(Global.Path.log, "opencode.log"), id: string = runID) { + // Do not set batchWindow to 0; it causes high idle CPU usage. + return Logger.toFile(formatter(id), file, { flag: "a" }) +} + +const stderrLogger = Logger.make((options) => process.stderr.write(formatter().log(options) + "\n")) + +export function minimumLogLevel() { + const value = process.env.KILO_LOG_LEVEL?.toUpperCase() + const levels = { + DEBUG: "Debug", + INFO: "Info", + WARN: "Warn", + ERROR: "Error", + } as const satisfies Record + return value && value in levels ? levels[value as keyof typeof levels] : levels.INFO +} + +export function loggers() { + return process.env.KILO_PRINT_LOGS === "1" ? [fileLogger(), stderrLogger] : [fileLogger()] +} + +export * as Logging from "./logging" diff --git a/packages/core/src/observability/otlp.ts b/packages/core/src/observability/otlp.ts new file mode 100644 index 00000000000..f2f49015fa8 --- /dev/null +++ b/packages/core/src/observability/otlp.ts @@ -0,0 +1,79 @@ +import { Layer } from "effect" +import { OtlpLogger } from "effect/unstable/observability" +import { Flag } from "../flag/flag" +import { InstallationChannel, InstallationVersion } from "../installation/version" +import { runID } from "./shared" + +const endpoint = Flag.OTEL_EXPORTER_OTLP_ENDPOINT + +const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS + ? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce( + (acc, entry) => { + const [key, ...value] = entry.split("=") + acc[key] = value.join("=") + return acc + }, + {} as Record, + ) + : undefined + +function resourceAttributes() { + const value = process.env.OTEL_RESOURCE_ATTRIBUTES + if (!value) return {} + try { + return Object.fromEntries( + value.split(",").map((entry) => { + const index = entry.indexOf("=") + if (index < 1) throw new Error("Invalid OTEL_RESOURCE_ATTRIBUTES entry") + return [decodeURIComponent(entry.slice(0, index)), decodeURIComponent(entry.slice(index + 1))] + }), + ) + } catch { + return {} + } +} + +export function resource(): { serviceName: string; serviceVersion: string; attributes: Record } { + return { + serviceName: "opencode", + serviceVersion: InstallationVersion, + attributes: { + ...resourceAttributes(), + "deployment.environment.name": InstallationChannel, + "opencode.client": Flag.KILO_CLIENT, + "opencode.run": runID, + "service.instance.id": runID, + }, + } +} + +export function loggers() { + if (!endpoint) return [] + return [OtlpLogger.make({ url: `${endpoint}/v1/logs`, resource: resource(), headers })] +} + +export async function tracingLayer() { + if (!endpoint) return Layer.empty + const NodeSdk = await import("@effect/opentelemetry/NodeSdk") + const OTLP = await import("@opentelemetry/exporter-trace-otlp-http") + const SdkBase = await import("@opentelemetry/sdk-trace-base") + const { AsyncLocalStorageContextManager } = await import("@opentelemetry/context-async-hooks") + const { context } = await import("@opentelemetry/api") + + // The Effect Node SDK does not register a global context manager, but the AI SDK uses it to parent spans. + const manager = new AsyncLocalStorageContextManager() + manager.enable() + context.setGlobalContextManager(manager) + + return NodeSdk.layer(() => ({ + resource: resource(), + spanProcessor: new SdkBase.BatchSpanProcessor( + new OTLP.OTLPTraceExporter({ + url: `${endpoint}/v1/traces`, + headers, + }), + ), + })) +} + +export * as Otlp from "./otlp" diff --git a/packages/core/src/observability/shared.ts b/packages/core/src/observability/shared.ts new file mode 100644 index 00000000000..76393aacce5 --- /dev/null +++ b/packages/core/src/observability/shared.ts @@ -0,0 +1 @@ +export const runID = crypto.randomUUID().slice(0, 8) diff --git a/packages/core/src/patch.ts b/packages/core/src/patch.ts new file mode 100644 index 00000000000..a4370d44aac --- /dev/null +++ b/packages/core/src/patch.ts @@ -0,0 +1,197 @@ +export * as Patch from "./patch" + +export type Hunk = + | { readonly type: "add"; readonly path: string; readonly contents: string } + | { readonly type: "delete"; readonly path: string } + | { + readonly type: "update" + readonly path: string + readonly movePath?: string + readonly chunks: ReadonlyArray + } + +export interface UpdateFileChunk { + readonly oldLines: ReadonlyArray + readonly newLines: ReadonlyArray + readonly changeContext?: string + readonly endOfFile?: boolean +} + +export interface FileUpdate { + readonly content: string + readonly bom: boolean +} + +export function parse(patchText: string): ReadonlyArray { + const lines = stripHeredoc(patchText.trim()).split("\n") + const begin = lines.findIndex((line) => line.trim() === "*** Begin Patch") + const end = lines.findIndex((line) => line.trim() === "*** End Patch") + if (begin === -1 || end === -1 || begin >= end) throw new Error("Invalid patch format: missing Begin/End markers") + + const hunks: Hunk[] = [] + let index = begin + 1 + while (index < end) { + const line = lines[index]! + if (line.startsWith("*** Add File:")) { + const path = line.slice("*** Add File:".length).trim() + if (!path) throw new Error("Invalid add file path") + const parsed = parseAdd(lines, index + 1) + hunks.push({ type: "add", path, contents: parsed.content }) + index = parsed.next + continue + } + if (line.startsWith("*** Delete File:")) { + const path = line.slice("*** Delete File:".length).trim() + if (!path) throw new Error("Invalid delete file path") + hunks.push({ type: "delete", path }) + index++ + continue + } + if (line.startsWith("*** Update File:")) { + const path = line.slice("*** Update File:".length).trim() + if (!path) throw new Error("Invalid update file path") + let next = index + 1 + let movePath: string | undefined + if (lines[next]?.startsWith("*** Move to:")) { + movePath = lines[next]!.slice("*** Move to:".length).trim() + if (!movePath) throw new Error("Invalid move file path") + next++ + } + const parsed = parseUpdate(lines, next) + if (parsed.chunks.length === 0) throw new Error(`Invalid update hunk for ${path}: expected at least one @@ chunk`) + hunks.push({ type: "update", path, movePath, chunks: parsed.chunks }) + index = parsed.next + continue + } + throw new Error(`Invalid patch line: ${line}`) + } + return hunks +} + +export function derive(path: string, chunks: ReadonlyArray, original: string): FileUpdate { + const source = splitBom(original) + const lines = source.text.split("\n") + if (lines.at(-1) === "") lines.pop() + const replacements = computeReplacements(lines, path, chunks) + const updated = [...lines] + for (const [start, remove, insert] of replacements.toReversed()) updated.splice(start, remove, ...insert) + if (updated.at(-1) !== "") updated.push("") + const next = splitBom(updated.join("\n")) + return { content: next.text, bom: source.bom || next.bom } +} + +export function joinBom(text: string, bom: boolean) { + const stripped = splitBom(text).text + return bom ? `\uFEFF${stripped}` : stripped +} + +function parseAdd(lines: ReadonlyArray, start: number) { + const content: string[] = [] + let index = start + while (index < lines.length && !lines[index]!.startsWith("***")) { + if (!lines[index]!.startsWith("+")) throw new Error(`Invalid add file line: ${lines[index]}`) + content.push(lines[index]!.slice(1)) + index++ + } + return { content: content.join("\n"), next: index } +} + +function parseUpdate(lines: ReadonlyArray, start: number) { + const chunks: UpdateFileChunk[] = [] + let index = start + while (index < lines.length && !lines[index]!.startsWith("***")) { + if (!lines[index]!.startsWith("@@")) { + throw new Error(`Invalid update file line: ${lines[index]}`) + } + const changeContext = lines[index]!.slice(2).trim() || undefined + const oldLines: string[] = [] + const newLines: string[] = [] + let endOfFile = false + index++ + while (index < lines.length && !lines[index]!.startsWith("@@")) { + const line = lines[index]! + if (line === "*** End of File") { + endOfFile = true + index++ + break + } + if (line.startsWith("***")) break + if (line.startsWith(" ")) { + oldLines.push(line.slice(1)) + newLines.push(line.slice(1)) + } else if (line.startsWith("-")) oldLines.push(line.slice(1)) + else if (line.startsWith("+")) newLines.push(line.slice(1)) + else throw new Error(`Invalid update chunk line: ${line}`) + index++ + } + chunks.push({ oldLines, newLines, changeContext, endOfFile: endOfFile || undefined }) + } + return { chunks, next: index } +} + +function computeReplacements(lines: ReadonlyArray, path: string, chunks: ReadonlyArray) { + const replacements: Array]> = [] + let lineIndex = 0 + for (const chunk of chunks) { + if (chunk.changeContext) { + const context = seek(lines, [chunk.changeContext], lineIndex) + if (context === -1) throw new Error(`Failed to find context '${chunk.changeContext}' in ${path}`) + lineIndex = context + 1 + } + if (chunk.oldLines.length === 0) { + replacements.push([lines.length, 0, chunk.newLines]) + continue + } + let oldLines = chunk.oldLines + let newLines = chunk.newLines + let found = seek(lines, oldLines, lineIndex, chunk.endOfFile) + if (found === -1 && oldLines.at(-1) === "") { + oldLines = oldLines.slice(0, -1) + if (newLines.at(-1) === "") newLines = newLines.slice(0, -1) + found = seek(lines, oldLines, lineIndex, chunk.endOfFile) + } + if (found === -1) throw new Error(`Failed to find expected lines in ${path}:\n${chunk.oldLines.join("\n")}`) + replacements.push([found, oldLines.length, newLines]) + lineIndex = found + oldLines.length + } + return replacements.toSorted((left, right) => left[0] - right[0]) +} + +function seek(lines: ReadonlyArray, pattern: ReadonlyArray, start: number, eof = false) { + if (pattern.length === 0) return -1 + for (const compare of [exact, rstrip, trim, normalized]) { + if (eof) { + const offset = lines.length - pattern.length + if (offset >= start && matches(lines, pattern, offset, compare)) return offset + } + for (let offset = start; offset <= lines.length - pattern.length; offset++) { + if (matches(lines, pattern, offset, compare)) return offset + } + } + return -1 +} + +function matches( + lines: ReadonlyArray, + pattern: ReadonlyArray, + offset: number, + compare: (left: string, right: string) => boolean, +) { + return pattern.every((line, index) => compare(lines[offset + index]!, line)) +} + +const exact = (left: string, right: string) => left === right +const rstrip = (left: string, right: string) => left.trimEnd() === right.trimEnd() +const trim = (left: string, right: string) => left.trim() === right.trim() +const normalized = (left: string, right: string) => normalize(left.trim()) === normalize(right.trim()) +const normalize = (value: string) => + value + .replace(/[‘’‚‛]/g, "'") + .replace(/[“”„‟]/g, '"') + .replace(/[‐‑‒–—―]/g, "-") + .replace(/…/g, "...") + .replace(/ /g, " ") +const splitBom = (text: string) => + text.startsWith("\uFEFF") ? { bom: true, text: text.slice(1) } : { bom: false, text } +const stripHeredoc = (input: string) => + input.match(/^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/)?.[2] ?? input diff --git a/packages/core/src/permission.ts b/packages/core/src/permission.ts index ec8038f7134..3f28632a034 100644 --- a/packages/core/src/permission.ts +++ b/packages/core/src/permission.ts @@ -1,45 +1,310 @@ export * as PermissionV2 from "./permission" -import { Schema } from "effect" +import { makeLocationNode } from "./effect/app-node" +import { Context, Deferred, Effect as EffectRuntime, Layer, Schema } from "effect" +import { Permission } from "@opencode-ai/schema/permission" +import { EventV2 } from "./event" +import { Location } from "./location" +import { AgentV2 } from "./agent" +import { SessionV2 } from "./session" +import { SessionStore } from "./session/store" import { Wildcard } from "./util/wildcard" +import { PermissionSaved } from "./permission/saved" -export const Action = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionV2.Action" }) -export type Action = typeof Action.Type +export { Effect, Rule, Ruleset } from "@opencode-ai/schema/permission" +const missingAgentPermissions: Permission.Ruleset = [{ action: "*", resource: "*", effect: "deny" }] -export const Rule = Schema.Struct({ - permission: Schema.String, - pattern: Schema.String, - action: Action, -}).annotate({ identifier: "PermissionV2.Rule" }) -export type Rule = typeof Rule.Type +export const ID = Permission.ID +export type ID = typeof ID.Type -export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionV2.Ruleset" }) -export type Ruleset = typeof Ruleset.Type +export const Source = Permission.Source +export type Source = typeof Source.Type -const EDIT_TOOLS = ["edit", "write", "apply_patch"] +const RequestFields = { + sessionID: Permission.Request.fields.sessionID, + action: Permission.Request.fields.action, + resources: Permission.Request.fields.resources, + save: Permission.Request.fields.save, + metadata: Permission.Request.fields.metadata, + source: Permission.Request.fields.source, +} + +export const Request = Permission.Request +export type Request = typeof Request.Type + +export const Reply = Permission.Reply +export type Reply = typeof Reply.Type + +export const AssertInput = Schema.Struct({ + id: ID.pipe(Schema.optional), + ...RequestFields, + agent: AgentV2.ID.pipe(Schema.optional), +}).annotate({ identifier: "PermissionV2.AssertInput" }) +export type AssertInput = typeof AssertInput.Type + +export const ReplyInput = Schema.Struct({ + requestID: ID, + reply: Reply, + message: Schema.String.pipe(Schema.optional), +}).annotate({ identifier: "PermissionV2.ReplyInput" }) +export type ReplyInput = typeof ReplyInput.Type + +export const AskResult = Schema.Struct({ + id: ID, + effect: Permission.Effect, +}).annotate({ identifier: "PermissionV2.AskResult" }) +export type AskResult = typeof AskResult.Type + +export const Event = Permission.Event + +export class DeclinedError extends Schema.TaggedErrorClass()("PermissionV2.DeclinedError", {}) {} + +export class CorrectedError extends Schema.TaggedErrorClass()("PermissionV2.CorrectedError", { + feedback: Schema.String, +}) {} + +export class BlockedError extends Schema.TaggedErrorClass()("PermissionV2.BlockedError", { + rules: Permission.Ruleset, +}) {} -export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule { +export class NotFoundError extends Schema.TaggedErrorClass()("PermissionV2.NotFoundError", { + requestID: ID, +}) {} + +export type Error = BlockedError | CorrectedError + +export function evaluate(action: string, resource: string, ...rulesets: Permission.Ruleset[]): Permission.Rule { return ( rulesets .flat() - .findLast((rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern)) ?? { - action: "ask", - permission, - pattern: "*", + .findLast((rule) => Wildcard.match(action, rule.action) && Wildcard.match(resource, rule.resource)) ?? { + action, + resource: "*", + effect: "ask", } ) } -export function merge(...rulesets: Ruleset[]): Ruleset { +export function merge(...rulesets: Permission.Ruleset[]): Permission.Ruleset { return rulesets.flat() } -export function disabled(tools: string[], ruleset: Ruleset): Set { - return new Set( - tools.filter((tool) => { - const permission = EDIT_TOOLS.includes(tool) ? "edit" : tool - const rule = ruleset.findLast((rule) => Wildcard.match(permission, rule.permission)) - return rule?.pattern === "*" && rule.action === "deny" - }), - ) +export interface Interface { + readonly ask: (input: AssertInput) => EffectRuntime.Effect + readonly assert: (input: AssertInput) => EffectRuntime.Effect + readonly reply: (input: ReplyInput) => EffectRuntime.Effect + readonly get: (id: ID) => EffectRuntime.Effect + readonly forSession: (sessionID: SessionV2.ID) => EffectRuntime.Effect> + readonly list: () => EffectRuntime.Effect> +} + +export class Service extends Context.Service()("@opencode/v2/Permission") {} + +interface Pending { + readonly request: Request + readonly agent?: AgentV2.ID + readonly deferred: Deferred.Deferred } + +const layer = Layer.effect( + Service, + EffectRuntime.gen(function* () { + const events = yield* EventV2.Service + const location = yield* Location.Service + const agents = yield* AgentV2.Service + const sessions = yield* SessionStore.Service + const saved = yield* PermissionSaved.Service + const pending = new Map() + + yield* EffectRuntime.addFinalizer(() => + EffectRuntime.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new DeclinedError()), { + discard: true, + }).pipe( + EffectRuntime.ensuring( + EffectRuntime.sync(() => { + pending.clear() + }), + ), + ), + ) + + const savedRules = EffectRuntime.fnUntraced(function* () { + return (yield* saved.list({ projectID: location.project.id })).map( + (item): Permission.Rule => ({ action: item.action, resource: item.resource, effect: "allow" }), + ) + }) + + const configured = EffectRuntime.fn("PermissionV2.configured")(function* ( + sessionID: SessionV2.ID, + agentID?: AgentV2.ID, + ) { + const session = yield* sessions.get(sessionID) + if (!session) return yield* new SessionV2.NotFoundError({ sessionID }) + const agent = yield* agents.resolve(agentID ?? session.agent) + return agent?.permissions ?? missingAgentPermissions + }) + + function denied(input: AssertInput, rules: Permission.Ruleset) { + return input.resources.some((resource) => evaluate(input.action, resource, rules).effect === "deny") + } + + function relevant(input: AssertInput, rules: Permission.Ruleset) { + return rules.filter((rule) => Wildcard.match(input.action, rule.action)) + } + + const evaluateInput = EffectRuntime.fnUntraced(function* (input: AssertInput) { + const rules = yield* configured(input.sessionID, input.agent) + if (denied(input, rules)) return { effect: "deny" as const, rules } + const all = [...rules, ...(yield* savedRules())] + const effects = input.resources.map((resource) => evaluate(input.action, resource, all).effect) + const effect: Permission.Effect = effects.includes("deny") ? "deny" : effects.includes("ask") ? "ask" : "allow" + return { effect, rules: all } + }) + + function request(input: AssertInput): Request { + return { + id: input.id ?? ID.create(), + sessionID: input.sessionID, + action: input.action, + resources: input.resources, + save: input.save, + metadata: input.metadata, + source: input.source, + } + } + + const create = (request: Request, agent?: AgentV2.ID) => + EffectRuntime.uninterruptible( + EffectRuntime.gen(function* () { + const deferred = yield* Deferred.make() + const item = { request, agent, deferred } + if (pending.has(request.id)) return yield* EffectRuntime.die(`Duplicate pending permission ID: ${request.id}`) + pending.set(request.id, item) + yield* events + .publish(Event.Asked, request) + .pipe(EffectRuntime.onError(() => EffectRuntime.sync(() => pending.delete(request.id)))) + return item + }), + ) + + const ask = EffectRuntime.fn("PermissionV2.ask")(function* (input: AssertInput) { + const result = yield* evaluateInput(input) + const value = request(input) + if (result.effect === "ask") yield* create(value, input.agent) + return { id: value.id, effect: result.effect } + }) + + const assert = EffectRuntime.fn("PermissionV2.assert")((input: AssertInput) => + EffectRuntime.uninterruptibleMask((restore) => + EffectRuntime.gen(function* () { + const result = yield* evaluateInput(input) + if (result.effect === "deny") { + return yield* new BlockedError({ + rules: relevant(input, result.rules), + }) + } + if (result.effect === "allow") return + const item = yield* create(request(input), input.agent) + return yield* restore(Deferred.await(item.deferred)).pipe( + EffectRuntime.catchTag("PermissionV2.DeclinedError", (error) => EffectRuntime.die(error)), + EffectRuntime.ensuring( + EffectRuntime.sync(() => { + pending.delete(item.request.id) + }), + ), + ) + }), + ), + ) + + const reply = EffectRuntime.fn("PermissionV2.reply")((input: ReplyInput) => + EffectRuntime.uninterruptible( + EffectRuntime.gen(function* () { + const existing = pending.get(input.requestID) + if (!existing) return yield* new NotFoundError({ requestID: input.requestID }) + yield* events.publish(Event.Replied, { + sessionID: existing.request.sessionID, + requestID: existing.request.id, + reply: input.reply, + }) + + if (input.reply === "reject") { + yield* Deferred.fail( + existing.deferred, + input.message ? new CorrectedError({ feedback: input.message }) : new DeclinedError(), + ) + pending.delete(input.requestID) + for (const [id, item] of pending) { + if (item.request.sessionID !== existing.request.sessionID) continue + yield* events.publish(Event.Replied, { + sessionID: item.request.sessionID, + requestID: item.request.id, + reply: "reject", + }) + yield* Deferred.fail(item.deferred, new DeclinedError()) + pending.delete(id) + } + return + } + + if (input.reply === "always" && existing.request.save?.length) { + yield* saved.add({ + projectID: location.project.id, + action: existing.request.action, + resources: existing.request.save, + }) + } + yield* Deferred.succeed(existing.deferred, undefined) + pending.delete(input.requestID) + if (input.reply !== "always" || !existing.request.save?.length) return + + const rememberedRules = yield* savedRules() + for (const [id, item] of pending) { + const input = { ...item.request } + const rules = yield* configured(item.request.sessionID, item.agent).pipe( + EffectRuntime.catchTag("Session.NotFoundError", () => EffectRuntime.succeed(undefined)), + ) + if (!rules) continue + if (denied(input, rules)) continue + const effective = [...rules, ...rememberedRules] + if ( + !item.request.resources.every( + (resource) => evaluate(item.request.action, resource, effective).effect === "allow", + ) + ) + continue + yield* events.publish(Event.Replied, { + sessionID: item.request.sessionID, + requestID: item.request.id, + reply: "always", + }) + yield* Deferred.succeed(item.deferred, undefined) + pending.delete(id) + } + }), + ), + ) + + const list = EffectRuntime.fn("PermissionV2.list")(function* () { + return Array.from(pending.values(), (item) => item.request) + }) + + const get = EffectRuntime.fn("PermissionV2.get")(function* (id: ID) { + return pending.get(id)?.request + }) + + const forSession = EffectRuntime.fn("PermissionV2.forSession")(function* (sessionID: SessionV2.ID) { + return Array.from(pending.values(), (item) => item.request).filter((request) => request.sessionID === sessionID) + }) + + return Service.of({ ask, assert, reply, get, forSession, list }) + }), +) + +export const locationLayer = layer.pipe(Layer.provideMerge(AgentV2.locationLayer)) + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [EventV2.node, Location.node, AgentV2.node, SessionStore.node, PermissionSaved.node], +}) diff --git a/packages/core/src/permission/saved.ts b/packages/core/src/permission/saved.ts new file mode 100644 index 00000000000..ffc4559afee --- /dev/null +++ b/packages/core/src/permission/saved.ts @@ -0,0 +1,79 @@ +export * as PermissionSaved from "./saved" + +import { eq } from "drizzle-orm" +import { Context, Effect, Layer, Schema } from "effect" +import { Database } from "../database/database" +import { makeGlobalNode } from "../effect/app-node" +import { ProjectV2 } from "../project" +import { PermissionTable } from "./sql" +import { PermissionSaved } from "@opencode-ai/schema/permission-saved" + +export const ID = PermissionSaved.ID +export type ID = typeof ID.Type + +export const Info = PermissionSaved.Info +export type Info = typeof Info.Type + +export const ListInput = Schema.Struct({ + projectID: ProjectV2.ID.pipe(Schema.optional), +}).annotate({ identifier: "PermissionSaved.ListInput" }) +export type ListInput = typeof ListInput.Type + +export const AddInput = Schema.Struct({ + projectID: ProjectV2.ID, + action: Schema.String, + resources: Schema.Array(Schema.String), +}).annotate({ identifier: "PermissionSaved.AddInput" }) +export type AddInput = typeof AddInput.Type + +export interface Interface { + readonly list: (input?: ListInput) => Effect.Effect> + readonly add: (input: AddInput) => Effect.Effect + readonly remove: (id: ID) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/PermissionSaved") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const { db } = yield* Database.Service + + const list = Effect.fn("PermissionSaved.list")(function* (input?: ListInput) { + const rows = yield* db + .select() + .from(PermissionTable) + .where(input?.projectID ? eq(PermissionTable.project_id, input.projectID) : undefined) + .all() + .pipe(Effect.orDie) + return rows.map( + (row): Info => ({ id: row.id, projectID: row.project_id, action: row.action, resource: row.resource }), + ) + }) + + const add = Effect.fn("PermissionSaved.add")(function* (input: AddInput) { + if (!input.resources.length) return + yield* db + .insert(PermissionTable) + .values( + input.resources.map((resource) => ({ + id: ID.create(), + project_id: input.projectID, + action: input.action, + resource, + })), + ) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie) + }) + + const remove = Effect.fn("PermissionSaved.remove")(function* (id: ID) { + yield* db.delete(PermissionTable).where(eq(PermissionTable.id, id)).run().pipe(Effect.orDie) + }) + + return Service.of({ list, add, remove }) + }), +) + +export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node] }) diff --git a/packages/core/src/permission/sql.ts b/packages/core/src/permission/sql.ts new file mode 100644 index 00000000000..c395555d795 --- /dev/null +++ b/packages/core/src/permission/sql.ts @@ -0,0 +1,20 @@ +import { sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core" +import { Timestamps } from "../database/schema.sql" +import { ProjectV2 } from "../project" +import { ProjectTable } from "../project/sql" +import type { PermissionSaved } from "./saved" + +export const PermissionTable = sqliteTable( + "permission", + { + id: text().$type().primaryKey(), + project_id: text() + .$type() + .notNull() + .references(() => ProjectTable.id, { onDelete: "cascade" }), + action: text().notNull(), + resource: text().notNull(), + ...Timestamps, + }, + (table) => [uniqueIndex("permission_project_action_resource_idx").on(table.project_id, table.action, table.resource)], +) diff --git a/packages/core/src/plugin.ts b/packages/core/src/plugin.ts index df8a40c1fa3..f6c071bca66 100644 --- a/packages/core/src/plugin.ts +++ b/packages/core/src/plugin.ts @@ -1,175 +1,167 @@ export * as PluginV2 from "./plugin" -import { createDraft, finishDraft, type Draft } from "immer" -import type { LanguageModelV3 } from "@ai-sdk/provider" -import { Context, Effect, Exit, Layer, Schema, Scope } from "effect" -import type { ModelV2 } from "./model" -import type { Catalog } from "./catalog" +import { makeLocationNode } from "./effect/app-node" +import { Context, Deferred, Effect, Exit, Layer, Scope } from "effect" +import type { Plugin as PluginRuntime } from "@kilocode/plugin/v2/effect" +import { Plugin } from "@opencode-ai/schema/plugin" +import { AgentV2 } from "./agent" +import { AISDK } from "./aisdk" +import { Catalog } from "./catalog" +import { CommandV2 } from "./command" import { EventV2 } from "./event" - -export const ID = Schema.String.pipe(Schema.brand("Plugin.ID")) +import { Integration } from "./integration" +import { KeyedMutex } from "./effect/keyed-mutex" +import { PluginHost } from "./plugin/host" +import { Reference } from "./reference" +import { SkillV2 } from "./skill" +import { State } from "./state" + +export const ID = Plugin.ID export type ID = typeof ID.Type - -export const Event = { - Added: EventV2.define({ - type: "plugin.added", - schema: { - id: ID, - }, - }), -} - -type HookSpec = { - "catalog.transform": { - input: Catalog.Editor - output: {} - } - "account.switched": { - input: { - serviceID: import("./account").AccountV2.ServiceID - from?: import("./account").AccountV2.ID - to?: import("./account").AccountV2.ID - } - output: {} - } - "aisdk.language": { - input: { - model: ModelV2.Info - sdk: any - options: Record - } - output: { - language?: LanguageModelV3 - } - } - "aisdk.sdk": { - input: { - model: ModelV2.Info - package: string - options: Record - } - output: { - sdk?: any - } - } -} - -export type Hooks = { - [Name in keyof HookSpec]: Readonly & { - -readonly [Field in keyof HookSpec[Name]["output"]]: HookSpec[Name]["output"][Field] extends object - ? Draft - : HookSpec[Name]["output"][Field] - } -} - -export type HookFunctions = { - [key in keyof Hooks]?: (input: Hooks[key]) => Effect.Effect -} - -export type HookInput = HookSpec[Name]["input"] -export type HookOutput = HookSpec[Name]["output"] - -export type Effect = Effect.Effect - -export function define(input: { id: ID; effect: Effect.Effect }) { - return input -} +export const Event = Plugin.Event export interface Interface { - readonly add: (input: { - id: ID - effect: Effect.Effect - }) => Effect.Effect + readonly add: (id: ID, effect: PluginRuntime["effect"]) => Effect.Effect readonly remove: (id: ID) => Effect.Effect - readonly triggerFor: ( - id: ID, - name: Name, - input: HookInput, - output: HookOutput, - ) => Effect.Effect & HookOutput> - readonly trigger: ( - name: Name, - input: HookInput, - output: HookOutput, - ) => Effect.Effect & HookOutput> + readonly wait: (id: ID) => Effect.Effect } export class Service extends Context.Service()("@opencode/v2/Plugin") {} -export const layer = Layer.effect( +const layer = Layer.effect( Service, Effect.gen(function* () { - let hooks: { - id: ID - hooks: HookFunctions - scope: Scope.Closeable - }[] = [] const events = yield* EventV2.Service + const locks = KeyedMutex.makeUnsafe() + const scope = yield* Scope.make() + const active = new Map() + const loading = new Set() + const waiters = new Map>>() + const failures = new Map>() + let host: Parameters[0] + + const add = Effect.fn("Plugin.add")(function* (id: ID, effect: PluginRuntime["effect"]) { + if (loading.has(id)) return yield* Effect.die(`Plugin load cycle detected for ${id}`) + + yield* locks.withLock(id)( + Effect.sync(() => { + loading.add(id) + failures.delete(id) + }).pipe( + Effect.andThen( + State.batch( + Effect.gen(function* () { + const existing = active.get(id) + active.delete(id) + if (existing) yield* Scope.close(existing, Exit.void).pipe(Effect.ignore) + + const child = yield* Scope.fork(scope) + yield* effect(host).pipe( + Scope.provide(child), + Effect.withSpan("Plugin.load", { attributes: { "plugin.id": id } }), + Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)), + ) + yield* events.publish(Event.Added, { id }) + active.set(id, child) + yield* Effect.forEach(waiters.get(id) ?? [], (waiter) => Deferred.succeed(waiter, undefined), { + discard: true, + }) + waiters.delete(id) + }), + ), + ), + Effect.onExit((exit) => { + if (Exit.isSuccess(exit)) return Effect.void + failures.set(id, exit) + return Effect.forEach(waiters.get(id) ?? [], (waiter) => Deferred.done(waiter, exit), { + discard: true, + }).pipe(Effect.ensuring(Effect.sync(() => waiters.delete(id)))) + }), + Effect.ensuring(Effect.sync(() => loading.delete(id))), + ), + ) + }) - const svc = Service.of({ - add: Effect.fn("Plugin.add")(function* (input) { - const existing = hooks.find((item) => item.id === input.id) - if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore) - const scope = yield* Scope.make() - const result = yield* input.effect.pipe(Scope.provide(scope)) - hooks = [ - ...hooks.filter((item) => item.id !== input.id), - { - id: input.id, - hooks: result ?? {}, - scope, - }, - ] - yield* events.publish(Event.Added, { id: input.id }) - }), - trigger: Effect.fn("Plugin.trigger")(function* (name, input, output) { - return yield* svc.triggerFor(ID.make("*"), name, input, output) - }), - triggerFor: Effect.fn("Plugin.triggerFor")(function* (id, name, input, output) { - const draftEntries = new Map>() - const event = { - ...input, - ...output, - } as Record - - for (const [field, value] of Object.entries(output)) { - if (value && typeof value === "object") { - draftEntries.set(field, createDraft(value)) - event[field] = draftEntries.get(field) - } - } + const remove = Effect.fn("Plugin.remove")(function* (id: ID) { + if (loading.has(id)) return yield* Effect.die(`Cannot remove plugin ${id} while it is loading`) + + yield* locks.withLock(id)( + State.batch( + Effect.gen(function* () { + const current = active.get(id) + active.delete(id) + failures.delete(id) + if (current) yield* Scope.close(current, Exit.void).pipe(Effect.ignore) + }), + ), + ) + }) - for (const item of hooks) { - if (id !== ID.make("*") && item.id !== id) continue - const match = item.hooks[name] - if (!match) continue - yield* match(event as any).pipe( - Effect.withSpan(`Plugin.hook.${name}`, { - attributes: { - plugin: item.id, - hook: name, - }, + const wait = Effect.fn("Plugin.wait")(function* (id: ID) { + const waiter = yield* Deferred.make() + const pending = yield* locks.withLock(id)( + Effect.sync(() => { + if (active.has(id)) return false + const failure = failures.get(id) + if (failure) return failure + const current = waiters.get(id) ?? new Set() + current.add(waiter) + waiters.set(id, current) + return true + }), + ) + if (!pending) return + if (typeof pending !== "boolean") return yield* pending + yield* Deferred.await(waiter).pipe( + Effect.ensuring( + locks.withLock(id)( + Effect.sync(() => { + const current = waiters.get(id) + current?.delete(waiter) + if (current?.size === 0) waiters.delete(id) }), - ) - } - - for (const [field, draft] of draftEntries) { - event[field] = finishDraft(draft) - } + ), + ), + ) + }) - return event as any - }), - remove: Effect.fn("Plugin.remove")(function* (id) { - const existing = hooks.find((item) => item.id === id) - hooks = hooks.filter((item) => item.id !== id) - if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore) + yield* Effect.addFinalizer((exit) => + Effect.gen(function* () { + active.clear() + yield* State.batch(Scope.close(scope, exit)) }), + ) + + const service = Service.of({ + add, + remove, + wait, }) - return svc + host = yield* PluginHost.make(service) + return service }), ) -export const defaultLayer = layer.pipe(Layer.provide(EventV2.defaultLayer)) +export const locationLayer = layer.pipe( + Layer.provideMerge(AgentV2.locationLayer), + Layer.provideMerge(AISDK.locationLayer), + Layer.provideMerge(Catalog.locationLayer), + Layer.provideMerge(CommandV2.locationLayer), + Layer.provideMerge(Integration.locationLayer), + Layer.provideMerge(Reference.locationLayer), +) -// opencode -// sdcok +export const node = makeLocationNode({ + service: Service, + layer, + deps: [ + EventV2.node, + AgentV2.node, + AISDK.node, + Catalog.node, + CommandV2.node, + Integration.node, + Reference.node, + SkillV2.node, + ], +}) diff --git a/packages/core/src/plugin/account.ts b/packages/core/src/plugin/account.ts deleted file mode 100644 index 71e64bf23f0..00000000000 --- a/packages/core/src/plugin/account.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Effect, Scope, Stream } from "effect" -import { AccountV2 } from "../account" -import { EventV2 } from "../event" -import { PluginV2 } from "../plugin" - -export const AccountPlugin = PluginV2.define({ - id: PluginV2.ID.make("account"), - effect: Effect.gen(function* () { - const accounts = yield* AccountV2.Service - const events = yield* EventV2.Service - const scope = yield* Scope.Scope - - yield* events.subscribe(AccountV2.Event.Switched).pipe( - Stream.runForEach((event) => - PluginV2.Service.use((plugin) => plugin.trigger("account.switched", event.data, {})).pipe(Effect.asVoid), - ), - Effect.forkIn(scope, { startImmediately: true }), - ) - - return { - "catalog.transform": Effect.fn(function* (evt) { - for (const item of evt.provider.list()) { - const account = yield* accounts.active(AccountV2.ServiceID.make(item.provider.id)).pipe(Effect.orDie) - if (!account) continue - evt.provider.update(item.provider.id, (provider) => { - provider.enabled = { - via: "account", - service: account.serviceID, - } - if (account.credential.type === "api") { - provider.options.aisdk.provider.apiKey = account.credential.key - Object.assign(provider.options.aisdk.provider, account.credential.metadata ?? {}) - } - if (account.credential.type === "oauth") { - provider.options.aisdk.provider.apiKey = account.credential.access - // kilocode_change start - if (provider.id === "kilo" && account.credential.accountId) { - provider.options.aisdk.provider.kilocodeOrganizationId = account.credential.accountId - } - // kilocode_change end - } - }) - } - }), - "account.switched": Effect.fn(function* () {}), - } - }), -}) diff --git a/packages/core/src/plugin/agent.ts b/packages/core/src/plugin/agent.ts index 9baba75c386..9a763c7ea9b 100644 --- a/packages/core/src/plugin/agent.ts +++ b/packages/core/src/plugin/agent.ts @@ -1,14 +1,16 @@ export * as AgentPlugin from "./agent" import path from "path" +import { define } from "./internal" import { Effect } from "effect" import { AgentV2 } from "../agent" import { Global } from "../global" import { Location } from "../location" import { PermissionV2 } from "../permission" -import { PluginV2 } from "../plugin" const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*") +const BUILD_SYSTEM = + "You are an AI coding agent. Help the user accomplish software engineering tasks by inspecting the workspace, making targeted changes, and using tools according to the configured permissions." const PROMPT_EXPLORE = `You are a file search specialist. You excel at thoroughly navigating and exploring codebases. @@ -21,7 +23,6 @@ Guidelines: - Use Glob for broad file pattern matching - Use Grep for searching file contents with regex - Use Read when you know the specific file path you need to read -- Use Bash for file operations like copying, moving, or listing directory contents - Adapt your search approach based on the thoroughness level specified by the caller - Return file paths as absolute paths in your final response - For clear communication, avoid using emojis @@ -78,7 +79,7 @@ Your output must be: "implement rate limiting" -> Rate limiting implementation "how do I connect postgres to my API" -> Postgres API connection "best practices for React hooks" -> React hooks best practices -"@src/auth.ts can you add refresh token support" -> Auth refresh token support +"@src/credential.ts can you add refresh token support" -> Credential refresh token support "@utils/parser.ts this is broken" -> Parser bug fix "look at @config.json" -> Config review "@App.tsx add dark mode toggle" -> Dark mode toggle in App @@ -96,74 +97,70 @@ Rules: - If the conversation ends with an unanswered question to the user, preserve that exact question - If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary` -export const Plugin = PluginV2.define({ - id: PluginV2.ID.make("agent"), - effect: Effect.gen(function* () { - const agent = yield* AgentV2.Service +export const Plugin = define({ + id: "agent", + effect: Effect.fn(function* (ctx) { const location = yield* Location.Service const worktree = location.directory const whitelistedDirs = [TRUNCATION_GLOB, path.join(Global.Path.tmp, "*")] const readonlyExternalDirectory: PermissionV2.Ruleset = [ - { permission: "external_directory", pattern: "*", action: "ask" }, + { action: "external_directory", resource: "*", effect: "ask" }, ...whitelistedDirs.map( - (pattern): PermissionV2.Rule => ({ permission: "external_directory", pattern, action: "allow" }), + (resource): PermissionV2.Rule => ({ action: "external_directory", resource, effect: "allow" }), ), ] const defaults: PermissionV2.Ruleset = [ - { permission: "*", pattern: "*", action: "allow" }, + { action: "*", resource: "*", effect: "allow" }, ...readonlyExternalDirectory, - { permission: "question", pattern: "*", action: "deny" }, - { permission: "plan_enter", pattern: "*", action: "deny" }, - { permission: "plan_exit", pattern: "*", action: "deny" }, - { permission: "repo_clone", pattern: "*", action: "deny" }, - { permission: "repo_overview", pattern: "*", action: "deny" }, - { permission: "read", pattern: "*", action: "allow" }, - { permission: "read", pattern: "*.env", action: "ask" }, - { permission: "read", pattern: "*.env.*", action: "ask" }, - { permission: "read", pattern: "*.env.example", action: "allow" }, + { action: "question", resource: "*", effect: "deny" }, + { action: "plan_enter", resource: "*", effect: "deny" }, + { action: "plan_exit", resource: "*", effect: "deny" }, + { action: "read", resource: "*", effect: "allow" }, + { action: "read", resource: "*.env", effect: "ask" }, + { action: "read", resource: "*.env.*", effect: "ask" }, + { action: "read", resource: "*.env.example", effect: "allow" }, ] - yield* agent.update((editor) => { - editor.update(AgentV2.ID.make("build"), (item) => { + yield* ctx.agent.transform((draft) => { + draft.update(AgentV2.defaultID, (item) => { item.description = "The default agent. Executes tools based on configured permissions." + item.system ??= BUILD_SYSTEM item.mode = "primary" item.permissions.push( ...PermissionV2.merge(defaults, [ - { permission: "question", pattern: "*", action: "allow" }, - { permission: "plan_enter", pattern: "*", action: "allow" }, + { action: "question", resource: "*", effect: "allow" }, + { action: "plan_enter", resource: "*", effect: "allow" }, ]), ) }) - editor.update(AgentV2.ID.make("plan"), (item) => { + draft.update(AgentV2.ID.make("plan"), (item) => { item.description = "Plan mode. Disallows all edit tools." item.mode = "primary" item.permissions.push( ...PermissionV2.merge(defaults, [ - { permission: "question", pattern: "*", action: "allow" }, - { permission: "plan_exit", pattern: "*", action: "allow" }, - { permission: "external_directory", pattern: path.join(Global.Path.data, "plans", "*"), action: "allow" }, - { permission: "edit", pattern: "*", action: "deny" }, - { permission: "edit", pattern: path.join(".opencode", "plans", "*.md"), action: "allow" }, + { action: "question", resource: "*", effect: "allow" }, + { action: "plan_exit", resource: "*", effect: "allow" }, + { action: "external_directory", resource: path.join(Global.Path.data, "plans", "*"), effect: "allow" }, + { action: "edit", resource: "*", effect: "deny" }, + { action: "edit", resource: path.join(".opencode", "plans", "*.md"), effect: "allow" }, { - permission: "edit", - pattern: path.relative(worktree, path.join(Global.Path.data, "plans", "*.md")), - action: "allow", + action: "edit", + resource: path.relative(worktree, path.join(Global.Path.data, "plans", "*.md")), + effect: "allow", }, ]), ) }) - editor.update(AgentV2.ID.make("general"), (item) => { + draft.update(AgentV2.ID.make("general"), (item) => { item.description = "General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel." item.mode = "subagent" - item.permissions.push( - ...PermissionV2.merge(defaults, [{ permission: "todowrite", pattern: "*", action: "deny" }]), - ) + item.permissions.push(...PermissionV2.merge(defaults, [{ action: "todowrite", resource: "*", effect: "deny" }])) }) - editor.update(AgentV2.ID.make("explore"), (item) => { + draft.update(AgentV2.ID.make("explore"), (item) => { item.description = 'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.' item.system = PROMPT_EXPLORE @@ -172,39 +169,37 @@ export const Plugin = PluginV2.define({ ...PermissionV2.merge( defaults, [ - { permission: "*", pattern: "*", action: "deny" }, - { permission: "grep", pattern: "*", action: "allow" }, - { permission: "glob", pattern: "*", action: "allow" }, - { permission: "list", pattern: "*", action: "allow" }, - { permission: "bash", pattern: "*", action: "allow" }, - { permission: "webfetch", pattern: "*", action: "allow" }, - { permission: "websearch", pattern: "*", action: "allow" }, - { permission: "read", pattern: "*", action: "allow" }, + { action: "*", resource: "*", effect: "deny" }, + { action: "grep", resource: "*", effect: "allow" }, + { action: "glob", resource: "*", effect: "allow" }, + { action: "webfetch", resource: "*", effect: "allow" }, + { action: "websearch", resource: "*", effect: "allow" }, + { action: "read", resource: "*", effect: "allow" }, ], readonlyExternalDirectory, ), ) }) - editor.update(AgentV2.ID.make("compaction"), (item) => { + draft.update(AgentV2.ID.make("compaction"), (item) => { item.mode = "primary" item.hidden = true item.system = PROMPT_COMPACTION - item.permissions.push(...PermissionV2.merge(defaults, [{ permission: "*", pattern: "*", action: "deny" }])) + item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) }) - editor.update(AgentV2.ID.make("title"), (item) => { + draft.update(AgentV2.ID.make("title"), (item) => { item.mode = "primary" item.hidden = true item.system = PROMPT_TITLE - item.permissions.push(...PermissionV2.merge(defaults, [{ permission: "*", pattern: "*", action: "deny" }])) + item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) }) - editor.update(AgentV2.ID.make("summary"), (item) => { + draft.update(AgentV2.ID.make("summary"), (item) => { item.mode = "primary" item.hidden = true item.system = PROMPT_SUMMARY - item.permissions.push(...PermissionV2.merge(defaults, [{ permission: "*", pattern: "*", action: "deny" }])) + item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) }) }) }), diff --git a/packages/core/src/plugin/boot.ts b/packages/core/src/plugin/boot.ts deleted file mode 100644 index 4456ac19085..00000000000 --- a/packages/core/src/plugin/boot.ts +++ /dev/null @@ -1,101 +0,0 @@ -export * as PluginBoot from "./boot" - -import { Context, Deferred, Effect, Layer } from "effect" -import { AccountV2 } from "../account" -import { AgentV2 } from "../agent" -import { Catalog } from "../catalog" -import { Config } from "../config" -import { ConfigAgentPlugin } from "../config/plugin/agent" -import { EventV2 } from "../event" -import { Location } from "../location" -import { Npm } from "../npm" -import { PluginV2 } from "../plugin" -import { AccountPlugin } from "./account" -import { AgentPlugin } from "./agent" -import { ConfigProviderPlugin } from "../config/plugin/provider" -import { EnvPlugin } from "./env" -import { ModelsDevPlugin } from "./models-dev" -import { ProviderPlugins } from "./provider" - -type Plugin = { - id: PluginV2.ID - effect: PluginV2.Effect< - | Catalog.Service - | AccountV2.Service - | AgentV2.Service - | Npm.Service - | EventV2.Service - | Location.Service - | PluginV2.Service - | Config.Service - > -} - -export interface Interface { - readonly wait: () => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/v2/PluginBoot") {} - -export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const catalog = yield* Catalog.Service - const plugin = yield* PluginV2.Service - const accounts = yield* AccountV2.Service - const agents = yield* AgentV2.Service - const config = yield* Config.Service - const location = yield* Location.Service - const npm = yield* Npm.Service - const events = yield* EventV2.Service - const done = yield* Deferred.make() - - const add = Effect.fn("PluginBoot.add")(function* (input: Plugin) { - yield* plugin.add({ - id: input.id, - effect: input.effect.pipe( - Effect.provideService(Catalog.Service, catalog), - Effect.provideService(AccountV2.Service, accounts), - Effect.provideService(AgentV2.Service, agents), - Effect.provideService(Config.Service, config), - Effect.provideService(Location.Service, location), - Effect.provideService(Npm.Service, npm), - Effect.provideService(EventV2.Service, events), - Effect.provideService(PluginV2.Service, plugin), - ), - }) - }) - - const boot = Effect.gen(function* () { - yield* add(EnvPlugin) - yield* add(AccountPlugin) - yield* add(AgentPlugin.Plugin) - for (const item of ProviderPlugins) { - yield* add(item) - } - yield* add(ModelsDevPlugin) - yield* add(ConfigProviderPlugin.Plugin) - yield* add(ConfigAgentPlugin.Plugin) - }).pipe(Effect.withSpan("PluginBoot.boot")) - - yield* boot.pipe( - Effect.exit, - Effect.flatMap((exit) => Deferred.done(done, exit)), - Effect.forkScoped, - ) - - return Service.of({ - wait: () => Deferred.await(done), - }) - }), -) - -export const defaultLayer = layer.pipe( - Layer.provide(Catalog.defaultLayer), - Layer.provide(EventV2.defaultLayer), - Layer.provide(PluginV2.defaultLayer), - Layer.provide(AccountV2.defaultLayer), - Layer.provide(AgentV2.defaultLayer), - Layer.provide(Config.defaultLayer), - Layer.provide(Npm.defaultLayer), -) diff --git a/packages/core/src/plugin/command.ts b/packages/core/src/plugin/command.ts new file mode 100644 index 00000000000..cbafd68b502 --- /dev/null +++ b/packages/core/src/plugin/command.ts @@ -0,0 +1,25 @@ +export * as CommandPlugin from "./command" + +import { define } from "./internal" +import { Effect } from "effect" +import { Location } from "../location" +import PROMPT_INITIALIZE from "./command/initialize.txt" +import PROMPT_REVIEW from "./command/review.txt" + +export const Plugin = define({ + id: "command", + effect: Effect.fn(function* (ctx) { + const location = yield* Location.Service + yield* ctx.command.transform((draft) => { + draft.update("init", (command) => { + command.template = PROMPT_INITIALIZE.replace("${path}", location.project.directory) + command.description = "guided AGENTS.md setup" + }) + draft.update("review", (command) => { + command.template = PROMPT_REVIEW.replace("${path}", location.project.directory) + command.description = "review changes [commit|branch|pr], defaults to uncommitted" + command.subtask = true + }) + }) + }), +}) diff --git a/packages/core/src/plugin/command/initialize.txt b/packages/core/src/plugin/command/initialize.txt new file mode 100644 index 00000000000..a403dac904a --- /dev/null +++ b/packages/core/src/plugin/command/initialize.txt @@ -0,0 +1,66 @@ +Create or update `AGENTS.md` for this repository. + +The goal is a compact instruction file that helps future Kilo sessions avoid mistakes and ramp up quickly. Every line should answer: "Would an agent likely miss this without help?" If not, leave it out. + +User-provided focus or constraints (honor these): +$ARGUMENTS + +## How to investigate + +Read the highest-value sources first: +- `README*`, root manifests, workspace config, lockfiles +- build, test, lint, formatter, typecheck, and codegen config +- CI workflows and pre-commit / task runner config +- existing instruction files (`AGENTS.md`, `CLAUDE.md`, `.cursor/rules/`, `.cursorrules`, `.github/copilot-instructions.md`) +- repo-local Kilo config such as `kilo.json` + +If architecture is still unclear after reading config and docs, inspect a small number of representative code files to find the real entrypoints, package boundaries, and execution flow. Prefer reading the files that explain how the system is wired together over random leaf files. + +Prefer executable sources of truth over prose. If docs conflict with config or scripts, trust the executable source and only keep what you can verify. + +## What to extract + +Look for the highest-signal facts for an agent working in this repo: +- exact developer commands, especially non-obvious ones +- how to run a single test, a single package, or a focused verification step +- required command order when it matters, such as `lint -> typecheck -> test` +- monorepo or multi-package boundaries, ownership of major directories, and the real app/library entrypoints +- framework or toolchain quirks: generated code, migrations, codegen, build artifacts, special env loading, dev servers, infra deploy flow +- repo-specific style or workflow conventions that differ from defaults +- testing quirks: fixtures, integration test prerequisites, snapshot workflows, required services, flaky or expensive suites +- important constraints from existing instruction files worth preserving + +Good `AGENTS.md` content is usually hard-earned context that took reading multiple files to infer. + +## Questions + +Only ask the user questions if the repo cannot answer something important. Use the `question` tool for one short batch at most. + +Good questions: +- undocumented team conventions +- branch / PR / release expectations +- missing setup or test prerequisites that are known but not written down + +Do not ask about anything the repo already makes clear. + +## Writing rules + +Include only high-signal, repo-specific guidance such as: +- exact commands and shortcuts the agent would otherwise guess wrong +- architecture notes that are not obvious from filenames +- conventions that differ from language or framework defaults +- setup requirements, environment quirks, and operational gotchas +- references to existing instruction sources that matter + +Exclude: +- generic software advice +- long tutorials or exhaustive file trees +- obvious language conventions +- speculative claims or anything you could not verify +- content better stored in another file referenced via `kilo.json` `instructions` + +When in doubt, omit. + +Prefer short sections and bullets. If the repo is simple, keep the file simple. If the repo is large, summarize the few structural facts that actually change how an agent should work. + +If `AGENTS.md` already exists at `${path}`, improve it in place rather than rewriting blindly. Preserve verified useful guidance, delete fluff or stale claims, and reconcile it with the current codebase. diff --git a/packages/core/src/plugin/command/review.txt b/packages/core/src/plugin/command/review.txt new file mode 100644 index 00000000000..071807ec874 --- /dev/null +++ b/packages/core/src/plugin/command/review.txt @@ -0,0 +1,100 @@ +You are a code reviewer. Your job is to review code changes and provide actionable feedback. + +--- + +Input: $ARGUMENTS + +--- + +## Determining What to Review + +Based on the input provided, determine which type of review to perform: + +1. **No arguments (default)**: Review all uncommitted changes + - Run: `git diff` for unstaged changes + - Run: `git diff --cached` for staged changes + - Run: `git status --short` to identify untracked (net new) files + +2. **Commit hash** (40-char SHA or short hash): Review that specific commit + - Run: `git show $ARGUMENTS` + +3. **Branch name**: Compare current branch to the specified branch + - Run: `git diff $ARGUMENTS...HEAD` + +4. **PR URL or number** (contains "github.com" or "pull" or looks like a PR number): Review the pull request + - Run: `gh pr view $ARGUMENTS` to get PR context + - Run: `gh pr diff $ARGUMENTS` to get the diff + +Use best judgement when processing input. + +--- + +## Gathering Context + +**Diffs alone are not enough.** After getting the diff, read the entire file(s) being modified to understand the full context. Code that looks wrong in isolation may be correct given surrounding logic—and vice versa. + +- Use the diff to identify which files changed +- Use `git status --short` to identify untracked files, then read their full contents +- Read the full file to understand existing patterns, control flow, and error handling +- Check for existing style guide or conventions files (CONVENTIONS.md, AGENTS.md, .editorconfig, etc.) + +--- + +## What to Look For + +**Bugs** - Your primary focus. +- Logic errors, off-by-one mistakes, incorrect conditionals +- If-else guards: missing guards, incorrect branching, unreachable code paths +- Edge cases: null/empty/undefined inputs, error conditions, race conditions +- Security issues: injection, auth bypass, data exposure +- Broken error handling that swallows failures, throws unexpectedly or returns error types that are not caught. + +**Structure** - Does the code fit the codebase? +- Does it follow existing patterns and conventions? +- Are there established abstractions it should use but doesn't? +- Excessive nesting that could be flattened with early returns or extraction + +**Performance** - Only flag if obviously problematic. +- O(n²) on unbounded data, N+1 queries, blocking I/O on hot paths + +**Behavior Changes** - If a behavioral change is introduced, raise it (especially if it's possibly unintentional). + +--- + +## Before You Flag Something + +**Be certain.** If you're going to call something a bug, you need to be confident it actually is one. + +- Only review the changes - do not review pre-existing code that wasn't modified +- Don't flag something as a bug if you're unsure - investigate first +- Don't invent hypothetical problems - if an edge case matters, explain the realistic scenario where it breaks +- If you need more context to be sure, use the tools below to get it + +**Don't be a zealot about style.** When checking code against conventions: + +- Verify the code is *actually* in violation. Don't complain about else statements if early returns are already being used correctly. +- Some "violations" are acceptable when they're the simplest option. A `let` statement is fine if the alternative is convoluted. +- Excessive nesting is a legitimate concern regardless of other style choices. + +--- + +## Tools + +Use these to inform your review: + +- **Explore agent** - Find how existing code handles similar problems. Check patterns, conventions, and prior art before claiming something doesn't fit. +- **Exa Code Context** - Verify correct usage of libraries/APIs before flagging something as wrong. +- **Web Search** - Research best practices if you're unsure about a pattern. + +If you're uncertain about something and can't verify it with these tools, say "I'm not sure about X" rather than flagging it as a definite issue. + +--- + +## Output + +1. If there is a bug, be direct and clear about why it is a bug. +2. Clearly communicate severity of issues. Do not overstate severity. +3. Critiques should clearly and explicitly communicate the scenarios, environments, or inputs that are necessary for the bug to arise. The comment should immediately indicate that the issue's severity depends on these factors. +4. Your tone should be matter-of-fact and not accusatory or overly positive. It should read as a helpful AI assistant suggestion without sounding too much like a human reviewer. +5. Write so the reader can quickly understand the issue without reading too closely. +6. AVOID flattery, do not give any comments that are not helpful to the reader. diff --git a/packages/core/src/plugin/env.ts b/packages/core/src/plugin/env.ts deleted file mode 100644 index 35e6981a40c..00000000000 --- a/packages/core/src/plugin/env.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Effect } from "effect" -import { PluginV2 } from "../plugin" - -export const EnvPlugin = PluginV2.define({ - id: PluginV2.ID.make("env"), - effect: Effect.gen(function* () { - return { - "catalog.transform": Effect.fn(function* (evt) { - for (const item of evt.provider.list()) { - const key = item.provider.env.find((env) => process.env[env]) - if (!key) continue - evt.provider.update(item.provider.id, (provider) => { - provider.enabled = { - via: "env", - name: key, - } - }) - } - }), - } - }), -}) diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts new file mode 100644 index 00000000000..13663e33e5e --- /dev/null +++ b/packages/core/src/plugin/host.ts @@ -0,0 +1,219 @@ +export * as PluginHost from "./host" + +import type { PluginContext as Interface } from "@kilocode/plugin/v2/effect" +import { Effect, Schema } from "effect" +import { AgentV2 } from "../agent" +import { AISDK } from "../aisdk" +import { Catalog } from "../catalog" +import { CommandV2 } from "../command" +import { Credential } from "../credential" +import { Integration } from "../integration" +import { ModelV2 } from "../model" +import { PluginV2 } from "../plugin" +import { ProviderV2 } from "../provider" +import { Reference } from "../reference" +import type { DeepMutable } from "../schema" +import { SkillV2 } from "../skill" + +const mutable = (value: T) => value as DeepMutable + +export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) { + const agents = yield* AgentV2.Service + const aisdk = yield* AISDK.Service + const catalog = yield* Catalog.Service + const commands = yield* CommandV2.Service + const integration = yield* Integration.Service + const reference = yield* Reference.Service + const skill = yield* SkillV2.Service + + return { + options: {}, + agent: { + reload: agents.reload, + transform: (callback) => + agents.transform((draft) => + callback({ + list: () => mutable(draft.list()), + get: (id) => mutable(draft.get(AgentV2.ID.make(id))), + default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)), + update: (id, update) => draft.update(AgentV2.ID.make(id), update), + remove: (id) => draft.remove(AgentV2.ID.make(id)), + }), + ), + }, + aisdk: { + sdk: (callback) => + aisdk.hook.sdk((event) => { + const output = { + model: mutable(event.model), + package: event.package, + options: event.options, + sdk: event.sdk, + } + const result = callback(output) + return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe( + Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))), + ) + }), + language: (callback) => + aisdk.hook.language((event) => { + const output = { + model: mutable(event.model), + sdk: event.sdk, + options: event.options, + language: event.language, + } + const result = callback(output) + return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe( + Effect.tap(() => Effect.sync(() => (event.language = output.language))), + ) + }), + }, + catalog: { + reload: catalog.reload, + transform: (callback) => + catalog.transform((draft) => + callback({ + provider: { + list: () => mutable(draft.provider.list()), + get: (id) => mutable(draft.provider.get(ProviderV2.ID.make(id))), + update: (id, update) => draft.provider.update(ProviderV2.ID.make(id), update), + remove: (id) => draft.provider.remove(ProviderV2.ID.make(id)), + }, + model: { + get: (providerID, modelID) => + mutable(draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID))), + update: (providerID, modelID, update) => + draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), update), + remove: (providerID, modelID) => + draft.model.remove(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), + default: { + get: draft.model.default.get, + set: (providerID, modelID) => + draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), + }, + }, + }), + ), + }, + command: { + reload: commands.reload, + transform: commands.transform, + }, + integration: { + reload: integration.reload, + connection: { + active: (id) => integration.connection.active(Integration.ID.make(id)), + resolve: (connection) => + integration.connection.resolve( + connection.type === "credential" ? { ...connection, id: Credential.ID.make(connection.id) } : connection, + ), + }, + transform: (callback) => + integration.transform((draft) => + callback({ + list: () => mutable(draft.list()), + get: (id) => mutable(draft.get(Integration.ID.make(id))), + update: (id, update) => draft.update(Integration.ID.make(id), update), + remove: (id) => draft.remove(Integration.ID.make(id)), + method: { + list: (id) => mutable(draft.method.list(Integration.ID.make(id))), + update: (input) => { + if ("authorize" in input) { + const methodID = Integration.MethodID.make(input.method.id) + const refresh = input.refresh + draft.method.update({ + integrationID: Integration.ID.make(input.integrationID), + method: { ...input.method, id: methodID }, + authorize: (inputs) => + input.authorize(inputs).pipe( + Effect.map((authorization) => { + if (authorization.mode === "auto") { + return { + ...authorization, + callback: authorization.callback.pipe( + Effect.map((credential) => + Credential.OAuth.make({ + ...credential, + methodID: Integration.MethodID.make(credential.methodID), + }), + ), + ), + } + } + return { + ...authorization, + callback: (code: string) => + authorization.callback(code).pipe( + Effect.map((credential) => + Credential.OAuth.make({ + ...credential, + methodID: Integration.MethodID.make(credential.methodID), + }), + ), + ), + } + }), + ), + ...(refresh + ? { + refresh: (value: Credential.OAuth) => + refresh(value).pipe( + Effect.map((next) => + Credential.OAuth.make({ + ...next, + methodID: Integration.MethodID.make(next.methodID), + }), + ), + ), + } + : {}), + ...(input.label ? { label: input.label } : {}), + }) + return + } + if (input.method.type === "env") { + draft.method.update({ + integrationID: Integration.ID.make(input.integrationID), + method: { type: "env", names: input.method.names }, + }) + return + } + draft.method.update({ + integrationID: Integration.ID.make(input.integrationID), + method: { type: "key", label: input.method.label }, + }) + }, + remove: (id, method) => + draft.method.remove(Integration.ID.make(id), Schema.decodeUnknownSync(Integration.Method)(method)), + }, + }), + ), + }, + plugin: { + add: (input) => plugin.add(PluginV2.ID.make(input.id), input.effect), + remove: (id) => plugin.remove(PluginV2.ID.make(id)), + }, + reference: { + reload: reference.reload, + transform: (callback) => + reference.transform((draft) => + callback({ + add: (name, source) => draft.add(name, Schema.decodeUnknownSync(Reference.Source)(source)), + remove: draft.remove, + list: draft.list, + }), + ), + }, + skill: { + reload: skill.reload, + transform: (callback) => + skill.transform((draft) => + callback({ + source: (source) => draft.source(Schema.decodeUnknownSync(SkillV2.Source)(source)), + list: draft.list, + }), + ), + }, + } satisfies Interface +}) diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts new file mode 100644 index 00000000000..80193452992 --- /dev/null +++ b/packages/core/src/plugin/internal.ts @@ -0,0 +1,152 @@ +export * as PluginInternal from "./internal" + +import { makeLocationNode } from "../effect/app-node" +import { httpClient } from "../effect/app-node-platform" +import type { PluginContext } from "@kilocode/plugin/v2/effect" +import { Effect, Layer, Scope } from "effect" +import { AgentV2 } from "../agent" +import { Catalog } from "../catalog" +import { CommandV2 } from "../command" +import { Config } from "../config" +import { ConfigAgentPlugin } from "../config/plugin/agent" +import { ConfigCommandPlugin } from "../config/plugin/command" +import { ConfigExternalPlugin } from "../config/plugin/external" +import { ConfigProviderPlugin } from "../config/plugin/provider" +import { ConfigReferencePlugin } from "../config/plugin/reference" +import { ConfigSkillPlugin } from "../config/plugin/skill" +import { EventV2 } from "../event" +import { FileSystem } from "../filesystem" +import { FSUtil } from "../fs-util" +import { Global } from "../global" +import { Integration } from "../integration" +import { Location } from "../location" +import { ModelsDev } from "../models-dev" +import { Npm } from "../npm" +import { PluginV2 } from "../plugin" +import { Reference } from "../reference" +import { SkillV2 } from "../skill" +import { State } from "../state" +import { FetchHttpClient, HttpClient } from "effect/unstable/http" +import { AgentPlugin } from "./agent" +import { CommandPlugin } from "./command" +import { ModelsDevPlugin } from "./models-dev" +import { ProviderPlugins } from "./provider" +import { VariantPlugin } from "./variant" + +export type Requirements = + | AgentV2.Service + | Catalog.Service + | CommandV2.Service + | Config.Service + | EventV2.Service + | FileSystem.Service + | FSUtil.Service + | Global.Service + | HttpClient.HttpClient + | Integration.Service + | Location.Service + | ModelsDev.Service + | Npm.Service + | Reference.Service + | SkillV2.Service + +export interface Plugin { + readonly id: string + readonly effect: (context: PluginContext) => Effect.Effect +} + +export function define(plugin: Plugin) { + return plugin +} + +const layer = Layer.effectDiscard( + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const commands = yield* CommandV2.Service + const plugin = yield* PluginV2.Service + const integration = yield* Integration.Service + const agents = yield* AgentV2.Service + const config = yield* Config.Service + const location = yield* Location.Service + const modelsDev = yield* ModelsDev.Service + const npm = yield* Npm.Service + const events = yield* EventV2.Service + const fs = yield* FSUtil.Service + const filesystem = yield* FileSystem.Service + const global = yield* Global.Service + const http = yield* HttpClient.HttpClient + const skill = yield* SkillV2.Service + const reference = yield* Reference.Service + const add = (input: Plugin) => { + const loaded = { + id: input.id, + effect: (context: PluginContext) => + input + .effect(context) + .pipe( + Effect.provideService(Catalog.Service, catalog), + Effect.provideService(CommandV2.Service, commands), + Effect.provideService(Integration.Service, integration), + Effect.provideService(AgentV2.Service, agents), + Effect.provideService(Config.Service, config), + Effect.provideService(Location.Service, location), + Effect.provideService(ModelsDev.Service, modelsDev), + Effect.provideService(Npm.Service, npm), + Effect.provideService(EventV2.Service, events), + Effect.provideService(FSUtil.Service, fs), + Effect.provideService(FileSystem.Service, filesystem), + Effect.provideService(Global.Service, global), + Effect.provideService(HttpClient.HttpClient, http), + Effect.provideService(SkillV2.Service, skill), + Effect.provideService(Reference.Service, reference), + ), + } + return plugin.add(PluginV2.ID.make(loaded.id), loaded.effect) + } + + yield* State.batch( + Effect.gen(function* () { + yield* add(ConfigReferencePlugin.Plugin) + yield* add(AgentPlugin.Plugin) + yield* add(CommandPlugin.Plugin) + // kilocode_change - Kilo's CLI registry supplies `kilo-config`; do not register the redundant opencode skill. + yield* add(ModelsDevPlugin) + yield* add(ConfigAgentPlugin.Plugin) + yield* add(ConfigCommandPlugin.Plugin) + yield* add(ConfigSkillPlugin.Plugin) + for (const item of ProviderPlugins) yield* add(item) + yield* add(ConfigExternalPlugin.Plugin) + yield* add(ConfigProviderPlugin.Plugin) + yield* add(VariantPlugin.Plugin) + }), + ).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true })) + }), +) + +export const locationLayer = layer.pipe( + Layer.provideMerge(Config.locationLayer), + Layer.provideMerge(FetchHttpClient.layer), +) + +export const node = makeLocationNode({ + name: "plugin-internal", + layer, + deps: [ + Catalog.node, + CommandV2.node, + PluginV2.node, + Integration.node, + AgentV2.node, + Config.node, + Location.node, + ModelsDev.node, + Npm.node, + EventV2.node, + FSUtil.node, + FileSystem.node, + Global.node, + httpClient, + SkillV2.node, + Reference.node, + ], +}) diff --git a/packages/core/src/plugin/models-dev.ts b/packages/core/src/plugin/models-dev.ts index 4733833fdbf..eda21e39b0f 100644 --- a/packages/core/src/plugin/models-dev.ts +++ b/packages/core/src/plugin/models-dev.ts @@ -1,17 +1,16 @@ -import { DateTime, Effect, Scope, Stream } from "effect" -import { Catalog } from "../catalog" +import { define } from "./internal" +import type { ModelV2Info } from "@kilocode/sdk/v2/types" +import { Effect, Stream } from "effect" import { EventV2 } from "../event" -import { ModelV2 } from "../model" import { ModelsDev } from "../models-dev" -import { PluginV2 } from "../plugin" import { ProviderV2 } from "../provider" function released(date: string) { const time = Date.parse(date) - return DateTime.makeUnsafe(Number.isFinite(time) ? time : 0) + return Number.isFinite(time) ? time : 0 } -function cost(input: ModelsDev.Model["cost"]) { +function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] { const base = { input: input?.input ?? 0, output: input?.output ?? 0, @@ -20,101 +19,165 @@ function cost(input: ModelsDev.Model["cost"]) { write: input?.cache_write ?? 0, }, } - if (!input?.context_over_200k) return [base] return [ base, - { - tier: { - type: "context" as const, - size: 200_000, - }, - input: input.context_over_200k.input, - output: input.context_over_200k.output, + ...(input?.tiers?.map((item) => ({ + tier: item.tier, + input: item.input, + output: item.output, cache: { - read: input.context_over_200k.cache_read ?? 0, - write: input.context_over_200k.cache_write ?? 0, + read: item.cache_read ?? 0, + write: item.cache_write ?? 0, }, - }, + })) ?? []), + ...(input?.context_over_200k + ? [ + { + tier: { + type: "context" as const, + size: 200_000, + }, + input: input.context_over_200k.input, + output: input.context_over_200k.output, + cache: { + read: input.context_over_200k.cache_read ?? 0, + write: input.context_over_200k.cache_write ?? 0, + }, + }, + ] + : []), ] } -function variants(model: ModelsDev.Model) { - return Object.entries(model.experimental?.modes ?? {}).map(([id, item]) => ({ - id: ModelV2.VariantID.make(id), - headers: { ...(item.provider?.headers ?? {}) }, - body: { ...(item.provider?.body ?? {}) }, - aisdk: { - provider: {}, - request: {}, - }, - })) +function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"] | undefined) { + if (!override) return base + const next = cost(override) + const [baseDefault, ...baseTiers] = base + const [nextDefault, ...nextTiers] = next + const tierKey = (item: ModelV2Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}` + const merge = (left: ModelV2Info["cost"][number], right: ModelV2Info["cost"][number]) => ({ + ...left, + ...right, + tier: right.tier ?? left.tier, + cache: { ...left.cache, ...right.cache }, + }) + const tiers = new Map(baseTiers.map((item) => [tierKey(item), item])) + for (const item of nextTiers) { + const current = tiers.get(tierKey(item)) + tiers.set(tierKey(item), current ? merge(current, item) : item) + } + return [merge(baseDefault ?? { input: 0, output: 0, cache: { read: 0, write: 0 } }, nextDefault), ...tiers.values()] } -export const ModelsDevPlugin = PluginV2.define({ - id: PluginV2.ID.make("models-dev"), - effect: Effect.gen(function* () { - const catalog = yield* Catalog.Service +function modeName(model: ModelsDev.Model, mode: string) { + return `${model.name} ${mode.charAt(0).toUpperCase()}${mode.slice(1)}` +} + +function applyModel( + draft: ModelV2Info, + model: ModelsDev.Model, + input: { + readonly name?: string + readonly cost?: ModelV2Info["cost"] + readonly request?: NonNullable["modes"]>[string]["provider"] + } = {}, +) { + draft.name = input.name ?? model.name + draft.family = model.family + draft.api = model.provider?.npm + ? { + id: model.id, + type: "aisdk", + package: model.provider.npm, + url: model.provider.api, + } + : { + id: model.id, + type: "native", + url: model.provider?.api, + settings: {}, + } + draft.capabilities = { + tools: model.tool_call, + input: [...(model.modalities?.input ?? [])], + output: [...(model.modalities?.output ?? [])], + } + draft.variants = [] + draft.time.released = released(model.release_date) + draft.cost = input.cost ?? cost(model.cost) + draft.status = model.status ?? "active" + draft.enabled = true + draft.limit = { + context: model.limit.context, + input: model.limit.input, + output: model.limit.output, + } + Object.assign(draft.request.headers, input.request?.headers ?? {}) + Object.assign(draft.request.body, input.request?.body ?? {}) +} + +export const ModelsDevPlugin = define({ + id: "models-dev", + effect: Effect.fn(function* (ctx) { const modelsDev = yield* ModelsDev.Service const events = yield* EventV2.Service - const scope = yield* Scope.Scope - const transform = yield* catalog.transform() - const refresh = Effect.fn("ModelsDevPlugin.refresh")(function* () { - const data = yield* modelsDev.get() - yield* transform((catalog) => { + yield* ctx.integration.transform( + Effect.fn(function* (integrations) { + const data = yield* modelsDev.get() + for (const item of Object.values(data)) { + if (item.env.length === 0) continue + const integrationID = item.id + integrations.update(integrationID, (integration) => (integration.name = item.name)) + integrations.method.update({ + integrationID, + method: { type: "key" }, + }) + integrations.method.update({ + integrationID, + method: { type: "env", names: [...item.env] }, + }) + } + }), + ) + yield* ctx.catalog.transform( + Effect.fn(function* (catalog) { + const data = yield* modelsDev.get() for (const item of Object.values(data)) { const providerID = ProviderV2.ID.make(item.id) catalog.provider.update(providerID, (provider) => { provider.name = item.name - provider.env = [...item.env] - provider.endpoint = item.npm + provider.api = item.npm ? { type: "aisdk", package: item.npm, url: item.api, } : { - type: "unknown", + type: "native", + url: item.api, + settings: {}, } }) for (const model of Object.values(item.models)) { - const modelID = ModelV2.ID.make(model.id) - catalog.model.update(providerID, modelID, (draft) => { - draft.name = model.name - draft.family = model.family ? ModelV2.Family.make(model.family) : undefined - draft.endpoint = model.provider?.npm - ? { - type: "aisdk", - package: model.provider?.npm, - url: model.provider.api, - } - : { - type: "unknown", - } - draft.capabilities = { - tools: model.tool_call, - input: [...(model.modalities?.input ?? [])], - output: [...(model.modalities?.output ?? [])], - } - draft.variants = variants(model) - draft.time.released = released(model.release_date) - draft.cost = cost(model.cost) - draft.status = model.status ?? "active" - draft.enabled = true - draft.limit = { - context: model.limit.context, - input: model.limit.input, - output: model.limit.output, - } - }) + const baseCost = cost(model.cost) + catalog.model.update(providerID, model.id, (draft) => applyModel(draft, model, { cost: baseCost })) + for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) { + catalog.model.update(providerID, `${model.id}-${mode}`, (draft) => + applyModel(draft, model, { + name: modeName(model, mode), + cost: mergeCost(baseCost, options.cost), + request: options.provider, + }), + ) + } } } - }) - }) - yield* refresh() + }), + ) yield* events.subscribe(ModelsDev.Event.Refreshed).pipe( - Stream.runForEach(() => refresh()), - Effect.forkIn(scope, { startImmediately: true }), + Stream.runForEach(() => ctx.integration.reload().pipe(Effect.andThen(ctx.catalog.reload()))), + Effect.forkScoped({ startImmediately: true }), ) - }).pipe(Effect.provide(ModelsDev.defaultLayer)), + }), }) diff --git a/packages/core/src/plugin/promise.ts b/packages/core/src/plugin/promise.ts new file mode 100644 index 00000000000..01928b41657 --- /dev/null +++ b/packages/core/src/plugin/promise.ts @@ -0,0 +1,93 @@ +export * as PluginPromise from "./promise" + +import { define } from "@kilocode/plugin/v2/effect" +import type { Plugin, PluginContext, Registration } from "@kilocode/plugin/v2/promise" +import { Effect, Scope } from "effect" + +// The Effect host hands back this registration shape; mirror it structurally so +// we do not have to alias the Effect package's `Registration` against the Promise one. +type HostRegistration = { readonly dispose: Effect.Effect } + +/** + * Adapts a Promise plugin into an Effect plugin so the existing Effect-only + * loader (`PluginV2` / `PluginInternal`) can run it unchanged. + * + * Hook registrations created during the async `setup` attach to the plugin's + * scope, so unloading the plugin disposes them. The captured fiber context + * preserves boot-time batching, so Promise-plugin transforms still coalesce + * into one reload per domain. + */ +export function fromPromise(plugin: Plugin) { + return define({ + id: plugin.id, + effect: (host) => + Effect.gen(function* () { + const scope = yield* Scope.Scope + const context = yield* Effect.context() + + // Run a hook registration on the plugin scope and resolve once it is registered. + const register = (effect: Effect.Effect): Promise => + Effect.runPromiseWith(context)(Scope.provide(scope)(effect)).then((registration) => ({ + dispose: () => Effect.runPromiseWith(context)(registration.dispose), + })) + + const run = (effect: Effect.Effect) => Effect.runPromiseWith(context)(effect) + + const transform = + (domain: { + transform: ( + callback: (draft: Draft) => Effect.Effect | void, + ) => Effect.Effect + }) => + (callback: (draft: Draft) => Promise | void) => + register(domain.transform((draft) => Effect.promise(() => Promise.resolve(callback(draft))))) + + const context2: PluginContext = { + options: host.options, + agent: { + transform: transform(host.agent), + reload: () => run(host.agent.reload()), + }, + aisdk: { + sdk: (callback) => + register(host.aisdk.sdk((event) => Effect.promise(() => Promise.resolve(callback(event))))), + language: (callback) => + register(host.aisdk.language((event) => Effect.promise(() => Promise.resolve(callback(event))))), + }, + catalog: { + transform: transform(host.catalog), + reload: () => run(host.catalog.reload()), + }, + command: { + transform: transform(host.command), + reload: () => run(host.command.reload()), + }, + integration: { + transform: transform(host.integration), + reload: () => run(host.integration.reload()), + connection: { + active: (id) => Effect.runPromiseWith(context)(host.integration.connection.active(id)), + resolve: (connection) => Effect.runPromiseWith(context)(host.integration.connection.resolve(connection)), + }, + }, + plugin: { + add: (input) => { + const child = fromPromise(input) + return run(host.plugin.add(child)) + }, + remove: (id) => run(host.plugin.remove(id)), + }, + reference: { + transform: transform(host.reference), + reload: () => run(host.reference.reload()), + }, + skill: { + transform: transform(host.skill), + reload: () => run(host.skill.reload()), + }, + } + + yield* Effect.promise(() => Promise.resolve(plugin.setup(context2))) + }), + }) +} diff --git a/packages/core/src/plugin/provider.ts b/packages/core/src/plugin/provider.ts index 1880787495f..1749b474ed3 100644 --- a/packages/core/src/plugin/provider.ts +++ b/packages/core/src/plugin/provider.ts @@ -1 +1,71 @@ -export { ProviderPlugins } from "./provider/index" +import { AlibabaPlugin } from "./provider/alibaba" +import { AmazonBedrockPlugin } from "./provider/amazon-bedrock" +import { AnthropicPlugin } from "./provider/anthropic" +import { AzureCognitiveServicesPlugin, AzurePlugin } from "./provider/azure" +import { CerebrasPlugin } from "./provider/cerebras" +import { CloudflareAIGatewayPlugin } from "./provider/cloudflare-ai-gateway" +import { CloudflareWorkersAIPlugin } from "./provider/cloudflare-workers-ai" +import { CoherePlugin } from "./provider/cohere" +import { DeepInfraPlugin } from "./provider/deepinfra" +import { DynamicProviderPlugin } from "./provider/dynamic" +import { GatewayPlugin } from "./provider/gateway" +import { GithubCopilotPlugin } from "./provider/github-copilot" +import { GitLabPlugin } from "./provider/gitlab" +import { GooglePlugin } from "./provider/google" +import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "./provider/google-vertex" +import { GroqPlugin } from "./provider/groq" +import { KiloPlugin } from "./provider/kilo" +import { LLMGatewayPlugin } from "./provider/llmgateway" +import { MistralPlugin } from "./provider/mistral" +import { NvidiaPlugin } from "./provider/nvidia" +import { OpenAIPlugin } from "./provider/openai" +import { SnowflakeCortexPlugin } from "./provider/snowflake-cortex" +import { OpenAICompatiblePlugin } from "./provider/openai-compatible" +import { OpencodePlugin } from "./provider/opencode" +import { OpenRouterPlugin } from "./provider/openrouter" +import { PerplexityPlugin } from "./provider/perplexity" +import { SapAICorePlugin } from "./provider/sap-ai-core" +import { TogetherAIPlugin } from "./provider/togetherai" +import { VercelPlugin } from "./provider/vercel" +import { VenicePlugin } from "./provider/venice" +import { XAIPlugin } from "./provider/xai" +import { ZenmuxPlugin } from "./provider/zenmux" +import type { PluginInternal } from "./internal" +import type { Scope } from "effect" + +export const ProviderPlugins: PluginInternal.Plugin[] = [ + AlibabaPlugin, + AmazonBedrockPlugin, + AnthropicPlugin, + AzureCognitiveServicesPlugin, + AzurePlugin, + CerebrasPlugin, + CloudflareAIGatewayPlugin, + CloudflareWorkersAIPlugin, + CoherePlugin, + DeepInfraPlugin, + GatewayPlugin, + GithubCopilotPlugin, + GitLabPlugin, + GooglePlugin, + GoogleVertexAnthropicPlugin, + GoogleVertexPlugin, + GroqPlugin, + KiloPlugin, + LLMGatewayPlugin, + MistralPlugin, + NvidiaPlugin, + OpencodePlugin, + SnowflakeCortexPlugin, + OpenAICompatiblePlugin, + OpenAIPlugin, + OpenRouterPlugin, + PerplexityPlugin, + SapAICorePlugin, + TogetherAIPlugin, + VercelPlugin, + VenicePlugin, + XAIPlugin, + ZenmuxPlugin, + DynamicProviderPlugin, +] diff --git a/packages/core/src/plugin/provider/alibaba.ts b/packages/core/src/plugin/provider/alibaba.ts index fa5c0a91cfb..c5c4be0d0be 100644 --- a/packages/core/src/plugin/provider/alibaba.ts +++ b/packages/core/src/plugin/provider/alibaba.ts @@ -1,15 +1,15 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" -export const AlibabaPlugin = PluginV2.define({ - id: PluginV2.ID.make("alibaba"), - effect: Effect.gen(function* () { - return { - "aisdk.sdk": Effect.fn(function* (evt) { +export const AlibabaPlugin = define({ + id: "alibaba", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/alibaba") return const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba")) evt.sdk = mod.createAlibaba(evt.options) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/amazon-bedrock.ts b/packages/core/src/plugin/provider/amazon-bedrock.ts index e7452ac2e97..0995cf1c172 100644 --- a/packages/core/src/plugin/provider/amazon-bedrock.ts +++ b/packages/core/src/plugin/provider/amazon-bedrock.ts @@ -1,7 +1,14 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import type { LanguageModelV3 } from "@ai-sdk/provider" +import { define } from "../internal" import { ProviderV2 } from "../../provider" +type MantleSDK = { + languageModel: (modelID: string) => LanguageModelV3 + chat: (modelID: string) => LanguageModelV3 + responses: (modelID: string) => LanguageModelV3 +} + // Bedrock cross-region inference profiles require regional prefixes only for // specific model/region combinations. Keep the mapping narrow and avoid // double-prefixing model IDs that models.dev already marks as global/us/eu/etc. @@ -46,26 +53,34 @@ function resolveModelID(modelID: string, region: string | undefined) { : modelID } -export const AmazonBedrockPlugin = PluginV2.define({ - id: PluginV2.ID.make("amazon-bedrock"), - effect: Effect.gen(function* () { - return { - "catalog.transform": Effect.fn(function* (evt) { +function selectMantleModel(sdk: MantleSDK, modelID: string) { + if (modelID === "openai.gpt-oss-safeguard-20b" || modelID === "openai.gpt-oss-safeguard-120b") + return sdk.chat(modelID) + return sdk.responses(modelID) +} + +export const AmazonBedrockPlugin = define({ + id: "amazon-bedrock", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/amazon-bedrock") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue evt.provider.update(item.provider.id, (provider) => { - if (provider.endpoint.type !== "aisdk") return - if (typeof provider.options.aisdk.provider.endpoint !== "string") return + if (provider.api.type !== "aisdk") return + if (typeof provider.request.body.endpoint !== "string") return // The AI SDK expects a base URL, but users configure Bedrock private/VPC // endpoints as `endpoint`; move it into the catalog endpoint URL once. - provider.endpoint.url = provider.options.aisdk.provider.endpoint - delete provider.options.aisdk.provider.endpoint + provider.api.url = provider.request.body.endpoint + delete provider.request.body.endpoint }) } }), - "aisdk.sdk": Effect.fn(function* (evt) { - if (evt.package !== "@ai-sdk/amazon-bedrock") return + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return const options = { ...evt.options } const profile = typeof options.profile === "string" ? options.profile : process.env.AWS_PROFILE const region = typeof options.region === "string" ? options.region : (process.env.AWS_REGION ?? "us-east-1") @@ -86,14 +101,26 @@ export const AmazonBedrockPlugin = PluginV2.define({ options.credentialProvider = fromNodeProviderChain(profile ? { profile } : {}) } + if (evt.package === "@ai-sdk/amazon-bedrock/mantle") { + const mod = yield* Effect.promise(() => import("@ai-sdk/amazon-bedrock/mantle")) + evt.sdk = mod.createBedrockMantle(options) + return + } + const mod = yield* Effect.promise(() => import("@ai-sdk/amazon-bedrock")) evt.sdk = mod.createAmazonBedrock(options) }), - "aisdk.language": Effect.fn(function* (evt) { + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return + if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") { + evt.language = selectMantleModel(evt.sdk, evt.model.api.id) + return + } const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION - evt.language = evt.sdk.languageModel(resolveModelID(evt.model.apiID, region)) + evt.language = evt.sdk.languageModel(resolveModelID(evt.model.api.id, region)) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/anthropic.ts b/packages/core/src/plugin/provider/anthropic.ts index 026da363492..cf883a0687f 100644 --- a/packages/core/src/plugin/provider/anthropic.ts +++ b/packages/core/src/plugin/provider/anthropic.ts @@ -1,25 +1,27 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" -export const AnthropicPlugin = PluginV2.define({ - id: PluginV2.ID.make("anthropic"), - effect: Effect.gen(function* () { - return { - "catalog.transform": Effect.fn(function* (evt) { +export const AnthropicPlugin = define({ + id: "anthropic", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/anthropic") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/anthropic") continue evt.provider.update(item.provider.id, (provider) => { - provider.options.headers["anthropic-beta"] = + provider.request.headers["anthropic-beta"] = "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14" }) } }), - "aisdk.sdk": Effect.fn(function* (evt) { + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/anthropic") return const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic")) evt.sdk = mod.createAnthropic(evt.options) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/azure.ts b/packages/core/src/plugin/provider/azure.ts index bea98cf2119..2e1f9d9b48f 100644 --- a/packages/core/src/plugin/provider/azure.ts +++ b/packages/core/src/plugin/provider/azure.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" import { ProviderV2 } from "../../provider" function selectLanguage(sdk: any, modelID: string, useChat: boolean) { @@ -10,30 +10,32 @@ function selectLanguage(sdk: any, modelID: string, useChat: boolean) { return sdk.languageModel(modelID) } -export const AzurePlugin = PluginV2.define({ - id: PluginV2.ID.make("azure"), - effect: Effect.gen(function* () { - return { - "catalog.transform": Effect.fn(function* (evt) { +export const AzurePlugin = define({ + id: "azure", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/azure") continue - const configured = item.provider.options.aisdk.provider.resourceName + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/azure") continue + const configured = item.provider.request.body.resourceName const resourceName = typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME if (!resourceName) continue evt.provider.update(item.provider.id, (provider) => { - provider.options.aisdk.provider.resourceName = resourceName + provider.request.body.resourceName = resourceName }) } }), - "aisdk.sdk": Effect.fn(function* (evt) { + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/azure") return if (evt.model.providerID === ProviderV2.ID.azure) { if ( !evt.options.resourceName && !evt.options.baseURL && - (evt.model.endpoint.type !== "aisdk" || !evt.model.endpoint.url) + (evt.model.api.type !== "aisdk" || !evt.model.api.url) ) { throw new Error( "AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it", @@ -43,34 +45,38 @@ export const AzurePlugin = PluginV2.define({ const mod = yield* Effect.promise(() => import("@ai-sdk/azure")) evt.sdk = mod.createAzure(evt.options) }), - "aisdk.language": Effect.fn(function* (evt) { + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.azure) return - evt.language = selectLanguage(evt.sdk, evt.model.apiID, Boolean(evt.options.useCompletionUrls)) + evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls)) }), - } + ) }), }) -export const AzureCognitiveServicesPlugin = PluginV2.define({ - id: PluginV2.ID.make("azure-cognitive-services"), - effect: Effect.gen(function* () { - return { - "catalog.transform": Effect.fn(function* (evt) { +export const AzureCognitiveServicesPlugin = define({ + id: "azure-cognitive-services", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME if (!resourceName) return for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/openai-compatible") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (!item.provider.id.includes("azure-cognitive-services")) continue evt.provider.update(item.provider.id, (provider) => { - provider.options.aisdk.provider.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai` + provider.request.body.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai` }) } }), - "aisdk.language": Effect.fn(function* (evt) { + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return - evt.language = selectLanguage(evt.sdk, evt.model.apiID, Boolean(evt.options.useCompletionUrls)) + evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls)) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/cerebras.ts b/packages/core/src/plugin/provider/cerebras.ts index b18884cb6ed..0fd651160fe 100644 --- a/packages/core/src/plugin/provider/cerebras.ts +++ b/packages/core/src/plugin/provider/cerebras.ts @@ -1,24 +1,26 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" -export const CerebrasPlugin = PluginV2.define({ - id: PluginV2.ID.make("cerebras"), - effect: Effect.gen(function* () { - return { - "catalog.transform": Effect.fn(function* (ctx) { - for (const item of ctx.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/cerebras") continue - ctx.provider.update(item.provider.id, (provider) => { - provider.options.headers["X-Cerebras-3rd-Party-Integration"] = "opencode" +export const CerebrasPlugin = define({ + id: "cerebras", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/cerebras") continue + evt.provider.update(item.provider.id, (provider) => { + provider.request.headers["X-Cerebras-3rd-Party-Integration"] = "opencode" }) } }), - "aisdk.sdk": Effect.fn(function* (evt) { + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/cerebras") return const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras")) evt.sdk = mod.createCerebras(evt.options) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts b/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts index 1cd974ee953..d416f6f19d3 100644 --- a/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts +++ b/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts @@ -1,13 +1,13 @@ import os from "os" import { InstallationVersion } from "../../installation/version" import { Effect, Option, Schema } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" -export const CloudflareAIGatewayPlugin = PluginV2.define({ - id: PluginV2.ID.make("cloudflare-ai-gateway"), - effect: Effect.gen(function* () { - return { - "aisdk.sdk": Effect.fn(function* (evt) { +export const CloudflareAIGatewayPlugin = define({ + id: "cloudflare-ai-gateway", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "ai-gateway-provider") return if (evt.options.baseURL) return @@ -24,14 +24,14 @@ export const CloudflareAIGatewayPlugin = PluginV2.define({ apiKey: config.apiKey, options: gatewayOptions(evt.options, metadata), } as any) - const unified = createUnified() + const unified = createUnified({ apiKey: config.apiKey }) evt.sdk = { languageModel(modelID: string) { return gateway(unified(modelID)) }, } }), - } + ) }), }) @@ -45,7 +45,7 @@ const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString) function gatewayConfig(options: Record): GatewayConfig | undefined { const accountId = process.env.CLOUDFLARE_ACCOUNT_ID ?? stringOption(options, "accountId") - // AccountPlugin copies CLI prompt metadata into options. The prompt stores the + // Credential projection copies key metadata into options. The prompt stores the // gateway as gatewayId, while older config examples may use gateway. const gatewayId = process.env.CLOUDFLARE_GATEWAY_ID ?? stringOption(options, "gatewayId") ?? stringOption(options, "gateway") diff --git a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts index 32cfb059f43..1a1c533eb55 100644 --- a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts +++ b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts @@ -1,38 +1,48 @@ import os from "os" import { InstallationVersion } from "../../installation/version" import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" import { ProviderV2 } from "../../provider" const providerID = ProviderV2.ID.make("cloudflare-workers-ai") -export const CloudflareWorkersAIPlugin = PluginV2.define({ - id: PluginV2.ID.make("cloudflare-workers-ai"), - effect: Effect.gen(function* () { - return { - "catalog.transform": Effect.fn(function* (evt) { +export const CloudflareWorkersAIPlugin = define({ + id: "cloudflare-workers-ai", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { const item = evt.provider.get(providerID) if (!item) return evt.provider.update(item.provider.id, (provider) => { - if (provider.endpoint.type !== "aisdk") return - if (provider.endpoint.url) return - const accountId = resolveAccountId(provider.options.aisdk.provider) - if (accountId) provider.endpoint.url = workersEndpoint(accountId) + if (provider.api.type !== "aisdk") return + if (provider.api.url) return + const accountId = resolveAccountId(provider.request.body) + if (accountId) provider.api.url = workersEndpoint(accountId) }) }), - "aisdk.sdk": Effect.fn(function* (evt) { + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.model.providerID !== providerID) return if (evt.package !== "@ai-sdk/openai-compatible") return - if (!hasWorkersEndpoint(evt.model.endpoint)) return + const accountId = resolveAccountId(evt.options) + if (!hasWorkersEndpoint(evt.model.api) && !accountId) return const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible")) - evt.sdk = mod.createOpenAICompatible(sdkOptions(evt.options) as any) + evt.sdk = mod.createOpenAICompatible( + sdkOptions({ + ...evt.options, + baseURL: evt.options.baseURL ?? (accountId ? workersEndpoint(accountId) : undefined), + }) as any, + ) }), - "aisdk.language": Effect.fn(function* (evt) { + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { if (evt.model.providerID !== providerID) return - evt.language = evt.sdk.languageModel(evt.model.apiID) + evt.language = evt.sdk.languageModel(evt.model.api.id) }), - } + ) }), }) @@ -44,8 +54,8 @@ function workersEndpoint(accountId: string) { return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1` } -function hasWorkersEndpoint(endpoint: ProviderV2.Endpoint) { - return endpoint.type === "aisdk" && Boolean(endpoint.url) +function hasWorkersEndpoint(api: ProviderV2.Api) { + return api.type === "aisdk" && Boolean(api.url) } function sdkOptions(options: Record) { diff --git a/packages/core/src/plugin/provider/cohere.ts b/packages/core/src/plugin/provider/cohere.ts index 991c370d175..0ca0708577a 100644 --- a/packages/core/src/plugin/provider/cohere.ts +++ b/packages/core/src/plugin/provider/cohere.ts @@ -1,15 +1,15 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" -export const CoherePlugin = PluginV2.define({ - id: PluginV2.ID.make("cohere"), - effect: Effect.gen(function* () { - return { - "aisdk.sdk": Effect.fn(function* (evt) { +export const CoherePlugin = define({ + id: "cohere", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/cohere") return const mod = yield* Effect.promise(() => import("@ai-sdk/cohere")) evt.sdk = mod.createCohere(evt.options) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/deepinfra.ts b/packages/core/src/plugin/provider/deepinfra.ts index bbd42f6e283..1b23e08ba4a 100644 --- a/packages/core/src/plugin/provider/deepinfra.ts +++ b/packages/core/src/plugin/provider/deepinfra.ts @@ -1,15 +1,15 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" -export const DeepInfraPlugin = PluginV2.define({ - id: PluginV2.ID.make("deepinfra"), - effect: Effect.gen(function* () { - return { - "aisdk.sdk": Effect.fn(function* (evt) { +export const DeepInfraPlugin = define({ + id: "deepinfra", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/deepinfra") return const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra")) evt.sdk = mod.createDeepInfra(evt.options) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/dynamic.ts b/packages/core/src/plugin/provider/dynamic.ts index e5abc7009e3..c84a6ed51f8 100644 --- a/packages/core/src/plugin/provider/dynamic.ts +++ b/packages/core/src/plugin/provider/dynamic.ts @@ -1,19 +1,19 @@ -import { Npm } from "../../npm" -import { Effect, Option } from "effect" +import { Effect } from "effect" import { pathToFileURL } from "url" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" +import { Npm } from "../../npm" -export const DynamicProviderPlugin = PluginV2.define({ - id: PluginV2.ID.make("dynamic-provider"), - effect: Effect.gen(function* () { +export const DynamicProviderPlugin = define({ + id: "dynamic-provider", + effect: Effect.fn(function* (ctx) { const npm = yield* Npm.Service - return { - "aisdk.sdk": Effect.fn(function* (evt) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.sdk) return const installedPath = evt.package.startsWith("file://") ? evt.package - : Option.getOrUndefined((yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint) + : (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`) const mod = yield* Effect.promise(async () => { @@ -26,6 +26,6 @@ export const DynamicProviderPlugin = PluginV2.define({ evt.sdk = mod[match](evt.options) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/gateway.ts b/packages/core/src/plugin/provider/gateway.ts index 5b08ad9ef5e..f097dcaca3f 100644 --- a/packages/core/src/plugin/provider/gateway.ts +++ b/packages/core/src/plugin/provider/gateway.ts @@ -1,15 +1,15 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" -export const GatewayPlugin = PluginV2.define({ - id: PluginV2.ID.make("gateway"), - effect: Effect.gen(function* () { - return { - "aisdk.sdk": Effect.fn(function* (evt) { +export const GatewayPlugin = define({ + id: "gateway", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/gateway") return const mod = yield* Effect.promise(() => import("@ai-sdk/gateway")) evt.sdk = mod.createGateway(evt.options) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/github-copilot.ts b/packages/core/src/plugin/provider/github-copilot.ts index 20b1ad6d6c3..ce210a67568 100644 --- a/packages/core/src/plugin/provider/github-copilot.ts +++ b/packages/core/src/plugin/provider/github-copilot.ts @@ -1,44 +1,52 @@ import { Effect } from "effect" import { ModelV2 } from "../../model" -import { PluginV2 } from "../../plugin" import { ProviderV2 } from "../../provider" +import type { PluginContext } from "@kilocode/plugin/v2/effect" -function shouldUseResponses(modelID: string) { - // Copilot supports Responses for GPT-5 class models, except mini variants - // which still need the chat-completions endpoint. - const match = /^gpt-(\d+)/.exec(modelID) - if (!match) return false - return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini") -} - -export const GithubCopilotPlugin = PluginV2.define({ - id: PluginV2.ID.make("github-copilot"), - effect: Effect.gen(function* () { - return { - "aisdk.sdk": Effect.fn(function* (evt) { +export const GithubCopilotPlugin = { + id: "github-copilot", + effect: Effect.fn(function* (ctx: PluginContext) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { + const item = evt.provider.get(ProviderV2.ID.githubCopilot) + if (!item || !item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) return + evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => { + // This chat-only alias conflicts with the Copilot GPT-5 Responses route, + // so hide it only for Copilot rather than for every provider catalog. + model.enabled = false + }) + }), + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/github-copilot") return const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider")) evt.sdk = mod.createOpenaiCompatible(evt.options) }), - "aisdk.language": Effect.fn(function* (evt) { + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) { - evt.language = evt.sdk.languageModel(evt.model.apiID) + evt.language = evt.sdk.languageModel(evt.model.api.id) return } - evt.language = shouldUseResponses(evt.model.apiID) - ? evt.sdk.responses(evt.model.apiID) - : evt.sdk.chat(evt.model.apiID) - }), - "catalog.transform": Effect.fn(function* (evt) { - const item = evt.provider.get(ProviderV2.ID.githubCopilot) - if (!item || !item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) return - evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => { - // This chat-only alias conflicts with the Copilot GPT-5 Responses route, - // so hide it only for Copilot rather than for every provider catalog. - model.enabled = false - }) + if (evt.options.endpoint === "responses" && evt.sdk.responses) { + evt.language = evt.sdk.responses(evt.model.api.id) + return + } + if (evt.options.endpoint === "chat" && evt.sdk.chat) { + evt.language = evt.sdk.chat(evt.model.api.id) + return + } + const match = /^gpt-(\d+)/.exec(evt.model.api.id) + // Copilot supports Responses for GPT-5 class models, except mini variants + // which still need the chat-completions endpoint. + evt.language = + match && Number(match[1]) >= 5 && !evt.model.api.id.startsWith("gpt-5-mini") && evt.sdk.responses + ? evt.sdk.responses(evt.model.api.id) + : evt.sdk.chat(evt.model.api.id) }), - } + ) }), -}) +} diff --git a/packages/core/src/plugin/provider/gitlab.ts b/packages/core/src/plugin/provider/gitlab.ts index 226f5a45eb4..8723cdaac2e 100644 --- a/packages/core/src/plugin/provider/gitlab.ts +++ b/packages/core/src/plugin/provider/gitlab.ts @@ -1,14 +1,14 @@ import os from "os" import { InstallationVersion } from "../../installation/version" import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" import { ProviderV2 } from "../../provider" -export const GitLabPlugin = PluginV2.define({ - id: PluginV2.ID.make("gitlab"), - effect: Effect.gen(function* () { - return { - "aisdk.sdk": Effect.fn(function* (evt) { +export const GitLabPlugin = define({ + id: "gitlab", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "gitlab-ai-provider") return const mod = yield* Effect.promise(() => import("gitlab-ai-provider")) evt.sdk = mod.createGitLab({ @@ -30,22 +30,22 @@ export const GitLabPlugin = PluginV2.define({ }, }) }), - "aisdk.language": Effect.fn(function* (evt) { + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.gitlab) return const featureFlags = typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {} - if (evt.model.apiID.startsWith("duo-workflow-")) { + if (evt.model.api.id.startsWith("duo-workflow-")) { const gitlab = yield* Effect.promise(() => import("gitlab-ai-provider")).pipe(Effect.orDie) const workflowRef = - typeof evt.model.options.aisdk.request.workflowRef === "string" - ? evt.model.options.aisdk.request.workflowRef - : undefined + typeof evt.model.request.body.workflowRef === "string" ? evt.model.request.body.workflowRef : undefined const workflowDefinition = - typeof evt.model.options.aisdk.request.workflowDefinition === "string" - ? evt.model.options.aisdk.request.workflowDefinition + typeof evt.model.request.body.workflowDefinition === "string" + ? evt.model.request.body.workflowDefinition : undefined const language = evt.sdk.workflowChat( - gitlab.isWorkflowModel(evt.model.apiID) ? evt.model.apiID : "duo-workflow", + gitlab.isWorkflowModel(evt.model.api.id) ? evt.model.api.id : "duo-workflow", { featureFlags, workflowDefinition, @@ -55,11 +55,11 @@ export const GitLabPlugin = PluginV2.define({ evt.language = language return } - evt.language = evt.sdk.agenticChat(evt.model.apiID, { + evt.language = evt.sdk.agenticChat(evt.model.api.id, { aiGatewayHeaders: evt.options.aiGatewayHeaders, featureFlags, }) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/google-vertex.ts b/packages/core/src/plugin/provider/google-vertex.ts index ae1692b933c..4e643c9f519 100644 --- a/packages/core/src/plugin/provider/google-vertex.ts +++ b/packages/core/src/plugin/provider/google-vertex.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" import { ProviderV2 } from "../../provider" function resolveProject(options: Record) { @@ -54,33 +54,38 @@ function authFetch(fetchWithRuntimeOptions?: unknown) { } } -export const GoogleVertexPlugin = PluginV2.define({ - id: PluginV2.ID.make("google-vertex"), - effect: Effect.gen(function* () { - return { - "catalog.transform": Effect.fn(function* (evt) { +export const GoogleVertexPlugin = define({ + id: "google-vertex", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue + if (item.provider.api.type !== "aisdk") continue if ( - item.provider.endpoint.package !== "@ai-sdk/google-vertex" && - !item.provider.endpoint.package.includes("@ai-sdk/openai-compatible") + item.provider.api.package !== "@ai-sdk/google-vertex" && + !( + item.provider.id === ProviderV2.ID.googleVertex && + item.provider.api.package.includes("@ai-sdk/openai-compatible") + ) ) continue - const project = resolveProject(item.provider.options.aisdk.provider) - const location = String(resolveLocation(item.provider.options.aisdk.provider)) + const project = resolveProject(item.provider.request.body) + const location = String(resolveLocation(item.provider.request.body)) evt.provider.update(item.provider.id, (provider) => { - if (project) provider.options.aisdk.provider.project = project - provider.options.aisdk.provider.location = location - if (provider.endpoint.type === "aisdk" && provider.endpoint.url) { - provider.endpoint.url = replaceVertexVars(provider.endpoint.url, project, location) + if (project) provider.request.body.project = project + provider.request.body.location = location + if (provider.api.type === "aisdk" && provider.api.url) { + provider.api.url = replaceVertexVars(provider.api.url, project, location) } - if (provider.endpoint.type === "aisdk" && provider.endpoint.package.includes("@ai-sdk/openai-compatible")) { - provider.options.aisdk.provider.fetch = authFetch(provider.options.aisdk.provider.fetch) + if (provider.api.type === "aisdk" && provider.api.package.includes("@ai-sdk/openai-compatible")) { + provider.request.body.fetch = authFetch(provider.request.body.fetch) } }) } }), - "aisdk.sdk": Effect.fn(function* (evt) { + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) { evt.options.fetch = authFetch(evt.options.fetch) return @@ -97,39 +102,43 @@ export const GoogleVertexPlugin = PluginV2.define({ location, }) }), - "aisdk.language": Effect.fn(function* (evt) { + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.googleVertex) return - evt.language = evt.sdk.languageModel(String(evt.model.apiID).trim()) + evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim()) }), - } + ) }), }) -export const GoogleVertexAnthropicPlugin = PluginV2.define({ - id: PluginV2.ID.make("google-vertex-anthropic"), - effect: Effect.gen(function* () { - return { - "catalog.transform": Effect.fn(function* (evt) { +export const GoogleVertexAnthropicPlugin = define({ + id: "google-vertex-anthropic", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/google-vertex/anthropic") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/google-vertex/anthropic") continue const project = - item.provider.options.aisdk.provider.project ?? + item.provider.request.body.project ?? process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCP_PROJECT ?? process.env.GCLOUD_PROJECT const location = - item.provider.options.aisdk.provider.location ?? + item.provider.request.body.location ?? process.env.GOOGLE_CLOUD_LOCATION ?? process.env.VERTEX_LOCATION ?? "global" evt.provider.update(item.provider.id, (provider) => { - if (project) provider.options.aisdk.provider.project = project - provider.options.aisdk.provider.location = location + if (project) provider.request.body.project = project + provider.request.body.location = location }) } }), - "aisdk.sdk": Effect.fn(function* (evt) { + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/google-vertex/anthropic") return const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic")) const project = @@ -153,10 +162,12 @@ export const GoogleVertexAnthropicPlugin = PluginV2.define({ : {}), }) }), - "aisdk.language": Effect.fn(function* (evt) { + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return - evt.language = evt.sdk.languageModel(String(evt.model.apiID).trim()) + evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim()) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/google.ts b/packages/core/src/plugin/provider/google.ts index 47e29c6b5d5..476af5b9121 100644 --- a/packages/core/src/plugin/provider/google.ts +++ b/packages/core/src/plugin/provider/google.ts @@ -1,15 +1,15 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" -export const GooglePlugin = PluginV2.define({ - id: PluginV2.ID.make("google"), - effect: Effect.gen(function* () { - return { - "aisdk.sdk": Effect.fn(function* (evt) { +export const GooglePlugin = define({ + id: "google", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/google") return const mod = yield* Effect.promise(() => import("@ai-sdk/google")) evt.sdk = mod.createGoogleGenerativeAI(evt.options) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/groq.ts b/packages/core/src/plugin/provider/groq.ts index f2052afd1a8..0bddb443095 100644 --- a/packages/core/src/plugin/provider/groq.ts +++ b/packages/core/src/plugin/provider/groq.ts @@ -1,15 +1,15 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" -export const GroqPlugin = PluginV2.define({ - id: PluginV2.ID.make("groq"), - effect: Effect.gen(function* () { - return { - "aisdk.sdk": Effect.fn(function* (evt) { +export const GroqPlugin = define({ + id: "groq", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/groq") return const mod = yield* Effect.promise(() => import("@ai-sdk/groq")) evt.sdk = mod.createGroq(evt.options) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/index.ts b/packages/core/src/plugin/provider/index.ts deleted file mode 100644 index fd02d322a1f..00000000000 --- a/packages/core/src/plugin/provider/index.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { AlibabaPlugin } from "./alibaba" -import { AmazonBedrockPlugin } from "./amazon-bedrock" -import { AnthropicPlugin } from "./anthropic" -import { AzureCognitiveServicesPlugin, AzurePlugin } from "./azure" -import { CerebrasPlugin } from "./cerebras" -import { CloudflareAIGatewayPlugin } from "./cloudflare-ai-gateway" -import { CloudflareWorkersAIPlugin } from "./cloudflare-workers-ai" -import { CoherePlugin } from "./cohere" -import { DeepInfraPlugin } from "./deepinfra" -import { DynamicProviderPlugin } from "./dynamic" -import { GatewayPlugin } from "./gateway" -import { GithubCopilotPlugin } from "./github-copilot" -import { GitLabPlugin } from "./gitlab" -import { GooglePlugin } from "./google" -import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "./google-vertex" -import { GroqPlugin } from "./groq" -import { KiloPlugin } from "./kilo" -import { LLMGatewayPlugin } from "./llmgateway" -import { MistralPlugin } from "./mistral" -import { NvidiaPlugin } from "./nvidia" -import { OpenAIPlugin } from "./openai" -import { OpenAICompatiblePlugin } from "./openai-compatible" -import { OpencodePlugin } from "./opencode" -import { OpenRouterPlugin } from "./openrouter" -import { PerplexityPlugin } from "./perplexity" -import { SapAICorePlugin } from "./sap-ai-core" -import { TogetherAIPlugin } from "./togetherai" -import { VercelPlugin } from "./vercel" -import { VenicePlugin } from "./venice" -import { XAIPlugin } from "./xai" -import { ZenmuxPlugin } from "./zenmux" - -export const ProviderPlugins = [ - AlibabaPlugin, - AmazonBedrockPlugin, - AnthropicPlugin, - AzureCognitiveServicesPlugin, - AzurePlugin, - CerebrasPlugin, - CloudflareAIGatewayPlugin, - CloudflareWorkersAIPlugin, - CoherePlugin, - DeepInfraPlugin, - GatewayPlugin, - GithubCopilotPlugin, - GitLabPlugin, - GooglePlugin, - GoogleVertexAnthropicPlugin, - GoogleVertexPlugin, - GroqPlugin, - KiloPlugin, - LLMGatewayPlugin, - MistralPlugin, - NvidiaPlugin, - OpencodePlugin, - OpenAICompatiblePlugin, - OpenAIPlugin, - OpenRouterPlugin, - PerplexityPlugin, - SapAICorePlugin, - TogetherAIPlugin, - VercelPlugin, - VenicePlugin, - XAIPlugin, - ZenmuxPlugin, - DynamicProviderPlugin, -] diff --git a/packages/core/src/plugin/provider/kilo.ts b/packages/core/src/plugin/provider/kilo.ts index 60b0482537a..5b7404947c8 100644 --- a/packages/core/src/plugin/provider/kilo.ts +++ b/packages/core/src/plugin/provider/kilo.ts @@ -1,45 +1,47 @@ import { createKilo, KILO_OPENROUTER_BASE } from "@kilocode/kilo-gateway" // kilocode_change import { Effect } from "effect" -import { PluginV2 } from "../../plugin" import { ProviderV2 } from "../../provider" // kilocode_change +import { define } from "../internal" -const id = ProviderV2.ID.make("kilo") // kilocode_change +const id = ProviderV2.ID.kilo // kilocode_change -export const KiloPlugin = PluginV2.define({ - id: PluginV2.ID.make("kilo"), - effect: Effect.gen(function* () { - return { - "catalog.transform": Effect.fn(function* (evt) { +export const KiloPlugin = define({ + id: "kilo", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { for (const item of evt.provider.list()) { if (item.provider.id !== id) continue // kilocode_change evt.provider.update(item.provider.id, (provider) => { // kilocode_change start - const options = provider.options.aisdk.provider + const options = provider.request.body const token = options.kilocodeToken ?? options.apiKey ?? process.env.KILO_API_KEY const org = process.env.KILO_ORG_ID ?? options.kilocodeOrganizationId - provider.endpoint = { + provider.api = { type: "aisdk", package: "@kilocode/kilo-gateway", url: KILO_OPENROUTER_BASE, } // kilocode_change end - provider.options.headers["HTTP-Referer"] = "https://kilo.ai/" + provider.request.headers["HTTP-Referer"] = "https://kilo.ai/" // kilocode_change start - provider.options.headers["X-Title"] = "Kilo Code" - options.kilocodeToken = token ?? "anonymous" + provider.request.headers["X-Title"] = "Kilo Code" + options.apiKey = token ?? "anonymous" + options.kilocodeToken = options.apiKey if (org) options.kilocodeOrganizationId = org - if (!provider.enabled) provider.enabled = { via: "custom", data: { anonymous: true } } // kilocode_change end }) } }), - // kilocode_change start - "aisdk.sdk": Effect.fn(function* (evt) { + ) + // kilocode_change start + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.model.providerID !== id) return evt.sdk = createKilo(evt.options) }), - // kilocode_change end - } + ) + // kilocode_change end }), }) diff --git a/packages/core/src/plugin/provider/llmgateway.ts b/packages/core/src/plugin/provider/llmgateway.ts index 818a30b971d..12416fd63c2 100644 --- a/packages/core/src/plugin/provider/llmgateway.ts +++ b/packages/core/src/plugin/provider/llmgateway.ts @@ -1,27 +1,30 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" +import { Integration } from "../../integration" import { ProviderV2 } from "../../provider" // kilocode_change -export const LLMGatewayPlugin = PluginV2.define({ - id: PluginV2.ID.make("llmgateway"), - effect: Effect.gen(function* () { - return { - "catalog.transform": Effect.fn(function* (evt) { +export const LLMGatewayPlugin = define({ + id: "llmgateway", + effect: Effect.fn(function* (ctx) { + const integrations = yield* Integration.Service + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.enabled === false) continue - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/openai-compatible") continue - if (item.provider.endpoint.url !== "https://api.llmgateway.io/v1") continue + if (item.provider.disabled) continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue + if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue if (item.provider.id !== ProviderV2.ID.make("llmgateway")) continue // kilocode_change + if (!(yield* integrations.get(Integration.ID.make(item.provider.id)))) continue evt.provider.update(item.provider.id, (provider) => { - provider.options.headers["HTTP-Referer"] = "https://kilo.ai/" // kilocode_change + provider.request.headers["HTTP-Referer"] = "https://kilo.ai/" // kilocode_change start - provider.options.headers["X-Title"] = "Kilo Code" - provider.options.headers["X-Source"] = "kilo" + provider.request.headers["X-Title"] = "Kilo Code" + provider.request.headers["X-Source"] = "kilo" // kilocode_change end }) } }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/mistral.ts b/packages/core/src/plugin/provider/mistral.ts index e7f0decb79e..a7319756598 100644 --- a/packages/core/src/plugin/provider/mistral.ts +++ b/packages/core/src/plugin/provider/mistral.ts @@ -1,15 +1,15 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" -export const MistralPlugin = PluginV2.define({ - id: PluginV2.ID.make("mistral"), - effect: Effect.gen(function* () { - return { - "aisdk.sdk": Effect.fn(function* (evt) { +export const MistralPlugin = define({ + id: "mistral", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/mistral") return const mod = yield* Effect.promise(() => import("@ai-sdk/mistral")) evt.sdk = mod.createMistral(evt.options) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/nvidia.ts b/packages/core/src/plugin/provider/nvidia.ts index 1cd0e40acc7..d27db3ad061 100644 --- a/packages/core/src/plugin/provider/nvidia.ts +++ b/packages/core/src/plugin/provider/nvidia.ts @@ -1,26 +1,26 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" import { ProviderV2 } from "../../provider" // kilocode_change -export const NvidiaPlugin = PluginV2.define({ - id: PluginV2.ID.make("nvidia"), - effect: Effect.gen(function* () { - return { - "catalog.transform": Effect.fn(function* (evt) { +export const NvidiaPlugin = define({ + id: "nvidia", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/openai-compatible") continue - if (item.provider.endpoint.url !== "https://integrate.api.nvidia.com/v1") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue + if (item.provider.api.url !== "https://integrate.api.nvidia.com/v1") continue if (item.provider.id !== ProviderV2.ID.make("nvidia")) continue // kilocode_change evt.provider.update(item.provider.id, (provider) => { - provider.options.headers["HTTP-Referer"] = "https://kilo.ai/" // kilocode_change + provider.request.headers["HTTP-Referer"] = "https://kilo.ai/" // kilocode_change // kilocode_change start - provider.options.headers["X-Title"] = "Kilo Code" - provider.options.headers["X-BILLING-INVOKE-ORIGIN"] ??= "KiloCode" + provider.request.headers["X-Title"] = "Kilo Code" + provider.request.headers["X-BILLING-INVOKE-ORIGIN"] ??= "KiloCode" // kilocode_change end }) } }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/openai-compatible.ts b/packages/core/src/plugin/provider/openai-compatible.ts index 76c33737066..d602ed0ff95 100644 --- a/packages/core/src/plugin/provider/openai-compatible.ts +++ b/packages/core/src/plugin/provider/openai-compatible.ts @@ -1,17 +1,17 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" -export const OpenAICompatiblePlugin = PluginV2.define({ - id: PluginV2.ID.make("openai-compatible"), - effect: Effect.gen(function* () { - return { - "aisdk.sdk": Effect.fn(function* (evt) { +export const OpenAICompatiblePlugin = define({ + id: "openai-compatible", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.sdk) return if (!evt.package.includes("@ai-sdk/openai-compatible")) return if (evt.options.includeUsage !== false) evt.options.includeUsage = true const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible")) evt.sdk = mod.createOpenAICompatible(evt.options as any) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/openai.ts b/packages/core/src/plugin/provider/openai.ts index 2d33fbcbbe1..2981dc0b9f0 100644 --- a/packages/core/src/plugin/provider/openai.ts +++ b/packages/core/src/plugin/provider/openai.ts @@ -1,25 +1,176 @@ -import { Effect } from "effect" +import { createServer } from "node:http" +import type { IntegrationOAuthMethodRegistration } from "@kilocode/plugin/v2/effect/integration" +import { define } from "@kilocode/plugin/v2/effect/plugin" +import { Deferred, Effect } from "effect" +import type { Scope } from "effect" +import { Credential } from "../../credential" +import { InstallationVersion } from "../../installation/version" +import { Integration } from "../../integration" +import { KiloOauthCallbackPage } from "../../kilocode/oauth/page" // kilocode_change import { ModelV2 } from "../../model" -import { PluginV2 } from "../../plugin" import { ProviderV2 } from "../../provider" +import type { PluginInternal } from "../internal" -export const OpenAIPlugin = PluginV2.define({ - id: PluginV2.ID.make("openai"), - effect: Effect.gen(function* () { - return { - "aisdk.sdk": Effect.fn(function* (evt) { - if (evt.package !== "@ai-sdk/openai") return - const mod = yield* Effect.promise(() => import("@ai-sdk/openai")) - evt.sdk = mod.createOpenAI(evt.options) - }), - "aisdk.language": Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.openai) return - evt.language = evt.sdk.responses(evt.model.apiID) - }), - "catalog.transform": Effect.fn(function* (evt) { +const clientID = "app_EMoamEEZ73f0CkXaXp7hrann" +const issuer = "https://auth.openai.com" +const callbackPort = 1455 +const pollingSafetyMargin = 3000 +const browserMethodID = Integration.MethodID.make("chatgpt-browser") +const headlessMethodID = Integration.MethodID.make("chatgpt-headless") + +type Pkce = { + verifier: string + challenge: string +} + +type TokenResponse = { + id_token: string + access_token: string + refresh_token: string + expires_in?: number +} + +type Claims = { + chatgpt_account_id?: string + organizations?: Array<{ id: string }> + "https://api.openai.com/auth"?: { chatgpt_account_id?: string } +} + +const browser = { + integrationID: Integration.ID.make("openai"), + method: { + id: browserMethodID, + type: "oauth", + label: "ChatGPT Pro/Plus (browser)", + }, + authorize: () => + Effect.gen(function* () { + const pkce = yield* Effect.promise(generatePKCE) + const state = base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)).buffer) + const code = yield* Deferred.make() + const redirect = `http://localhost:${callbackPort}/auth/callback` + const server = createServer((request, response) => { + const url = new URL(request.url ?? "/", `http://localhost:${callbackPort}`) + if (url.pathname !== "/auth/callback") { + response.writeHead(404).end("Not found") + return + } + // kilocode_change start - unrelated localhost requests must not terminate the active OAuth attempt + if (url.searchParams.get("state") !== state) { + response.writeHead(400, { "Content-Type": "text/html" }).end(KiloOauthCallbackPage.error("Invalid OAuth state")) + return + } + // kilocode_change end + const error = url.searchParams.get("error_description") ?? url.searchParams.get("error") + const value = url.searchParams.get("code") + if (error) { + Effect.runFork(Deferred.fail(code, new Error(error))) + response + .writeHead(400, { "Content-Type": "text/html" }) + .end(KiloOauthCallbackPage.error(error, { provider: "ChatGPT" })) // kilocode_change + return + } + if (!value) { + const message = "Missing authorization code" + Effect.runFork(Deferred.fail(code, new Error(message))) + response + .writeHead(400, { "Content-Type": "text/html" }) + .end(KiloOauthCallbackPage.error(message, { provider: "ChatGPT" })) // kilocode_change + return + } + Effect.runFork(Deferred.succeed(code, value)) + response + .writeHead(200, { "Content-Type": "text/html" }) + .end(KiloOauthCallbackPage.success({ provider: "ChatGPT" })) // kilocode_change + }) + yield* Effect.callback((resume) => { + server.once("error", (error) => resume(Effect.fail(error))) + server.listen(callbackPort, "localhost", () => resume(Effect.void)) + }) + yield* Effect.addFinalizer(() => Effect.sync(() => server.close())) + return { + mode: "auto" as const, + url: authorizeURL(redirect, pkce, state), + instructions: "Complete authorization in your browser. This window will close automatically.", + callback: Deferred.await(code).pipe( + Effect.flatMap((value) => exchange(value, redirect, pkce)), + Effect.map((tokens) => credential(browserMethodID, tokens)), + ), + } + }), + refresh: (value) => refresh(browserMethodID, value), +} satisfies IntegrationOAuthMethodRegistration + +const headless = { + integrationID: Integration.ID.make("openai"), + method: { + id: headlessMethodID, + type: "oauth", + label: "ChatGPT Pro/Plus (headless)", + }, + authorize: () => + Effect.gen(function* () { + const device = yield* request<{ device_auth_id: string; user_code: string; interval: string }>( + `${issuer}/api/accounts/deviceauth/usercode`, + { + method: "POST", + headers: headers("application/json"), + body: JSON.stringify({ client_id: clientID }), + }, + ) + const interval = Math.max(Number.parseInt(device.interval) || 5, 1) * 1000 + return { + mode: "auto" as const, + url: `${issuer}/codex/device`, + instructions: `Enter code: ${device.user_code}`, + callback: Effect.gen(function* () { + while (true) { + const response = yield* Effect.tryPromise({ + try: (signal) => + fetch(`${issuer}/api/accounts/deviceauth/token`, { + method: "POST", + headers: headers("application/json"), + body: JSON.stringify({ device_auth_id: device.device_auth_id, user_code: device.user_code }), + signal, + }), + catch: (cause) => cause, + }) + if (response.ok) { + const data = (yield* Effect.promise(() => response.json())) as { + authorization_code: string + code_verifier: string + } + return credential( + headlessMethodID, + yield* exchange(data.authorization_code, `${issuer}/deviceauth/callback`, { + verifier: data.code_verifier, + challenge: "", + }), + ) + } + if (response.status !== 403 && response.status !== 404) { + return yield* Effect.fail(new Error(`Device authorization failed: ${response.status}`)) + } + yield* Effect.sleep(interval + pollingSafetyMargin) + } + }), + } + }), + refresh: (value) => refresh(headlessMethodID, value), +} satisfies IntegrationOAuthMethodRegistration + +export const OpenAIPlugin = define({ + id: "openai", + effect: Effect.fn(function* (ctx) { + yield* ctx.integration.transform((draft) => { + draft.method.update(browser) + draft.method.update(headless) + }) + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/openai") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/openai") continue if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => { // OpenAIPlugin sends OpenAI models through Responses; this alias is a @@ -28,6 +179,122 @@ export const OpenAIPlugin = PluginV2.define({ }) } }), - } + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@ai-sdk/openai") return + const mod = yield* Effect.promise(() => import("@ai-sdk/openai")) + evt.sdk = mod.createOpenAI(evt.options) + }), + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { + if (evt.model.providerID !== ProviderV2.ID.openai) return + evt.language = evt.sdk.responses(evt.model.api.id) + }), + ) }), -}) +} satisfies PluginInternal.Plugin) + +function headers(contentType: string) { + return { "Content-Type": contentType, "User-Agent": `kilo/${InstallationVersion}` } // kilocode_change +} + +function exchange(code: string, redirect: string, pkce: Pkce) { + return request(`${issuer}/oauth/token`, { + method: "POST", + headers: headers("application/x-www-form-urlencoded"), + body: new URLSearchParams({ + grant_type: "authorization_code", + code, + redirect_uri: redirect, + client_id: clientID, + code_verifier: pkce.verifier, + }).toString(), + }) +} + +function refresh(methodID: Integration.MethodID, value: Pick) { + return request(`${issuer}/oauth/token`, { + method: "POST", + headers: headers("application/x-www-form-urlencoded"), + body: new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: value.refresh, + client_id: clientID, + }).toString(), + }).pipe( + Effect.map((tokens) => { + const next = credential(methodID, tokens) + return Credential.OAuth.make({ ...next, metadata: next.metadata ?? value.metadata }) + }), + ) +} + +function request(url: string, init: RequestInit) { + return Effect.tryPromise({ + try: async (signal) => { + const response = await fetch(url, { ...init, signal }) + if (!response.ok) throw new Error(`Request failed: ${response.status}`) + return response.json() as Promise + }, + catch: (cause) => cause, + }) +} + +function credential(methodID: Integration.MethodID, tokens: TokenResponse) { + const accountID = extractAccountID(tokens) + return Credential.OAuth.make({ + type: "oauth", + methodID, + refresh: tokens.refresh_token, + access: tokens.access_token, + expires: Date.now() + (tokens.expires_in ?? 3600) * 1000, + metadata: accountID ? { accountID } : undefined, + }) +} + +async function generatePKCE(): Promise { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" + const verifier = Array.from(crypto.getRandomValues(new Uint8Array(43)), (byte) => chars[byte % chars.length]).join("") + const challenge = base64UrlEncode(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))) + return { verifier, challenge } +} + +function base64UrlEncode(buffer: ArrayBuffer) { + return Buffer.from(buffer).toString("base64url") +} + +function authorizeURL(redirect: string, pkce: Pkce, state: string) { + return `${issuer}/oauth/authorize?${new URLSearchParams({ + response_type: "code", + client_id: clientID, + redirect_uri: redirect, + scope: "openid profile email offline_access", + code_challenge: pkce.challenge, + code_challenge_method: "S256", + id_token_add_organizations: "true", + codex_cli_simplified_flow: "true", + state, + originator: "kilo", // kilocode_change + })}` +} + +function extractAccountID(tokens: TokenResponse) { + return claim(tokens.id_token) ?? claim(tokens.access_token) +} + +function claim(token: string) { + const part = token.split(".")[1] + if (!part) return + try { + const claims = JSON.parse(Buffer.from(part, "base64url").toString()) as Claims + return ( + claims.chatgpt_account_id ?? + claims["https://api.openai.com/auth"]?.chatgpt_account_id ?? + claims.organizations?.[0]?.id + ) + } catch { + return + } +} diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index 64d20f8bd4e..3653e2cb8b0 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -1,32 +1,44 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "@kilocode/plugin/v2/effect/plugin" import { ProviderV2 } from "../../provider" -export const OpencodePlugin = PluginV2.define({ - id: PluginV2.ID.make("opencode"), - effect: Effect.gen(function* () { - let hasKey = false - return { - "catalog.transform": Effect.fn(function* (evt) { - const item = evt.provider.get(ProviderV2.ID.opencode) +// kilocode_change start - Kilo keeps only the free-tier catalog gate from upstream's opencode plugin. +// +// Upstream turned this plugin into a full identity + remote-config integration: an OAuth device +// flow against https://console.opencode.ai (client id "opencode-cli"), an "OpenCode Console +// account" login method, an "API key (service account)" method, and a fetch that lets that console +// drive Kilo's provider/model catalog. Kilo routes providers through the Kilo gateway and does not +// offer a competitor's account system as a sign-in option, so none of that is registered here. +// +// What remains is the behavior Kilo actually relies on and shipped before the v1.17.13 merge: +// gate the opencode ("zen") provider's paid models unless the user supplies a key, and mark the +// provider as "public" otherwise. The provider itself still reaches the catalog through models.dev +// sync (see catalog.ts), so this gate stays live. +// +// Do not restore the console auth flow on future merges without a product decision. +export const OpencodePlugin = define({ + id: "opencode", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (catalog) { + const item = catalog.provider.get(ProviderV2.ID.opencode) if (!item) return - hasKey = Boolean( - process.env.OPENCODE_API_KEY || - item.provider.env.some((env) => process.env[env]) || - item.provider.options.aisdk.provider.apiKey || - (item.provider.enabled && item.provider.enabled.via === "account"), - ) - evt.provider.update(item.provider.id, (provider) => { - if (!hasKey) provider.options.aisdk.provider.apiKey = "public" + // Read inside the transform so catalog reloads see current credentials, not a boot-time snapshot. + // A connection (env method, service-account key, ...) counts as credentials, exactly as before the merge. + const connected = (yield* ctx.integration.connection.active("opencode")) !== undefined + const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.request.body.apiKey) + catalog.provider.update(item.provider.id, (provider) => { + if (!hasKey) provider.request.body.apiKey = "public" }) if (hasKey) return for (const model of item.models.values()) { if (!model.cost.some((cost) => cost.input > 0)) continue - evt.model.update(item.provider.id, model.id, (draft) => { + catalog.model.update(item.provider.id, model.id, (draft) => { draft.enabled = false }) } }), - } + ) }), }) +// kilocode_change end diff --git a/packages/core/src/plugin/provider/openrouter.ts b/packages/core/src/plugin/provider/openrouter.ts index bbee2d62341..6f671aad5bc 100644 --- a/packages/core/src/plugin/provider/openrouter.ts +++ b/packages/core/src/plugin/provider/openrouter.ts @@ -1,20 +1,20 @@ import { Effect } from "effect" import { ModelV2 } from "../../model" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" import { ProviderV2 } from "../../provider" // kilocode_change -export const OpenRouterPlugin = PluginV2.define({ - id: PluginV2.ID.make("openrouter"), - effect: Effect.gen(function* () { - return { - "catalog.transform": Effect.fn(function* (evt) { +export const OpenRouterPlugin = define({ + id: "openrouter", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@openrouter/ai-sdk-provider") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue if (item.provider.id !== ProviderV2.ID.openrouter) continue // kilocode_change evt.provider.update(item.provider.id, (provider) => { - provider.options.headers["HTTP-Referer"] = "https://kilo.ai/" // kilocode_change - provider.options.headers["X-Title"] = "Kilo Code" // kilocode_change + provider.request.headers["HTTP-Referer"] = "https://kilo.ai/" // kilocode_change + provider.request.headers["X-Title"] = "Kilo Code" // kilocode_change }) for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) { if (!item.models.has(modelID)) continue @@ -26,11 +26,13 @@ export const OpenRouterPlugin = PluginV2.define({ } } }), - "aisdk.sdk": Effect.fn(function* (evt) { + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "@openrouter/ai-sdk-provider") return const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider")) evt.sdk = mod.createOpenRouter(evt.options) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/perplexity.ts b/packages/core/src/plugin/provider/perplexity.ts index 2415ab7c1a2..44c1ef2fc0a 100644 --- a/packages/core/src/plugin/provider/perplexity.ts +++ b/packages/core/src/plugin/provider/perplexity.ts @@ -1,15 +1,15 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" -export const PerplexityPlugin = PluginV2.define({ - id: PluginV2.ID.make("perplexity"), - effect: Effect.gen(function* () { - return { - "aisdk.sdk": Effect.fn(function* (evt) { +export const PerplexityPlugin = define({ + id: "perplexity", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/perplexity") return const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity")) evt.sdk = mod.createPerplexity(evt.options) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/sap-ai-core.ts b/packages/core/src/plugin/provider/sap-ai-core.ts index 7c57b785bff..8c668d8b414 100644 --- a/packages/core/src/plugin/provider/sap-ai-core.ts +++ b/packages/core/src/plugin/provider/sap-ai-core.ts @@ -1,15 +1,15 @@ -import { Npm } from "../../npm" -import { Effect, Option } from "effect" +import { Effect } from "effect" import { pathToFileURL } from "url" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" +import { Npm } from "../../npm" import { ProviderV2 } from "../../provider" -export const SapAICorePlugin = PluginV2.define({ - id: PluginV2.ID.make("sap-ai-core"), - effect: Effect.gen(function* () { +export const SapAICorePlugin = define({ + id: "sap-ai-core", + effect: Effect.fn(function* (ctx) { const npm = yield* Npm.Service - return { - "aisdk.sdk": Effect.fn(function* (evt) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return const serviceKey = process.env.AICORE_SERVICE_KEY ?? @@ -18,7 +18,7 @@ export const SapAICorePlugin = PluginV2.define({ const installedPath = evt.package.startsWith("file://") ? evt.package - : Option.getOrUndefined((yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint) + : (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`) const mod = yield* Effect.promise(async () => { @@ -35,10 +35,12 @@ export const SapAICorePlugin = PluginV2.define({ : {}, ) }), - "aisdk.language": Effect.fn(function* (evt) { + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return - evt.language = evt.sdk(evt.model.apiID) + evt.language = evt.sdk(evt.model.api.id) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/snowflake-cortex.ts b/packages/core/src/plugin/provider/snowflake-cortex.ts new file mode 100644 index 00000000000..788ac63eb03 --- /dev/null +++ b/packages/core/src/plugin/provider/snowflake-cortex.ts @@ -0,0 +1,89 @@ +import { Effect } from "effect" +import { define } from "../internal" +import { ProviderV2 } from "../../provider" + +type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise + +// Exported for testing: intercepts Cortex-specific request/response quirks. +export function cortexFetch(upstream: FetchLike = fetch) { + return async (url: string | URL | Request, init?: RequestInit): Promise => { + if (init?.body && typeof init.body === "string") { + try { + const body = JSON.parse(init.body) + if ("max_tokens" in body) { + body.max_completion_tokens = body.max_tokens + delete body.max_tokens + init = { ...init, body: JSON.stringify(body) } + } + } catch {} + } + + const response = await upstream(url, init) + + // Cortex returns 400 "conversation complete" as a normal stop condition + if (!response.ok && response.status === 400) { + try { + const errorData = (await response.clone().json()) as Record + if ( + String(errorData.message || errorData.error || "") + .toLowerCase() + .includes("conversation complete") + ) { + return new Response( + JSON.stringify({ choices: [{ finish_reason: "stop", message: { content: "", role: "assistant" } }] }), + { status: 200, headers: new Headers({ "content-type": "application/json" }) }, + ) + } + } catch {} + } + + // Cortex returns role:"" in streaming deltas; the AI SDK schema requires "assistant" + if (response.body && response.headers.get("content-type")?.includes("text/event-stream")) { + const reader = response.body.getReader() + const encoder = new TextEncoder() + const decoder = new TextDecoder() + const stream = new ReadableStream({ + async pull(ctrl) { + const { done, value } = await reader.read() + if (done) { + ctrl.close() + return + } + ctrl.enqueue( + encoder.encode(decoder.decode(value, { stream: true }).replace(/"role"\s*:\s*""/g, '"role":"assistant"')), + ) + }, + cancel() { + reader.cancel() + }, + }) + return new Response(stream, { headers: response.headers, status: response.status }) + } + + return response + } +} + +export const SnowflakeCortexPlugin = define({ + id: "snowflake-cortex", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return + const token = + process.env.SNOWFLAKE_CORTEX_TOKEN ?? + process.env.SNOWFLAKE_CORTEX_PAT ?? + (typeof evt.options.token === "string" ? evt.options.token : undefined) ?? + (typeof evt.options.apiKey === "string" ? evt.options.apiKey : undefined) + const upstream = typeof evt.options.fetch === "function" ? (evt.options.fetch as FetchLike) : undefined + if (evt.options.includeUsage !== false) evt.options.includeUsage = true + const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible")) + evt.sdk = mod.createOpenAICompatible({ + ...evt.options, + ...(token ? { apiKey: token } : {}), + fetch: cortexFetch(upstream) as typeof fetch, + } as any) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/togetherai.ts b/packages/core/src/plugin/provider/togetherai.ts index b1870f26625..8022e0de668 100644 --- a/packages/core/src/plugin/provider/togetherai.ts +++ b/packages/core/src/plugin/provider/togetherai.ts @@ -1,15 +1,15 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" -export const TogetherAIPlugin = PluginV2.define({ - id: PluginV2.ID.make("togetherai"), - effect: Effect.gen(function* () { - return { - "aisdk.sdk": Effect.fn(function* (evt) { +export const TogetherAIPlugin = define({ + id: "togetherai", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/togetherai") return const mod = yield* Effect.promise(() => import("@ai-sdk/togetherai")) evt.sdk = mod.createTogetherAI(evt.options) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/venice.ts b/packages/core/src/plugin/provider/venice.ts index 8a3b950245c..1a602ffd50b 100644 --- a/packages/core/src/plugin/provider/venice.ts +++ b/packages/core/src/plugin/provider/venice.ts @@ -1,15 +1,15 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" -export const VenicePlugin = PluginV2.define({ - id: PluginV2.ID.make("venice"), - effect: Effect.gen(function* () { - return { - "aisdk.sdk": Effect.fn(function* (evt) { +export const VenicePlugin = define({ + id: "venice", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "venice-ai-sdk-provider") return const mod = yield* Effect.promise(() => import("venice-ai-sdk-provider")) evt.sdk = mod.createVenice(evt.options) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/vercel.ts b/packages/core/src/plugin/provider/vercel.ts index 961bbd2ea5d..1f07f702a43 100644 --- a/packages/core/src/plugin/provider/vercel.ts +++ b/packages/core/src/plugin/provider/vercel.ts @@ -1,27 +1,29 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" import { ProviderV2 } from "../../provider" // kilocode_change -export const VercelPlugin = PluginV2.define({ - id: PluginV2.ID.make("vercel"), - effect: Effect.gen(function* () { - return { - "catalog.transform": Effect.fn(function* (evt) { +export const VercelPlugin = define({ + id: "vercel", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/vercel") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/vercel") continue if (item.provider.id !== ProviderV2.ID.make("vercel")) continue // kilocode_change evt.provider.update(item.provider.id, (provider) => { - provider.options.headers["http-referer"] = "https://kilo.ai/" // kilocode_change - provider.options.headers["x-title"] = "Kilo Code" // kilocode_change + provider.request.headers["http-referer"] = "https://kilo.ai/" // kilocode_change + provider.request.headers["x-title"] = "Kilo Code" // kilocode_change }) } }), - "aisdk.sdk": Effect.fn(function* (evt) { + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/vercel") return const mod = yield* Effect.promise(() => import("@ai-sdk/vercel")) evt.sdk = mod.createVercel(evt.options) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/xai.ts b/packages/core/src/plugin/provider/xai.ts index b54aa7374c6..8145a3480a0 100644 --- a/packages/core/src/plugin/provider/xai.ts +++ b/packages/core/src/plugin/provider/xai.ts @@ -1,20 +1,22 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" import { ProviderV2 } from "../../provider" -export const XAIPlugin = PluginV2.define({ - id: PluginV2.ID.make("xai"), - effect: Effect.gen(function* () { - return { - "aisdk.sdk": Effect.fn(function* (evt) { +export const XAIPlugin = define({ + id: "xai", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/xai") return const mod = yield* Effect.promise(() => import("@ai-sdk/xai")) evt.sdk = mod.createXai(evt.options) }), - "aisdk.language": Effect.fn(function* (evt) { + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.make("xai")) return - evt.language = evt.sdk.responses(evt.model.apiID) + evt.language = evt.sdk.responses(evt.model.api.id) }), - } + ) }), }) diff --git a/packages/core/src/plugin/provider/zenmux.ts b/packages/core/src/plugin/provider/zenmux.ts index 2433b4385f8..327fb1f1619 100644 --- a/packages/core/src/plugin/provider/zenmux.ts +++ b/packages/core/src/plugin/provider/zenmux.ts @@ -1,23 +1,23 @@ import { Effect } from "effect" -import { PluginV2 } from "../../plugin" +import { define } from "../internal" import { ProviderV2 } from "../../provider" // kilocode_change -export const ZenmuxPlugin = PluginV2.define({ - id: PluginV2.ID.make("zenmux"), - effect: Effect.gen(function* () { - return { - "catalog.transform": Effect.fn(function* (evt) { +export const ZenmuxPlugin = define({ + id: "zenmux", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { for (const item of evt.provider.list()) { - if (item.provider.endpoint.type !== "aisdk") continue - if (item.provider.endpoint.package !== "@ai-sdk/openai-compatible") continue - if (item.provider.endpoint.url !== "https://zenmux.ai/api/v1") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue + if (item.provider.api.url !== "https://zenmux.ai/api/v1") continue if (item.provider.id !== ProviderV2.ID.make("zenmux")) continue // kilocode_change evt.provider.update(item.provider.id, (provider) => { - provider.options.headers["HTTP-Referer"] ??= "https://kilo.ai/" // kilocode_change - provider.options.headers["X-Title"] ??= "Kilo Code" // kilocode_change + provider.request.headers["HTTP-Referer"] ??= "https://kilo.ai/" // kilocode_change + provider.request.headers["X-Title"] ??= "Kilo Code" // kilocode_change }) } }), - } + ) }), }) diff --git a/packages/core/src/plugin/skill.ts b/packages/core/src/plugin/skill.ts new file mode 100644 index 00000000000..ea723dd89de --- /dev/null +++ b/packages/core/src/plugin/skill.ts @@ -0,0 +1,31 @@ +/// + +export * as SkillPlugin from "./skill" + +import { define } from "./internal" +import { Effect } from "effect" +import { AbsolutePath } from "../schema" +import { SkillV2 } from "../skill" +import customizeOpencodeContent from "./skill/customize-opencode.md" with { type: "text" } + +export const CustomizeOpencodeContent = customizeOpencodeContent + +export const Plugin = define({ + id: "skill", + effect: Effect.fn(function* (ctx) { + yield* ctx.skill.transform((draft) => { + draft.source( + SkillV2.EmbeddedSource.make({ + type: "embedded", + skill: SkillV2.Info.make({ + name: "customize-opencode", + description: + "Use ONLY when the user is editing or creating opencode's own configuration: opencode.json, opencode.jsonc, files under .opencode/, or files under ~/.config/opencode/. Also use when creating or fixing opencode agents, subagents, commands, skills, plugins, MCP servers, or permission rules. Do not use for the user's own application code, or for any project that is not configuring opencode itself.", + location: AbsolutePath.make("/builtin/customize-opencode.md"), + content: CustomizeOpencodeContent, + }), + }), + ) + }) + }), +}) diff --git a/packages/core/src/plugin/skill/customize-opencode.md b/packages/core/src/plugin/skill/customize-opencode.md new file mode 100644 index 00000000000..6d8beba6e0b --- /dev/null +++ b/packages/core/src/plugin/skill/customize-opencode.md @@ -0,0 +1,453 @@ + + +# Customizing opencode + +opencode validates its own config strictly and refuses to start when a field +is wrong. The shapes below cover the common surface area, but they are a +**summary, not the source of truth**. + +## Full schema reference + +The authoritative list of every config option — with field types, enums, +defaults, and descriptions — lives in the published JSON Schema: + +**** + +If a field is not documented in this skill, or you need to confirm an exact +shape before writing config, **fetch that URL and read the schema directly** +rather than guessing. opencode hard-fails on invalid config, so the cost of a +wrong shape is a broken startup. + +Independently, every `opencode.json` should declare +`"$schema": "https://opencode.ai/config.json"` so the user's editor catches +mistakes as they type. + +## Applying changes + +Config is loaded once when opencode starts and is not hot-reloaded. After +saving changes to `opencode.json`, an agent file, a skill, a plugin, or any +other config-time file, **tell the user to quit and restart opencode** for +the changes to take effect. The running session will keep using the +already-loaded config until then. + +## Where files live + +| Scope | Path | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| Project config | `./opencode.json`, `./opencode.jsonc`, or `.opencode/opencode.json` (opencode walks up from the cwd to the worktree root) | +| Global config | `~/.config/opencode/opencode.json` (NOT `~/.opencode/`) | +| Project agents | `.opencode/agent/.md` or `.opencode/agents/.md` | +| Global agents | `~/.config/opencode/agent(s)/.md` | +| Project commands | `.opencode/command/.md` or `.opencode/commands/.md` | +| Global commands | `~/.config/opencode/command(s)/.md` | +| Project skills | `.opencode/skill(s)//SKILL.md` | +| Global skills | `~/.config/opencode/skill(s)//SKILL.md` | +| External skills (auto-loaded) | `~/.claude/skills//SKILL.md`, `~/.agents/skills//SKILL.md` | + +Configs from each scope are deep-merged. Project overrides global. Unknown +top-level keys in `opencode.json` are rejected with `ConfigInvalidError`. + +## opencode.json + +Every field is optional. + +```json +{ + "$schema": "https://opencode.ai/config.json", + "username": "string", + "model": "provider/model-id", + "small_model": "provider/model-id", + "default_agent": "agent-name", + "shell": "/bin/zsh", + "logLevel": "DEBUG" | "INFO" | "WARN" | "ERROR", + "share": "manual" | "auto" | "disabled", + "autoupdate": true | false | "notify", + "snapshot": true, + "instructions": ["AGENTS.md", "docs/style.md"], + + "skills": { + "paths": [".opencode/skills", "/abs/path/to/skills"], + "urls": ["https://example.com/.well-known/skills/"] + }, + + "references": { + "docs": { + "path": "../docs", + "description": "Use for product behavior and documentation conventions" + }, + "sdk": { + "repository": "owner/sdk", + "branch": "main", + "description": "Use for SDK implementation details", + "hidden": true + } + }, + + "agent": { + "my-agent": { + "model": "anthropic/claude-sonnet-4-6", + "mode": "subagent", + "description": "...", + "permission": { "edit": "deny" } + } + }, + + "command": { + "deploy": { "description": "...", "template": "..." } + }, + + "provider": { + "anthropic": { "options": { "apiKey": "..." } } + }, + "disabled_providers": ["openai"], + "enabled_providers": ["anthropic"], + + "mcp": { + "playwright": { + "type": "local", + "command": ["npx", "-y", "@playwright/mcp"], + "enabled": true, + "environment": {} + }, + "remote-thing": { + "type": "remote", + "url": "https://...", + "headers": { "Authorization": "Bearer ..." } + } + }, + + "plugin": [ + "opencode-gemini-auth", + "opencode-foo@1.2.3", + "./local-plugin.ts", + ["opencode-bar", { "option": "value" }] + ], + + "permission": { + "edit": "deny", + "bash": { "git *": "allow", "*": "ask" } + }, + + "formatter": false, + "lsp": false, + + "experimental": { + "primary_tools": ["edit"], + "mcp_timeout": 30000 + }, + + "tool_output": { "max_lines": 200, "max_bytes": 8192 }, + + "compaction": { "auto": true, "tail_turns": 15 } +} +``` + +Shape notes worth being explicit about: + +- `model` always carries a provider prefix: `"anthropic/claude-sonnet-4-6"`. +- `skills` is an object with `paths` and/or `urls`, not an array. +- `references` is an object keyed by alias. Each value is a local path, Git repository, or string shorthand. +- `agent` is an object keyed by agent name, not an array. +- `command` is an object keyed by command name, not an array. +- `plugin` is an array of strings or `[name, options]` tuples, not an object. +- `mcp[name].command` is an array of strings, never a single string. `type` is required. +- `permission` is either a string action or an object keyed by tool name. + +## Skills + +opencode's skill loader scans for `**/SKILL.md` inside skill directories. The +file is named `SKILL.md` exactly, and lives in its own folder named after the +skill: + +``` +.opencode/skills/my-skill/SKILL.md +``` + +Frontmatter: + +```markdown +--- +name: my-skill +description: One sentence covering what this skill does AND when to trigger it. Front-load the literal keywords or filenames the user is likely to say. +--- + +# My Skill + +(skill body in markdown: instructions, examples, references) +``` + +- `name` is required, lowercase hyphen-separated, up to 64 chars, and matches the folder name. +- `description` is effectively required: skills without one are filtered out and never surfaced to the model. Cover both _what_ the skill does and _when_ to use it. Write in third person ("Use when...", not "I help with..."). Front-load concrete trigger keywords and filenames; gate with "Use ONLY when..." if the skill should stay quiet on adjacent topics. +- Optional: `license`, `compatibility`, `metadata` (string-string map). + +Register skills from non-default locations via `skills.paths` (scanned +recursively for `**/SKILL.md`) and `skills.urls` (each URL serves a list of +skills). + +## References + +References make local directories and Git repositories outside the active +project available as supporting context. Configure them under `references`, +keyed by the alias used in `@` autocomplete: + +```json +{ + "references": { + "docs": { + "path": "../product-docs", + "description": "Use for product behavior and terminology" + }, + "effect": { + "repository": "Effect-TS/effect", + "branch": "main", + "description": "Use for Effect implementation details" + } + } +} +``` + +Local `path` values may be relative to the declaring config, absolute, or use +`~/`. Git `repository` values accept Git URLs, host/path references, and GitHub +`owner/repo` shorthand; `branch` is optional. Both forms support optional +`description` and `hidden` fields. + +- Only references with a `description` are advertised to agents in system context. +- `hidden: true` removes a reference from TUI `@` autocomplete only. It remains available to agents and by direct path. +- Reference directories are automatically allowed through the external-directory boundary; normal read/edit/tool permissions still apply. +- String shorthand is supported: use `"docs": "../docs"` for local paths or `"effect": "Effect-TS/effect"` for Git repositories. + +## Agents + +Two ways to define an agent. Use the file form for anything non-trivial. + +### Inline (in `opencode.json`) + +```json +{ + "agent": { + "my-reviewer": { + "description": "Reviews PRs for style violations.", + "mode": "subagent", + "model": "anthropic/claude-sonnet-4-6", + "permission": { "edit": "deny", "bash": "ask" }, + "prompt": "You are a strict PR reviewer..." + } + } +} +``` + +### File + +``` +.opencode/agent/my-reviewer.md OR .opencode/agents/my-reviewer.md +``` + +```markdown +--- +description: Reviews PRs for style violations. +mode: subagent +model: anthropic/claude-sonnet-4-6 +permission: + edit: deny + bash: ask +--- + +You are a strict PR reviewer. Focus on... +``` + +The file body becomes the agent's `prompt`. Do not also put `prompt:` in the +frontmatter. + +`mode` is one of `"primary"`, `"subagent"`, `"all"`. + +Allowed top-level frontmatter fields: `name, model, variant, description, mode, +hidden, color, steps, options, permission, disable, temperature, top_p`. Any +unknown field is silently routed into `options`. + +To disable a built-in agent: `agent: { build: { disable: true } }`, or in a +file, `disable: true` in frontmatter. + +`default_agent` must point to a non-hidden, primary-mode agent. + +### Built-in agents + +opencode ships with `build`, `plan`, `general`, `explore`. Hidden internal agents: +`compaction`, `title`, `summary`. To override a built-in's fields, define the +same key in `agent: { : { ... } }`. + +## Commands + +opencode's command loader scans for `**/*.md` inside command directories. The +file is named after the command, and lives directly inside the `command` folder: + +``` +.opencode/command/deploy.md +``` + +Frontmatter: + +```markdown +--- +description: One sentence describing what the command does. +agent: build +model: anthropic/claude-sonnet-4-6 +--- + +(command body in markdown: the prompt opencode runs, with $ARGUMENTS for the user's input) +``` + +- `template` is the command body — everything below the frontmatter — and is required: it is the prompt opencode runs when the command is invoked. Do not also put a `template:` key in the frontmatter. +- `$ARGUMENTS` is replaced with everything the user typed after the command; `$1`, `$2`, … pull individual positional arguments. +- Optional: `description`, `agent`, `model`, `variant`, `subtask`. + +## Plugins + +`plugin:` is an array. Each entry is one of: + +```json +"plugin": [ + "opencode-gemini-auth", // npm spec, latest + "opencode-foo@1.2.3", // npm spec, pinned + "./local-plugin.ts", // file path, relative to the declaring config + "file:///abs/path/plugin.js", // file URL + ["opencode-bar", { "key": "val" }] // tuple form with options +] +``` + +Auto-discovered plugins (no config entry needed): any `*.ts` or `*.js` file in +`.opencode/plugin/` or `.opencode/plugins/`. + +A plugin module exports `default` (or any named export) of type +`Plugin = (input: PluginInput, options?) => Promise`. The export is a +function, not a plain object literal, and the function returns an object +(return `{}` if there is nothing to register). + +```ts +import type { Plugin } from "@kilocode/plugin" + +export default (async ({ client, project, directory, $ }) => { + return { + config: (cfg) => { + // cfg is the live merged config; mutate fields here. + }, + "tool.execute.before": async (input, output) => { + // mutate output.args before the tool runs + }, + } +}) satisfies Plugin +``` + +Hook surface (mutate `output` in place; return `void`): + +- `event(input)`: every bus event +- `config(cfg)`: once on init with the merged config +- `chat.message`, `chat.params`, `chat.headers` +- `tool.execute.before`, `tool.execute.after` +- `tool.definition` +- `command.execute.before` +- `shell.env` +- `permission.ask` +- `experimental.chat.messages.transform`, `experimental.chat.system.transform`, + `experimental.session.compacting`, `experimental.compaction.autocontinue`, + `experimental.text.complete` + +Special object-shaped (not callbacks): `tool: { my_tool: { ... } }`, +`auth: { ... }`, `provider: { ... }`. + +## MCP servers + +`mcp:` is an object keyed by server name. Each server is discriminated by +`type`: + +```json +{ + "mcp": { + "playwright": { + "type": "local", + "command": ["npx", "-y", "@playwright/mcp"], + "enabled": true, + "environment": { "BROWSER": "chromium" } + }, + "github": { + "type": "remote", + "url": "https://...", + "enabled": true, + "headers": { "Authorization": "Bearer {env:GITHUB_TOKEN}" } + }, + "old-server": { "enabled": false } + } +} +``` + +`command` is an array of strings. `environment` sets environment variables for +a local MCP server. `type` is required. Use `enabled: false` to +disable a server inherited from a parent config. String values such as header +tokens support `{env:VAR}` interpolation (and `{file:path}`); the shell-style +`${VAR}` is not substituted. + +## Permissions + +```json +"permission": { + "edit": "deny", + "bash": { "git *": "allow", "rm *": "deny", "*": "ask" }, + "external_directory": { "~/secrets/**": "deny", "*": "allow" } +} +``` + +Actions: `"allow"`, `"ask"`, `"deny"`. + +Per-tool value forms: `"allow"` shorthand (treated as `{"*": "allow"}`), or an +object `{ pattern: action }`. Within an object, **insertion order matters**. +opencode evaluates the LAST matching rule, so put broad rules first and narrow +rules last. + +`permission: "allow"` (a string at the top level) is shorthand for "allow +everything" and is rarely what the user wants. + +Known permission keys: `read, edit, glob, grep, list, bash, task, +external_directory, todowrite, question, webfetch, websearch, lsp, doom_loop, +skill`. Some of these (`todowrite, +question, webfetch, websearch, doom_loop`) only accept a flat +action, not a per-pattern object. + +`external_directory` patterns are filesystem paths (use `~/`, absolute paths, +or globs like `~/projects/**`). + +Per-agent `permission:` overrides top-level `permission:`. Plan Mode lives on +the `plan` agent's permission ruleset (`edit: deny *`). + +## Escape hatches + +When a user's config is broken and opencode won't start, these env vars help: + +- `KILO_DISABLE_PROJECT_CONFIG=1`: skip the project's local `opencode.json` + and start from globals only. Run from the project directory, opencode loads, + the user edits the broken file, then they restart without the flag. +- `KILO_CONFIG=/path/to/file.json`: load an additional explicit config. +- `KILO_CONFIG_CONTENT='{"$schema":"https://opencode.ai/config.json"}'`: + inject inline JSON as a final local-scope merge. +- `KILO_DISABLE_DEFAULT_PLUGINS=1`: skip default plugins. +- `KILO_PURE=1`: skip external plugins entirely. +- `KILO_DISABLE_EXTERNAL_SKILLS=1`, + `KILO_DISABLE_CLAUDE_CODE_SKILLS=1`: skip the external skill scans under + `~/.claude/` and `~/.agents/`. + +## When proposing edits + +- Validate against the schema before writing. If you are unsure of a field's + exact shape, or the field is not covered in this skill, fetch + `https://opencode.ai/config.json` and read the schema rather than guessing. +- Preserve `$schema` and any existing fields the user did not ask to change. +- For agent, command, skill, and plugin definitions, prefer creating new files + in the correct location over inlining everything in `opencode.json`. +- If the user's existing config is malformed, point them at the env-var escape + hatches above so they can edit from inside opencode without breaking their + session. +- After saving any config change, remind the user to quit and restart opencode + — running sessions keep using the already-loaded config. diff --git a/packages/core/src/plugin/variant.ts b/packages/core/src/plugin/variant.ts new file mode 100644 index 00000000000..8576c4304b3 --- /dev/null +++ b/packages/core/src/plugin/variant.ts @@ -0,0 +1,39 @@ +export * as VariantPlugin from "./variant" + +import type { ModelV2Info } from "@kilocode/sdk/v2/types" +import { Effect } from "effect" +import { define } from "./internal" + +export const Plugin = define({ + id: "variant", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform((catalog) => { + for (const record of catalog.provider.list()) { + for (const model of record.models.values()) { + catalog.model.update(model.providerID, model.id, (draft) => { + const generated = generate(draft) + if (generated.length === 0) return + + const explicit = new Map(draft.variants.map((variant) => [variant.id, variant])) + const generatedIDs = new Set(generated.map((variant) => variant.id)) + draft.variants = [ + ...generated.map((variant) => explicit.get(variant.id) ?? variant), + ...draft.variants.filter((variant) => !generatedIDs.has(variant.id)), + ] + }) + } + } + }) + }), +}) + +export function generate(model: ModelV2Info): ModelV2Info["variants"] { + if (model.api.type !== "aisdk" || model.api.package !== "@ai-sdk/openai-compatible") return [] + const ids = `${model.id} ${model.api.id}`.toLowerCase() + if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return [] + return ["high", "max"].map((id) => ({ + id, + headers: {}, + body: { reasoning_effort: id }, + })) +} diff --git a/packages/core/src/policy.ts b/packages/core/src/policy.ts index 78bd74f1cd4..a2adebb54e4 100644 --- a/packages/core/src/policy.ts +++ b/packages/core/src/policy.ts @@ -1,5 +1,6 @@ export * as Policy from "./policy" +import { makeLocationNode } from "./effect/app-node" import { Context, Effect as EffectRuntime, Layer, Schema } from "effect" import { Wildcard } from "./util/wildcard" import { Location } from "./location" @@ -16,11 +17,12 @@ export class Info extends Schema.Class("Policy.Info")({ export interface Interface { readonly load: (statements: Info[]) => EffectRuntime.Effect readonly evaluate: (action: string, resource: string, fallback: Effect) => EffectRuntime.Effect + readonly hasStatements: () => boolean } export class Service extends Context.Service()("@opencode/v2/Policy") {} -export const layer = Layer.effect( +const layer = Layer.effect( Service, EffectRuntime.gen(function* () { let statements: Info[] = [] @@ -30,6 +32,7 @@ export const layer = Layer.effect( load: EffectRuntime.fn("Policy.load")(function* (input) { statements = input }), + hasStatements: () => statements.length > 0, evaluate: EffectRuntime.fn("Policy.evaluate")(function* (action, resource, fallback) { return ( statements.findLast( @@ -41,4 +44,6 @@ export const layer = Layer.effect( }), ) -export const defaultLayer = layer +export const locationLayer = layer + +export const node = makeLocationNode({ service: Service, layer, deps: [Location.node] }) diff --git a/packages/core/src/process.ts b/packages/core/src/process.ts index f076ea4e42c..16e9118d1f5 100644 --- a/packages/core/src/process.ts +++ b/packages/core/src/process.ts @@ -3,15 +3,24 @@ import type { PlatformError } from "effect/PlatformError" import { ChildProcess } from "effect/unstable/process" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" import { CrossSpawnSpawner } from "./cross-spawn-spawner" +import { makeGlobalNode } from "./effect/app-node" export class AppProcessError extends Schema.TaggedErrorClass()("AppProcessError", { command: Schema.String, exitCode: Schema.optional(Schema.Number), stderr: Schema.optional(Schema.String), - cause: Schema.optional(Schema.Defect), -}) {} + cause: Schema.optional(Schema.Defect()), +}) { + override get message() { + const detail = + this.stderr?.trim() || (this.cause instanceof Error ? this.cause.message : this.cause && String(this.cause)) + const status = this.exitCode === undefined ? "" : ` (exit ${this.exitCode})` + return `Command failed${status}: ${this.command}${detail ? `: ${detail}` : ""}` + } +} export interface RunOptions { + readonly combineOutput?: boolean readonly maxOutputBytes?: number readonly maxErrorBytes?: number readonly signal?: AbortSignal @@ -29,8 +38,10 @@ export interface RunStreamOptions { export interface RunResult { readonly command: string readonly exitCode: number + readonly output?: Buffer readonly stdout: Buffer readonly stderr: Buffer + readonly outputTruncated?: boolean readonly stdoutTruncated: boolean readonly stderrTruncated: boolean } @@ -79,7 +90,7 @@ const describeCommand = (command: ChildProcess.Command): string => { const wrapError = (description: string, cause: unknown): AppProcessError => cause instanceof AppProcessError ? cause : new AppProcessError({ command: description, cause }) -const abortError = (signal: AbortSignal): Error => { +export const abortError = (signal: AbortSignal): Error => { const reason = signal.reason if (reason instanceof Error) return reason const err = new Error("Aborted") @@ -87,7 +98,7 @@ const abortError = (signal: AbortSignal): Error => { return err } -const waitForAbort = (signal: AbortSignal) => +export const waitForAbort = (signal: AbortSignal) => Effect.callback((resume) => { if (signal.aborted) { resume(Effect.fail(abortError(signal))) @@ -107,7 +118,7 @@ const normalizeStdin = ( ? Stream.make(input) : input -const collectStream = (stream: Stream.Stream, maxOutputBytes: number | undefined) => +export const collectStream = (stream: Stream.Stream, maxOutputBytes: number | undefined) => Stream.runFold( stream, () => ({ chunks: [] as Uint8Array[], bytes: 0, truncated: false }), @@ -125,7 +136,7 @@ const collectStream = (stream: Stream.Stream, maxOutp }, ).pipe(Effect.map((x) => ({ buffer: Buffer.concat(x.chunks), truncated: x.truncated }))) -export const layer = Layer.effect( +const layer = Layer.effect( Service, Effect.gen(function* () { const spawner = yield* ChildProcessSpawner @@ -135,6 +146,22 @@ export const layer = Layer.effect( const collect = Effect.scoped( Effect.gen(function* () { const handle = yield* spawner.spawn(command) + if (options?.combineOutput) { + const [output, exitCode] = yield* Effect.all( + [collectStream(handle.all, options.maxOutputBytes), handle.exitCode], + { concurrency: "unbounded" }, + ) + return { + command: description, + exitCode, + output: output.buffer, + stdout: Buffer.alloc(0), + stderr: Buffer.alloc(0), + outputTruncated: output.truncated, + stdoutTruncated: false, + stderrTruncated: false, + } satisfies RunResult + } const [stdout, stderr, exitCode] = yield* Effect.all( [ collectStream(handle.stdout, options?.maxOutputBytes), @@ -229,6 +256,6 @@ export const layer = Layer.effect( }), ) -export const defaultLayer = layer.pipe(Layer.provide(CrossSpawnSpawner.defaultLayer)) +export const node = makeGlobalNode({ service: Service, layer: layer, deps: [CrossSpawnSpawner.node] }) export * as AppProcess from "./process" diff --git a/packages/core/src/project.ts b/packages/core/src/project.ts index 63f075a605e..d08222a7246 100644 --- a/packages/core/src/project.ts +++ b/packages/core/src/project.ts @@ -1,42 +1,42 @@ +export * as ProjectV2 from "./project" export * as Project from "./project" import { Context, Effect, Layer, Schema } from "effect" import path from "path" -import { AbsolutePath, withStatics } from "./schema" -import { AppFileSystem } from "./filesystem" +import { AbsolutePath } from "./schema" +import { FSUtil } from "./fs-util" import { Git } from "./git" +import { makeGlobalNode } from "./effect/app-node" import { Hash } from "./util/hash" +import { ProjectDirectories } from "./project/directories" +import { ProjectSchema } from "./project/schema" -export const ID = Schema.String.pipe( - Schema.brand("Project.ID"), - withStatics((schema) => ({ - global: schema.make("global"), - })), -) -export type ID = typeof ID.Type +export const ID = ProjectSchema.ID +export type ID = ProjectSchema.ID -export const Vcs = Schema.Union([ - Schema.Struct({ - type: Schema.Literal("git"), - store: AbsolutePath, - }), -]) -export type Vcs = typeof Vcs.Type +export const Vcs = ProjectSchema.Vcs +export type Vcs = ProjectSchema.Vcs export class Info extends Schema.Class("Project.Info")({ id: ID, }) {} +export const DirectoriesInput = ProjectDirectories.ListInput +export type DirectoriesInput = typeof DirectoriesInput.Type + +export const Directories = ProjectDirectories.ListOutput +export type Directories = typeof Directories.Type + +export interface Resolved { + readonly previous?: ID + readonly id: ID + readonly directory: AbsolutePath + readonly vcs?: Vcs +} + export interface Interface { - readonly resolve: (input: AbsolutePath) => Effect.Effect< - { - previous?: ID - id: ID - directory: AbsolutePath - vcs?: Vcs - }, - never - > + readonly directories: (input: DirectoriesInput) => Effect.Effect + readonly resolve: (input: AbsolutePath) => Effect.Effect /** * Temporary bridge method for writing the resolved project ID to the repo-local cache. * @@ -51,11 +51,16 @@ export interface Interface { export class Service extends Context.Service()("@opencode/ProjectV2") {} -export const layer = Layer.effect( +const layer = Layer.effect( Service, Effect.gen(function* () { - const fs = yield* AppFileSystem.Service + const fs = yield* FSUtil.Service const git = yield* Git.Service + const projectDirectories = yield* ProjectDirectories.Service + + const directories = Effect.fn("Project.directories")(function* (input: DirectoriesInput) { + return yield* projectDirectories.list(input.projectID) + }) const cached = Effect.fnUntraced(function* (dir: string) { return yield* fs.readFileString(path.join(dir, "kilo")).pipe( // kilocode_change @@ -65,8 +70,8 @@ export const layer = Layer.effect( ) }) - const remote = Effect.fnUntraced(function* (repo: Git.Repo) { - const origin = yield* git.remote(repo) + const remote = Effect.fnUntraced(function* (repo: Git.Repository) { + const origin = yield* git.remote.get(repo) if (!origin) return undefined const normalized = url(origin) if (!normalized) return undefined @@ -97,23 +102,22 @@ export const layer = Layer.effect( return `${host.toLowerCase()}/${pathname}` } - const root = Effect.fnUntraced(function* (repo: Git.Repo) { - const root = (yield* git.roots(repo))[0] + const root = Effect.fnUntraced(function* (repo: Git.Repository) { + const root = (yield* git.history.rootCommits(repo))[0] return root ? ID.make(root) : undefined }) const resolve = Effect.fn("Project.resolve")(function* (input: AbsolutePath) { - const repo = yield* git.find(input) + const repo = yield* git.repo.discover(input) if (!repo) return { id: ID.global, directory: AbsolutePath.make(path.parse(input).root), vcs: undefined } - const previous = yield* cached(repo.store) + const previous = yield* cached(repo.commonDirectory) const id = (yield* remote(repo)) ?? previous ?? (yield* root(repo)) - return { previous, id: id ?? ID.global, - directory: repo.directory, - vcs: { type: "git" as const, store: repo.store }, + directory: repo.worktree, + vcs: { type: "git" as const, store: repo.commonDirectory }, } }) @@ -121,8 +125,12 @@ export const layer = Layer.effect( yield* fs.writeFileString(path.join(input.store, "kilo"), input.id).pipe(Effect.ignore) // kilocode_change }) - return Service.of({ resolve, commit }) + return Service.of({ directories, resolve, commit }) }), ) -export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer)) +export const node = makeGlobalNode({ + service: Service, + layer: layer, + deps: [FSUtil.node, Git.node, ProjectDirectories.node], +}) diff --git a/packages/core/src/project/copy-strategies.ts b/packages/core/src/project/copy-strategies.ts new file mode 100644 index 00000000000..59466b9901a --- /dev/null +++ b/packages/core/src/project/copy-strategies.ts @@ -0,0 +1,35 @@ +import { Effect } from "effect" +import { AbsolutePath } from "../schema" +import { Git } from "../git" +import { DirectoryUnavailableError, StrategyID, type ListEntry, type Strategy } from "./copy" + +export function makeGitWorktreeStrategy(input: { + git: Git.Interface + canonical: (directory: AbsolutePath) => Effect.Effect +}) { + return { + id: StrategyID.make("git_worktree"), + create: Effect.fn("ProjectCopy.GitWorktree.create")(function* (options) { + const repository = yield* input.git.repo.discover(options.sourceDirectory) + if (!repository) return yield* new DirectoryUnavailableError({ directory: options.sourceDirectory }) + yield* input.git.worktree.create({ repository, directory: options.directory }) + return { directory: yield* input.canonical(options.directory) } + }), + remove: Effect.fn("ProjectCopy.GitWorktree.remove")(function* (options) { + const found = yield* input.git.repo.discover(options.directory) + if (!found) return yield* new DirectoryUnavailableError({ directory: options.directory }) + yield* input.git.worktree.remove({ repository: found, directory: options.directory, force: options.force }) + }), + list: Effect.fn("ProjectCopy.GitWorktree.list")(function* (directory) { + const found = yield* input.git.repo.discover(directory) + if (!found) return yield* new DirectoryUnavailableError({ directory }) + const entries = yield* input.git.worktree.list(found) + return yield* Effect.forEach(entries, (entry) => + input.canonical(entry.directory).pipe( + Effect.map((directory) => ({ directory, type: entry.kind === "main" ? "root" : "copy" }) as const), + Effect.catchTag("ProjectCopy.DirectoryUnavailableError", () => Effect.succeed(undefined)), + ), + ).pipe(Effect.map((items) => items.filter((item): item is ListEntry => item !== undefined))) + }), + } satisfies Strategy +} diff --git a/packages/core/src/project/copy.ts b/packages/core/src/project/copy.ts new file mode 100644 index 00000000000..a1a5cc84dbb --- /dev/null +++ b/packages/core/src/project/copy.ts @@ -0,0 +1,292 @@ +export * as ProjectCopy from "./copy" + +import { Context, Effect, Layer, Schema } from "effect" +import path from "path" +import { AbsolutePath } from "../schema" +import { FSUtil } from "../fs-util" +import { Git } from "../git" +import { makeLocationNode } from "../effect/app-node" +import { Project } from "../project" +import { ProjectDirectories } from "./directories" +import { makeGitWorktreeStrategy } from "./copy-strategies" +import { Slug } from "../util/slug" +import { EventV2 } from "../event" +import { Database } from "../database/database" +import { Location } from "../location" +import { Event } from "@opencode-ai/schema/project-directories" +import { ProjectCopy } from "@opencode-ai/schema/project-copy" + +export const StrategyID = ProjectCopy.StrategyID +export type StrategyID = typeof StrategyID.Type + +export const CreateInput = ProjectCopy.CreateInput +export type CreateInput = typeof CreateInput.Type + +export const RemoveInput = ProjectCopy.RemoveInput +export type RemoveInput = typeof RemoveInput.Type + +export const RefreshInput = Schema.Struct({ + projectID: Project.ID, +}).annotate({ identifier: "ProjectCopy.RefreshInput" }) +export type RefreshInput = typeof RefreshInput.Type + +export const RefreshResult = Schema.Struct({ + updated: Schema.Array(AbsolutePath), + removed: Schema.Array(AbsolutePath), +}).annotate({ identifier: "ProjectCopy.RefreshResult" }) +export type RefreshResult = typeof RefreshResult.Type + +export const Copy = ProjectCopy.Copy +export type Copy = typeof Copy.Type + +export const ListEntry = Schema.Struct({ + directory: AbsolutePath, + type: Schema.Literals(["root", "copy"]), +}).annotate({ identifier: "ProjectCopy.ListEntry" }) +export type ListEntry = typeof ListEntry.Type + +export class SourceDirectoryNotFoundError extends Schema.TaggedErrorClass()( + "ProjectCopy.SourceDirectoryNotFoundError", + { directory: AbsolutePath }, +) {} + +export class DestinationExistsError extends Schema.TaggedErrorClass()( + "ProjectCopy.DestinationExistsError", + { directory: AbsolutePath }, +) {} + +export class DirectoryUnavailableError extends Schema.TaggedErrorClass()( + "ProjectCopy.DirectoryUnavailableError", + { directory: AbsolutePath }, +) {} + +export class InvalidDirectoryError extends Schema.TaggedErrorClass()( + "ProjectCopy.InvalidDirectoryError", + { directory: AbsolutePath }, +) {} + +export class StrategyUnavailableError extends Schema.TaggedErrorClass()( + "ProjectCopy.StrategyUnavailableError", + { strategy: StrategyID }, +) {} + +export class DuplicateStrategyError extends Schema.TaggedErrorClass()( + "ProjectCopy.DuplicateStrategyError", + { strategy: StrategyID }, +) {} + +export type Error = + | SourceDirectoryNotFoundError + | DestinationExistsError + | DirectoryUnavailableError + | InvalidDirectoryError + | StrategyUnavailableError + | Git.WorktreeError + +export interface Strategy { + readonly id: StrategyID + readonly create: (input: { + sourceDirectory: AbsolutePath + directory: AbsolutePath + }) => Effect.Effect + readonly remove: (input: { + directory: AbsolutePath + force: boolean + }) => Effect.Effect + readonly list: (directory: AbsolutePath) => Effect.Effect +} + +export { Event } + +export interface Interface { + readonly register: (strategy: Strategy) => Effect.Effect + readonly create: (input: CreateInput) => Effect.Effect + readonly remove: (input: RemoveInput) => Effect.Effect + readonly refresh: (input: RefreshInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/ProjectCopy") {} + +export const refreshAfterBoot = Effect.gen(function* () { + const location = yield* Location.Service + const copies = yield* Service + yield* Effect.gen(function* () { + yield* Effect.logInfo("project copy refresh started", { projectID: location.project.id }) + const result = yield* copies.refresh({ projectID: location.project.id }) + yield* Effect.logInfo("project copy refresh done", { + projectID: location.project.id, + updated: result.updated, + removed: result.removed, + }) + }).pipe( + Effect.catchCause((cause) => Effect.logWarning("project copy refresh failed", { cause })), + Effect.forkScoped, + Effect.asVoid, + ) +}) + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const git = yield* Git.Service + const directories = yield* ProjectDirectories.Service + const db = (yield* Database.Service).db + const events = yield* EventV2.Service + + const changed = Effect.fnUntraced(function* (projectID: Project.ID, update: boolean) { + if (update) yield* events.publish(Event.Updated, { projectID }) + }) + + const canonical = Effect.fnUntraced(function* (input: AbsolutePath) { + const resolved = AbsolutePath.make(yield* fs.resolve(input)) + if (!(yield* fs.isDir(resolved))) return yield* new DirectoryUnavailableError({ directory: input }) + return resolved + }) + + const registry = new Map() + + const register = Effect.fn("ProjectCopy.register")(function* (strategy: Strategy) { + if (registry.has(strategy.id)) return yield* new DuplicateStrategyError({ strategy: strategy.id }) + registry.set(strategy.id, strategy) + }) + + // Register default strategies + yield* register(makeGitWorktreeStrategy({ git, canonical })).pipe(Effect.orDie) + + const strategies = () => Array.from(registry.values()) + + const source = Effect.fnUntraced(function* (input: AbsolutePath, projectID: Project.ID) { + const sourceDirectory = yield* canonical(input) + if (!(yield* directories.contains({ projectID, directory: sourceDirectory }))) + return yield* new SourceDirectoryNotFoundError({ directory: sourceDirectory }) + return sourceDirectory + }) + + const getStrategy = Effect.fnUntraced(function* (id: StrategyID) { + const found = registry.get(id) + if (!found) return yield* new StrategyUnavailableError({ strategy: id }) + return found + }) + + const create = Effect.fn("ProjectCopy.create")(function* (input: CreateInput) { + const selected = yield* getStrategy(input.strategy) + const sourceDirectory = yield* source(input.sourceDirectory, input.projectID) + yield* fs.makeDirectory(input.directory, { recursive: true }).pipe(Effect.orDie) + const name = input.name ?? Slug.create() + let suffix = 1 + let copyDirectory = AbsolutePath.make(path.join(input.directory, name)) + while (yield* fs.existsSafe(copyDirectory)) { + suffix++ + if (suffix > 10) return yield* new DestinationExistsError({ directory: copyDirectory }) + copyDirectory = AbsolutePath.make(path.join(input.directory, `${name}-${suffix}`)) + } + + const result = yield* selected.create({ + directory: copyDirectory, + sourceDirectory, + }) + yield* changed( + input.projectID, + yield* directories.create({ + projectID: input.projectID, + directory: result.directory, + strategy: input.strategy, + behavior: "replace", + }), + ) + return result + }) + + const remove = Effect.fn("ProjectCopy.remove")(function* (input: RemoveInput) { + const copyDirectory = yield* canonical(input.directory) + const stored = yield* directories.get({ projectID: input.projectID, directory: copyDirectory }) + if (!stored?.strategy) return yield* new InvalidDirectoryError({ directory: copyDirectory }) + yield* (yield* getStrategy(StrategyID.make(stored.strategy))).remove({ + directory: copyDirectory, + force: input.force, + }) + yield* changed( + input.projectID, + yield* directories.remove({ projectID: input.projectID, directory: copyDirectory }), + ) + }) + + const refresh = Effect.fn("ProjectCopy.refresh")(function* (input: RefreshInput) { + const stored = yield* directories.list(input.projectID) + const checked = yield* Effect.forEach( + stored, + (item) => fs.isDir(item.directory).pipe(Effect.map((exists) => ({ ...item, exists }))), + { concurrency: "unbounded" }, + ) + const sourceDirectories = checked + .filter((item) => item.strategy === undefined && item.exists) + .map((item) => item.directory) + const discovered = yield* Effect.forEach( + sourceDirectories, + (sourceDirectory) => + Effect.forEach(strategies(), (strategy) => + strategy.list(sourceDirectory).pipe( + Effect.catchTag("ProjectCopy.DirectoryUnavailableError", () => Effect.succeed([])), + Effect.map((items) => + items.map((item) => ({ + directory: item.directory, + strategy: item.type === "copy" ? strategy.id : undefined, + })), + ), + ), + ), + { concurrency: "unbounded" }, + ).pipe( + Effect.map((sets) => new Map(sets.flat(2).map((item) => [item.directory, item] as const)).values().toArray()), + ) + const removed = checked.filter((item) => !item.exists).map((item) => item.directory) + const result = yield* db + .transaction((tx) => + Effect.all({ + updated: Effect.forEach(discovered, (item) => + directories.create( + { + projectID: input.projectID, + directory: item.directory, + strategy: item.strategy, + behavior: "replace", + }, + tx, + ), + ), + removed: Effect.forEach(removed, (directory) => + directories.remove({ projectID: input.projectID, directory }, tx), + ), + }), + ) + .pipe(Effect.orDie) + const changes = { + updated: discovered.filter((_, index) => result.updated[index]).map((item) => item.directory), + removed: removed.filter((_, index) => result.removed[index]), + } + yield* changed(input.projectID, changes.updated.length > 0 || changes.removed.length > 0) + return changes + }) + + return Service.of({ + register, + create, + remove, + refresh, + }) + }), +) + +export const locationLayer = layer +export const node = makeLocationNode({ + service: Service, + layer: layer, + deps: [FSUtil.node, Git.node, ProjectDirectories.node, EventV2.node, Database.node], +}) + +export const refreshNode = makeLocationNode({ + name: "project-copy-refresh", + layer: Layer.effectDiscard(refreshAfterBoot), + deps: [node, Location.node], +}) diff --git a/packages/core/src/project/directories.ts b/packages/core/src/project/directories.ts new file mode 100644 index 00000000000..6c9ad2e5159 --- /dev/null +++ b/packages/core/src/project/directories.ts @@ -0,0 +1,158 @@ +export * as ProjectDirectories from "./directories" + +import { and, asc, desc, eq, isNotNull, isNull, ne, or } from "drizzle-orm" +import { Context, Effect, Layer, Schema } from "effect" +import { Database } from "../database/database" +import { makeGlobalNode } from "../effect/app-node" +import { AbsolutePath, optional } from "../schema" +import { ProjectSchema } from "./schema" +import { ProjectDirectoryTable } from "./sql" +import type { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" + +export interface Directory { + readonly directory: AbsolutePath + readonly strategy?: string +} + +export const CreateInput = Schema.Struct({ + projectID: ProjectSchema.ID, + directory: AbsolutePath, + strategy: Schema.optional(Schema.String), + behavior: Schema.Literals(["ignore", "replace"]).pipe(Schema.optional), +}) +export type CreateInput = typeof CreateInput.Type + +export const RemoveInput = Schema.Struct({ + projectID: ProjectSchema.ID, + directory: AbsolutePath, +}) +export type RemoveInput = typeof RemoveInput.Type + +type DatabaseClient = EffectDrizzleSqlite.EffectSQLiteDatabase +export type Transaction = Parameters[0]>[0] + +export const ListInput = Schema.Struct({ + projectID: ProjectSchema.ID, +}).annotate({ identifier: "Project.DirectoriesInput" }) +export type ListInput = typeof ListInput.Type + +export const ListOutput = Schema.Array( + Schema.Struct({ + directory: AbsolutePath, + strategy: optional(Schema.String), + }), +).annotate({ identifier: "Project.Directories" }) +export type ListOutput = typeof ListOutput.Type + +export interface Interface { + readonly list: (projectID: ProjectSchema.ID) => Effect.Effect> + readonly get: (input: { + projectID: ProjectSchema.ID + directory: AbsolutePath + }) => Effect.Effect + readonly contains: (input: { projectID: ProjectSchema.ID; directory: AbsolutePath }) => Effect.Effect + readonly create: (input: CreateInput, tx?: Transaction) => Effect.Effect + readonly remove: (input: RemoveInput, tx?: Transaction) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/ProjectDirectories") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const db = (yield* Database.Service).db + + const create = Effect.fn("ProjectDirectories.create")(function* (input: CreateInput, tx?: Transaction) { + const insert = (tx ?? db) + .insert(ProjectDirectoryTable) + .values({ project_id: input.projectID, directory: input.directory, strategy: input.strategy }) + const query = + input.behavior === "replace" + ? insert.onConflictDoUpdate({ + target: [ProjectDirectoryTable.project_id, ProjectDirectoryTable.directory], + set: { strategy: input.strategy ?? null }, + setWhere: input.strategy + ? or(isNull(ProjectDirectoryTable.strategy), ne(ProjectDirectoryTable.strategy, input.strategy)) + : isNotNull(ProjectDirectoryTable.strategy), + }) + : insert.onConflictDoNothing() + return ( + (yield* query.returning({ directory: ProjectDirectoryTable.directory }).get().pipe(Effect.orDie)) !== undefined + ) + }) + + const remove = Effect.fn("ProjectDirectories.remove")(function* (input: RemoveInput, tx?: Transaction) { + return ( + (yield* (tx ?? db) + .delete(ProjectDirectoryTable) + .where( + and( + eq(ProjectDirectoryTable.project_id, input.projectID), + eq(ProjectDirectoryTable.directory, input.directory), + ), + ) + .returning({ directory: ProjectDirectoryTable.directory }) + .get() + .pipe(Effect.orDie)) !== undefined + ) + }) + + const list = Effect.fn("ProjectDirectories.list")(function* (projectID: ProjectSchema.ID) { + const rows = yield* db + .select({ directory: ProjectDirectoryTable.directory, strategy: ProjectDirectoryTable.strategy }) + .from(ProjectDirectoryTable) + .where(eq(ProjectDirectoryTable.project_id, projectID)) + .orderBy(desc(ProjectDirectoryTable.time_created), asc(ProjectDirectoryTable.directory)) + .all() + .pipe(Effect.orDie) + return rows.map((row) => ({ directory: row.directory, strategy: row.strategy ?? undefined })) + }) + + const contains = Effect.fn("ProjectDirectories.contains")(function* (input: { + projectID: ProjectSchema.ID + directory: AbsolutePath + }) { + return ( + (yield* db + .select({ directory: ProjectDirectoryTable.directory }) + .from(ProjectDirectoryTable) + .where( + and( + eq(ProjectDirectoryTable.project_id, input.projectID), + eq(ProjectDirectoryTable.directory, input.directory), + ), + ) + .get() + .pipe(Effect.orDie)) !== undefined + ) + }) + + const get = Effect.fn("ProjectDirectories.get")(function* (input: { + projectID: ProjectSchema.ID + directory: AbsolutePath + }) { + const row = yield* db + .select({ directory: ProjectDirectoryTable.directory, strategy: ProjectDirectoryTable.strategy }) + .from(ProjectDirectoryTable) + .where( + and( + eq(ProjectDirectoryTable.project_id, input.projectID), + eq(ProjectDirectoryTable.directory, input.directory), + ), + ) + .get() + .pipe(Effect.orDie) + return row ? { directory: row.directory, strategy: row.strategy ?? undefined } : undefined + }) + + return Service.of({ + list, + get, + contains, + create, + remove, + }) + }), +) + +export const node = makeGlobalNode({ service: Service, layer: layer, deps: [Database.node] }) diff --git a/packages/core/src/project/schema.ts b/packages/core/src/project/schema.ts new file mode 100644 index 00000000000..eed359abad2 --- /dev/null +++ b/packages/core/src/project/schema.ts @@ -0,0 +1,16 @@ +export * as ProjectSchema from "./schema" + +import { Schema } from "effect" +import { Project } from "@opencode-ai/schema/project" +import { AbsolutePath } from "../schema" + +export const ID = Project.ID +export type ID = typeof ID.Type + +export const Vcs = Schema.Union([ + Schema.Struct({ + type: Schema.Literal("git"), + store: AbsolutePath, + }), +]) +export type Vcs = typeof Vcs.Type diff --git a/packages/core/src/project/sql.ts b/packages/core/src/project/sql.ts new file mode 100644 index 00000000000..ab05fdac4a1 --- /dev/null +++ b/packages/core/src/project/sql.ts @@ -0,0 +1,35 @@ +import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core" +import * as DatabasePath from "../database/path" +import { Timestamps } from "../database/schema.sql" +import { ProjectSchema } from "./schema" + +export const ProjectTable = sqliteTable("project", { + id: text().$type().primaryKey(), + worktree: DatabasePath.absoluteColumn().notNull(), + vcs: text(), + name: text(), + icon_url: text(), + icon_url_override: text(), + icon_color: text(), + ...Timestamps, + time_initialized: integer(), + sandboxes: DatabasePath.absoluteArrayColumn().notNull(), + commands: text({ mode: "json" }).$type<{ start?: string }>(), +}) + +export const ProjectDirectoryTable = sqliteTable( + "project_directory", + { + project_id: text() + .$type() + .notNull() + .references(() => ProjectTable.id, { onDelete: "cascade" }), + directory: DatabasePath.absoluteColumn().notNull(), + type: text().$type<"main" | "root" | "git_worktree">(), + strategy: text(), + time_created: integer() + .notNull() + .$default(() => Date.now()), + }, + (table) => [primaryKey({ columns: [table.project_id, table.directory] })], +) diff --git a/packages/core/src/provider.ts b/packages/core/src/provider.ts index 7ba2172ada3..bbc481c97b9 100644 --- a/packages/core/src/provider.ts +++ b/packages/core/src/provider.ts @@ -1,120 +1,26 @@ export * as ProviderV2 from "./provider" -import { withStatics } from "./schema" -import { Schema } from "effect" +import { Types } from "effect" +import { Provider } from "@opencode-ai/schema/provider" -export const ID = Schema.String.pipe( - Schema.brand("ProviderV2.ID"), - withStatics((schema) => ({ - // Well-known providers - opencode: schema.make("opencode"), - anthropic: schema.make("anthropic"), - openai: schema.make("openai"), - google: schema.make("google"), - googleVertex: schema.make("google-vertex"), - githubCopilot: schema.make("github-copilot"), - amazonBedrock: schema.make("amazon-bedrock"), - azure: schema.make("azure"), - openrouter: schema.make("openrouter"), - mistral: schema.make("mistral"), - gitlab: schema.make("gitlab"), - })), -) +// kilocode_change - preserve Kilo's well-known routing ID without forking the shared schema +export const ID = Object.assign(Provider.ID, { kilo: Provider.ID.make("kilo") }) export type ID = typeof ID.Type -const OpenAIResponses = Schema.Struct({ - type: Schema.Literal("openai/responses"), - url: Schema.String, - websocket: Schema.optional(Schema.Boolean), -}) +export const AISDK = Provider.AISDK -const OpenAICompletions = Schema.Struct({ - type: Schema.Literal("openai/completions"), - url: Schema.String, - reasoning: Schema.Union([ - Schema.Struct({ - type: Schema.Literal("reasoning_content"), - }), - Schema.Struct({ - type: Schema.Literal("reasoning_details"), - }), - ]).pipe(Schema.optional), -}) -export type OpenAICompletions = typeof OpenAICompletions.Type +export const Native = Provider.Native -const AISDK = Schema.Struct({ - type: Schema.Literal("aisdk"), - package: Schema.String, - url: Schema.String.pipe(Schema.optional), -}) +export const Api = Provider.Api +export type Api = Provider.Api +export type MutableApi = T extends Api + ? Omit, "settings"> & (undefined extends T["settings"] ? { settings?: any } : { settings: any }) + : never -const AnthropicMessages = Schema.Struct({ - type: Schema.Literal("anthropic/messages"), - url: Schema.String, -}) +export const Request = Provider.Request +export type Request = Provider.Request -const UnknownEndpoint = Schema.Struct({ - type: Schema.Literal("unknown"), -}) +export const Info = Provider.Info +export type Info = Provider.Info -export const Endpoint = Schema.Union([ - UnknownEndpoint, - OpenAIResponses, - OpenAICompletions, - AnthropicMessages, - AISDK, -]).pipe(Schema.toTaggedUnion("type")) -export type Endpoint = typeof Endpoint.Type - -export const Options = Schema.Struct({ - headers: Schema.Record(Schema.String, Schema.String), - body: Schema.Record(Schema.String, Schema.Any), - aisdk: Schema.Struct({ - provider: Schema.Record(Schema.String, Schema.Any), - request: Schema.Record(Schema.String, Schema.Any), - }), -}) -export type Options = typeof Options.Type - -export class Info extends Schema.Class("ProviderV2.Info")({ - id: ID, - name: Schema.String, - enabled: Schema.Union([ - Schema.Literal(false), - Schema.Struct({ - via: Schema.Literal("env"), - name: Schema.String, - }), - Schema.Struct({ - via: Schema.Literal("account"), - service: Schema.String, - }), - Schema.Struct({ - via: Schema.Literal("custom"), - data: Schema.Record(Schema.String, Schema.Any), - }), - ]), - env: Schema.String.pipe(Schema.Array), - endpoint: Endpoint, - options: Options, -}) { - static empty(providerID: ID) { - return new Info({ - id: providerID, - name: providerID, - enabled: false, - env: [], - endpoint: { - type: "unknown", - }, - options: { - headers: {}, - body: {}, - aisdk: { - provider: {}, - request: {}, - }, - }, - }) - } -} +export type MutableInfo = Omit, "api"> & { api: MutableApi } diff --git a/packages/core/src/pty.ts b/packages/core/src/pty.ts new file mode 100644 index 00000000000..25741eacf53 --- /dev/null +++ b/packages/core/src/pty.ts @@ -0,0 +1,380 @@ +export * as Pty from "./pty" + +import { makeGlobalNode, makeLocationNode } from "./effect/app-node" // kilocode_change +import { Context, Effect, Layer, Schema, Types } from "effect" +import { Pty } from "@opencode-ai/schema/pty" +import { Config } from "./config" +import { EventV2 } from "./event" +import { Location } from "./location" +import { PtyID } from "./pty/schema" +import { SessionSchema } from "./session/schema" // kilocode_change +import { Shell } from "./shell" +import { lazy } from "./util/lazy" +import { KiloPtySelfCommand } from "./kilocode/pty-self-command" // kilocode_change +import * as KiloPtyRegistry from "./kilocode/pty/registry" // kilocode_change +import type { Active, Subscriber } from "./kilocode/pty/registry" // kilocode_change + +const BUFFER_LIMIT = 1024 * 1024 * 2 +// Exited sessions stay observable (status, exit code, retained output) until removed explicitly. +// Cap retention so abandoned terminals do not accumulate unbounded buffers. +const EXITED_LIMIT = 25 +const pty = lazy(() => import("#pty")) + +// kilocode_change - the Kilo `sessionID` field now lives on the canonical shared schema (see +// packages/schema/src/pty.ts) so the generated SDK carries it; reuse that schema verbatim here. +export const Info = Pty.Info +export type Info = Types.DeepMutable + +export const CreateInput = Pty.CreateInput + +export type CreateInput = Types.DeepMutable + +export const UpdateInput = Schema.Struct({ + ...Pty.UpdateInput.fields, + sessionID: Schema.optional(Schema.NullOr(SessionSchema.ID)), // kilocode_change +}) + +export type UpdateInput = Types.DeepMutable + +// kilocode_change - the shared events already carry Kilo's extended Info (see packages/schema/src/pty.ts), +// so reuse them verbatim instead of redefining pty.created/pty.updated here. +export const Event = Pty.Event + +export type AttachInput = { + // Absolute output cursor to replay from. -1 tails from the current end; omitted replays the full retained buffer. + readonly cursor?: number + // Callbacks fire synchronously from the native PTY data path; keep them non-blocking. + readonly onData: (chunk: string) => void + // Fired once when the session stops producing output: process exit (exitCode set), removal, or service teardown. + readonly onEnd: (event: { exitCode?: number }) => void + // Canonical routes can replay retained output after exit; legacy callers retain the former error. + readonly allowExited?: boolean // kilocode_change +} + +export type Attachment = { + // Retained output from the requested cursor to the current end. + readonly replay: string + // Absolute output cursor after replay. + readonly cursor: number + readonly write: (data: string) => void + // Starts live delivery after the caller has applied replay and cursor metadata. + readonly activate: () => void + readonly detach: () => void +} + +export class NotFoundError extends Schema.TaggedErrorClass()("Pty.NotFoundError", { + ptyID: PtyID, +}) {} + +export class ExitedError extends Schema.TaggedErrorClass()("Pty.ExitedError", { + ptyID: PtyID, +}) {} + +export interface Interface { + readonly list: () => Effect.Effect + readonly get: (id: PtyID) => Effect.Effect + readonly create: (input: CreateInput) => Effect.Effect + readonly update: (id: PtyID, input: UpdateInput) => Effect.Effect + readonly remove: (id: PtyID) => Effect.Effect + readonly removeDirectory: (location: Location.Ref) => Effect.Effect // kilocode_change + readonly write: (id: PtyID, data: string) => Effect.Effect + readonly attach: (id: PtyID, input: AttachInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/Pty") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const events = yield* EventV2.Service + const location = yield* Location.Service + const config = yield* Config.Service + const context = yield* Effect.context() + const runFork = Effect.runForkWith(context) + const sessions = KiloPtyRegistry.sessions // kilocode_change + + function notifyEnd(session: Active, event: { exitCode?: number }) { + for (const subscriber of session.subscribers.values()) { + if (!subscriber.active) { + subscriber.end = event + continue + } + try { + subscriber.onEnd(event) + } catch (error) { + Effect.runSync(Effect.logDebug("PTY subscriber end callback failed", { id: session.info.id, error })) + } + } + session.subscribers.clear() + } + + const requireSession = Effect.fn("Pty.requireSession")(function* (id: PtyID) { + const session = sessions.get(id) + const owner = Location.Ref.make({ directory: location.directory, workspaceID: location.workspaceID }) + if (!session || !KiloPtyRegistry.sameLocation(session.location, owner)) + return yield* new NotFoundError({ ptyID: id }) + return session + }) + + const removeSession = Effect.fnUntraced(function* (id: PtyID) { + // kilocode_change start - removal and its deleted event are one uninterruptible lifecycle transition. + const session = sessions.get(id) + if (!session || !KiloPtyRegistry.claimRemoval(id)) return + yield* Effect.uninterruptible( + Effect.gen(function* () { + yield* Effect.logInfo("removing session", { id }) + yield* Effect.promise(() => KiloPtyRegistry.teardown(session)) + sessions.delete(id) + KiloPtyRegistry.removeExited(session) + yield* events + .publish(Event.Deleted, { id: session.info.id }, { location: session.location }) + .pipe(Effect.catch((error) => Effect.logWarning("failed to publish PTY deleted event", { id, error }))) + }).pipe(Effect.ensuring(Effect.sync(() => KiloPtyRegistry.releaseRemoval(id)))), + ) + // kilocode_change end + }) + + const remove = Effect.fn("Pty.remove")(function* (id: PtyID) { + yield* requireSession(id) + yield* removeSession(id) + }) + + const removeDirectory = Effect.fn("Pty.removeDirectory")(function* (target: Location.Ref) { + const owned = Array.from(sessions.values()).filter( + (session) => + KiloPtyRegistry.sameDirectory(session.location.directory, target.directory) && + session.location.workspaceID === target.workspaceID, + ) + yield* Effect.forEach(owned, (session) => removeSession(session.info.id), { concurrency: 4, discard: true }) + }) + + const list = Effect.fn("Pty.list")(function* () { + const owner = Location.Ref.make({ directory: location.directory, workspaceID: location.workspaceID }) + return Array.from(sessions.values()) + .filter((session) => KiloPtyRegistry.sameLocation(session.location, owner)) + .map((session) => session.info) + }) + + const get = Effect.fn("Pty.get")(function* (id: PtyID) { + return (yield* requireSession(id)).info + }) + + const createBody = Effect.fn("Pty.createBody")(function* (input: CreateInput, owner: Location.Ref) { + const id = PtyID.ascending() + // kilocode_change start - resolve Kilo self-commands to the real binary, arguments, and project cwd + const resolved = KiloPtySelfCommand.resolve({ + command: input.command, + args: input.args ? [...input.args] : undefined, + cwd: input.cwd, + }) + const implicit = !resolved.command + const command = resolved.command || Shell.preferred(Config.latest(yield* config.entries(), "shell")) + const base = resolved.args ?? [] + const args = implicit && Shell.login(command) ? [...base, "-l"] : [...base] + const cwd = resolved.cwd || location.directory + // kilocode_change end + const env = { + ...process.env, + ...input.env, + TERM: "xterm-256color", + KILO_TERMINAL: "1", + KILO_PTY_ID: id, // kilocode_change - let nested Kilo processes identify their parent terminal + } as Record + // kilocode_change start - do not expose local server credentials to user terminals. + // node-pty inherits parent values for omitted keys, so empty tombstones are required. + env.KILO_SERVER_PASSWORD = "" + env.KILO_SERVER_USERNAME = "" + // kilocode_change end + if (process.platform === "win32") { + env.LC_ALL = "C.UTF-8" + env.LC_CTYPE = "C.UTF-8" + env.LANG = "C.UTF-8" + } + yield* Effect.logInfo("creating session", { id, cmd: command, args, cwd }) + const { spawn } = yield* Effect.promise(() => pty()) + // kilocode_change start - spawn with initial terminal dimensions + const proc = yield* Effect.sync(() => + spawn(command, args, { + name: "xterm-256color", + cwd, + env, + cols: input.size?.cols, + rows: input.size?.rows, + }), + ) + // kilocode_change end + const info: Info = { + id, + title: input.title || `Terminal ${id.slice(-4)}`, + command, + args, + cwd, + status: "running", + pid: proc.pid, + } + const session: Active = { + info, + location: owner, // kilocode_change + process: proc, + buffer: "", + bufferCursor: 0, + cursor: 0, + subscribers: new Map(), + listeners: [], + stopping: false, // kilocode_change + terminated: false, + } + sessions.set(id, session) + session.listeners.push( + proc.onData((chunk) => { + session.cursor += chunk.length + for (const [token, subscriber] of session.subscribers.entries()) { + if (!subscriber.active) { + subscriber.pending.push(chunk) + continue + } + try { + subscriber.onData(chunk) + } catch { + session.subscribers.delete(token) + } + } + session.buffer += chunk + if (session.buffer.length <= BUFFER_LIMIT) return + const excess = session.buffer.length - BUFFER_LIMIT + session.buffer = session.buffer.slice(excess) + session.bufferCursor += excess + }), + proc.onExit(({ exitCode }) => { + if (session.info.status === "exited") return + if (session.stopping) { + session.info.status = "exited" + session.info.exitCode = exitCode + return + } + session.info.status = "exited" + session.info.exitCode = exitCode + notifyEnd(session, { exitCode }) + KiloPtyRegistry.markExited(session) + runFork( + Effect.gen(function* () { + yield* Effect.logInfo("session exited", { id, exitCode }) + yield* events + .publish(Event.Exited, { id, exitCode }, { location: session.location }) + .pipe(Effect.catch((error) => Effect.logWarning("failed to publish PTY exited event", { id, error }))) + while (KiloPtyRegistry.exitedCount(session.location) > EXITED_LIMIT) { + const oldest = KiloPtyRegistry.oldestExited(session.location) + if (!oldest) break + yield* removeSession(oldest) + if (sessions.has(oldest)) break + KiloPtyRegistry.removeExitedID(session.location, oldest) + } + }), + ) + }), + ) + yield* events + .publish(Event.Created, { info }, { location: session.location }) + .pipe(Effect.catch((error) => Effect.logWarning("failed to publish PTY created event", { id, error }))) + return info + }) + + const create = Effect.fn("Pty.create")(function* (input: CreateInput) { + const owner = Location.Ref.make({ directory: location.directory, workspaceID: location.workspaceID }) + const release = KiloPtyRegistry.beginCreate(owner) + return yield* createBody(input, owner).pipe(Effect.ensuring(Effect.sync(release))) + }) + + const update = Effect.fn("Pty.update")(function* (id: PtyID, input: UpdateInput) { + const session = yield* requireSession(id) + if (input.title) session.info.title = input.title + // kilocode_change start - associate nested Kilo TUI terminals with the viewed session + if ("sessionID" in input) session.info.sessionID = input.sessionID ?? undefined + // kilocode_change end + if (input.size && session.info.status === "running") session.process.resize(input.size.cols, input.size.rows) + yield* events.publish(Event.Updated, { info: session.info }, { location: session.location }) + return session.info + }) + + const write = Effect.fn("Pty.write")(function* (id: PtyID, data: string) { + const session = yield* requireSession(id) + if (session.info.status === "running") session.process.write(data) + }) + + const attach = Effect.fn("Pty.attach")(function* (id: PtyID, input: AttachInput) { + const session = yield* requireSession(id) + if (session.info.status !== "running" && !input.allowExited) return yield* new ExitedError({ ptyID: id }) // kilocode_change + yield* Effect.logInfo("client attached to session", { id, directory: location.directory }) + const token = {} + const subscriber: Subscriber = { + onData: input.onData, + onEnd: input.onEnd, + active: false, + detached: false, + pending: [], + end: session.info.status === "exited" ? { exitCode: session.info.exitCode } : undefined, // kilocode_change + } + session.subscribers.set(token, subscriber) + const start = session.bufferCursor + const end = session.cursor + const from = + input.cursor === -1 + ? end + : typeof input.cursor === "number" && Number.isSafeInteger(input.cursor) + ? Math.max(0, input.cursor) + : 0 + const replay = (() => { + if (!session.buffer || from >= end) return "" + const offset = Math.max(0, from - start) + if (offset >= session.buffer.length) return "" + return session.buffer.slice(offset) + })() + return { + replay, + cursor: end, + write: (data: string) => { + if (session.info.status === "running") session.process.write(data) + }, + activate: () => { + if (subscriber.active || subscriber.detached) return + subscriber.active = true + try { + for (const chunk of subscriber.pending) subscriber.onData(chunk) + subscriber.pending.length = 0 + if (subscriber.end) subscriber.onEnd(subscriber.end) + } catch { + session.subscribers.delete(token) + } + }, + detach: () => { + subscriber.detached = true + subscriber.pending.length = 0 + subscriber.end = undefined + session.subscribers.delete(token) + }, + } + }) + + return Service.of({ list, get, create, update, remove, removeDirectory, write, attach }) // kilocode_change + }), +) + +export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer)) + +export const shutdown = KiloPtyRegistry.shutdown // kilocode_change +export const terminateDirectory = KiloPtyRegistry.terminateDirectory // kilocode_change + +export const shutdownNode = makeGlobalNode({ + name: "pty-shutdown", + layer: Layer.effectDiscard( + Effect.gen(function* () { + const release = yield* Effect.promise(() => KiloPtyRegistry.acquireOwner()) + yield* Effect.addFinalizer(() => + Effect.promise(release).pipe(Effect.catch((error) => Effect.logError("failed to shut down PTYs", { error }))), + ) + }), + ), + deps: [], +}) // kilocode_change + +export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Location.node, Config.node] }) diff --git a/packages/core/src/pty/driver.ts b/packages/core/src/pty/driver.ts new file mode 100644 index 00000000000..4fab47ff97e --- /dev/null +++ b/packages/core/src/pty/driver.ts @@ -0,0 +1,2 @@ +// kilocode_change - expose the conditional PTY driver to Kilo's legacy interactive terminal +export * from "#pty" diff --git a/packages/core/src/pty/protocol.ts b/packages/core/src/pty/protocol.ts new file mode 100644 index 00000000000..21c6a89f734 --- /dev/null +++ b/packages/core/src/pty/protocol.ts @@ -0,0 +1,37 @@ +export * as PtyProtocol from "./protocol" + +// Wire protocol for PTY websocket transports. The PTY domain service is transport-free; server +// routes adapt Pty.attach to websockets with these helpers so every surface speaks one protocol. +// +// Outbound frames are raw UTF-8 terminal chunks. One control frame — a 0x00 byte followed by +// UTF-8 JSON — carries the absolute output cursor after replay so clients can resume later. + +const encoder = new TextEncoder() +const decoder = new TextDecoder("utf-8", { fatal: true }) + +// Replay can be megabytes; send it in bounded frames. +export const REPLAY_CHUNK = 64 * 1024 + +export function metaFrame(cursor: number) { + const bytes = encoder.encode(JSON.stringify({ cursor })) + const out = new Uint8Array(bytes.length + 1) + out[0] = 0 + out.set(bytes, 1) + return out +} + +export function chunks(data: string) { + const out: string[] = [] + for (let i = 0; i < data.length; i += REPLAY_CHUNK) out.push(data.slice(i, i + REPLAY_CHUNK)) + return out +} + +// Inbound client frames are UTF-8 text or binary; invalid UTF-8 input is dropped. +export function decodeInput(message: string | Uint8Array | ArrayBuffer) { + if (typeof message === "string") return message + try { + return decoder.decode(message instanceof ArrayBuffer ? new Uint8Array(message) : message) + } catch { + return undefined + } +} diff --git a/packages/opencode/src/pty/pty.bun.ts b/packages/core/src/pty/pty.bun.ts similarity index 100% rename from packages/opencode/src/pty/pty.bun.ts rename to packages/core/src/pty/pty.bun.ts diff --git a/packages/opencode/src/pty/pty.node.ts b/packages/core/src/pty/pty.node.ts similarity index 80% rename from packages/opencode/src/pty/pty.node.ts rename to packages/core/src/pty/pty.node.ts index 76f415f4cdd..54691d1e84d 100644 --- a/packages/opencode/src/pty/pty.node.ts +++ b/packages/core/src/pty/pty.node.ts @@ -4,7 +4,10 @@ import type { Opts, Proc } from "./pty" export type { Disp, Exit, Opts, Proc } from "./pty" export function spawn(file: string, args: string[], opts: Opts): Proc { - const proc = pty.spawn(file, args, opts) + const proc = pty.spawn(file, args, { + ...opts, + ...(process.platform === "win32" ? { useConptyDll: true } : {}), + }) return { pid: proc.pid, onData(listener) { diff --git a/packages/opencode/src/pty/pty.ts b/packages/core/src/pty/pty.ts similarity index 100% rename from packages/opencode/src/pty/pty.ts rename to packages/core/src/pty/pty.ts diff --git a/packages/core/src/pty/schema.ts b/packages/core/src/pty/schema.ts new file mode 100644 index 00000000000..ab0c40521b1 --- /dev/null +++ b/packages/core/src/pty/schema.ts @@ -0,0 +1 @@ +export { ID as PtyID } from "@opencode-ai/schema/pty" diff --git a/packages/core/src/pty/ticket.ts b/packages/core/src/pty/ticket.ts new file mode 100644 index 00000000000..07838b14157 --- /dev/null +++ b/packages/core/src/pty/ticket.ts @@ -0,0 +1,56 @@ +export * as PtyTicket from "./ticket" + +import { WorkspaceV2 } from "../workspace" +import { PtyTicket } from "@opencode-ai/schema/pty-ticket" +import { PtyID } from "./schema" +import { Cache, Context, Duration, Effect, Layer } from "effect" +import { makeGlobalNode } from "../effect/app-node" + +const DEFAULT_TTL = Duration.seconds(60) +const CAPACITY = 10_000 + +export const ConnectToken = PtyTicket.ConnectToken + +export type Scope = { + readonly ptyID: PtyID + readonly directory?: string + readonly workspaceID?: WorkspaceV2.ID +} + +export interface Interface { + issue(input: Scope): Effect.Effect + consume(input: Scope & { readonly ticket: string }): Effect.Effect +} + +export class Service extends Context.Service()("@opencode/PtyTicket") {} + +function matches(record: Scope, input: Scope) { + return ( + record.ptyID === input.ptyID && record.directory === input.directory && record.workspaceID === input.workspaceID + ) +} + +// Tickets are inserted via Cache.set and removed atomically via invalidateWhen. The lookup is +// never invoked; it dies if it ever is, which would signal a misuse of the Service interface. +const noLookup = () => Effect.die("PtyTicket cache must be used via set/invalidateWhen, never get") + +// Visible for tests so the TTL can be shortened. Production uses `layer` with the default TTL. +export const make = (ttl: Duration.Input = DEFAULT_TTL) => + Effect.gen(function* () { + const cache = yield* Cache.make({ capacity: CAPACITY, lookup: noLookup, timeToLive: ttl }) + const expiresIn = Math.max(1, Math.round(Duration.toSeconds(Duration.fromInputUnsafe(ttl)))) + return Service.of({ + issue: Effect.fn("PtyTicket.issue")(function* (input) { + const ticket = crypto.randomUUID() + yield* Cache.set(cache, ticket, input) + return { ticket, expires_in: expiresIn } + }), + consume: Effect.fn("PtyTicket.consume")(function* (input) { + return yield* Cache.invalidateWhen(cache, input.ticket, (stored) => matches(stored, input)) + }), + }) + }) + +const layer = Layer.effect(Service, make()) + +export const node = makeGlobalNode({ service: Service, layer: layer, deps: [] }) diff --git a/packages/core/src/public-event-manifest.ts b/packages/core/src/public-event-manifest.ts new file mode 100644 index 00000000000..11b84f79054 --- /dev/null +++ b/packages/core/src/public-event-manifest.ts @@ -0,0 +1,7 @@ +export * as PublicEventManifest from "./public-event-manifest" + +import { Event } from "@opencode-ai/schema/event" +import { EventManifest } from "@opencode-ai/schema/event-manifest" + +export const Definitions = EventManifest.ServerDefinitions +export const Latest = Event.latest(Definitions) diff --git a/packages/core/src/question.ts b/packages/core/src/question.ts new file mode 100644 index 00000000000..79e0ea5e038 --- /dev/null +++ b/packages/core/src/question.ts @@ -0,0 +1,153 @@ +export * as QuestionV2 from "./question" + +import { makeLocationNode } from "./effect/app-node" +import { Context, Deferred, Effect, Layer, Schema } from "effect" +import { Question } from "@opencode-ai/schema/question" +import { EventV2 } from "./event" +import { SessionSchema } from "./session/schema" + +export const ID = Question.ID +export type ID = typeof ID.Type + +export const Option = Question.Option +export type Option = typeof Option.Type + +export const Info = Question.Info +export type Info = typeof Info.Type + +export const Prompt = Question.Prompt +export type Prompt = typeof Prompt.Type + +export const Tool = Question.Tool +export type Tool = typeof Tool.Type + +export const Request = Question.Request +export type Request = typeof Request.Type + +export const Answer = Question.Answer +export type Answer = typeof Answer.Type + +export const Reply = Question.Reply +export type Reply = typeof Reply.Type + +export const Event = Question.Event + +export class RejectedError extends Schema.TaggedErrorClass()("QuestionV2.RejectedError", {}) { + override get message() { + return "The user dismissed this question" + } +} + +export class NotFoundError extends Schema.TaggedErrorClass()("QuestionV2.NotFoundError", { + requestID: ID, +}) {} + +export interface AskInput { + readonly sessionID: SessionSchema.ID + readonly questions: ReadonlyArray + readonly tool?: Tool +} + +export interface ReplyInput { + readonly requestID: ID + readonly answers: ReadonlyArray +} + +export interface Interface { + readonly ask: (input: AskInput) => Effect.Effect, RejectedError> + readonly reply: (input: ReplyInput) => Effect.Effect + readonly reject: (requestID: ID) => Effect.Effect + readonly list: () => Effect.Effect> +} + +export class Service extends Context.Service()("@opencode/v2/Question") {} + +interface Pending { + readonly request: Request + readonly deferred: Deferred.Deferred, RejectedError> +} + +/** + * Location-owned pending prompts. The Location layer map must materialize this + * layer once per embedded Location so replies cannot settle another Location's + * deferred request. + */ +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const events = yield* EventV2.Service + const pending = new Map() + + yield* Effect.addFinalizer(() => + Effect.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new RejectedError()), { + discard: true, + }).pipe( + Effect.ensuring( + Effect.sync(() => { + pending.clear() + }), + ), + ), + ) + + const ask = Effect.fn("QuestionV2.ask")((input: AskInput) => + Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const id = ID.ascending() + const deferred = yield* Deferred.make, RejectedError>() + const request: Request = { id, ...input } + pending.set(id, { request, deferred }) + return yield* events.publish(Event.Asked, request).pipe( + Effect.andThen(restore(Deferred.await(deferred))), + Effect.ensuring( + Effect.sync(() => { + pending.delete(id) + }), + ), + ) + }), + ), + ) + + const reply = Effect.fn("QuestionV2.reply")((input: ReplyInput) => + Effect.uninterruptible( + Effect.gen(function* () { + const existing = pending.get(input.requestID) + if (!existing) return yield* new NotFoundError({ requestID: input.requestID }) + yield* events.publish(Event.Replied, { + sessionID: existing.request.sessionID, + requestID: existing.request.id, + answers: input.answers.map((answer) => [...answer]), + }) + yield* Deferred.succeed(existing.deferred, input.answers) + pending.delete(input.requestID) + }), + ), + ) + + const reject = Effect.fn("QuestionV2.reject")((requestID: ID) => + Effect.uninterruptible( + Effect.gen(function* () { + const existing = pending.get(requestID) + if (!existing) return yield* new NotFoundError({ requestID }) + yield* events.publish(Event.Rejected, { + sessionID: existing.request.sessionID, + requestID: existing.request.id, + }) + yield* Deferred.fail(existing.deferred, new RejectedError()) + pending.delete(requestID) + }), + ), + ) + + const list = Effect.fn("QuestionV2.list")(function* () { + return Array.from(pending.values(), (item) => item.request) + }) + + return Service.of({ ask, reply, reject, list }) + }), +) + +export const locationLayer = layer + +export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] }) diff --git a/packages/core/src/reference.ts b/packages/core/src/reference.ts new file mode 100644 index 00000000000..ddf53f751d4 --- /dev/null +++ b/packages/core/src/reference.ts @@ -0,0 +1,135 @@ +export * as Reference from "./reference" + +import { makeLocationNode } from "./effect/app-node" +import { Context, Effect, Layer, Scope, Types } from "effect" +import { Reference } from "@opencode-ai/schema/reference" +import { Global } from "./global" +import { EventV2 } from "./event" +import { Repository } from "./repository" +import { RepositoryCache } from "./repository-cache" +import { AbsolutePath } from "./schema" +import { State } from "./state" + +export const LocalSource = Reference.LocalSource +export type LocalSource = Reference.LocalSource + +export const GitSource = Reference.GitSource +export type GitSource = Reference.GitSource + +export const Source = Reference.Source +export type Source = Reference.Source + +export const Event = Reference.Event + +export const Info = Reference.Info +export type Info = Reference.Info + +type Data = { + sources: Map> +} + +type Draft = { + add(name: string, source: Source): void + remove(name: string): void + list(): readonly [string, Source][] +} + +export interface Interface extends State.Transformable { + readonly replace: (sources: readonly (readonly [string, Source])[]) => Effect.Effect // kilocode_change + readonly list: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/Reference") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const global = yield* Global.Service + const events = yield* EventV2.Service + const cache = yield* RepositoryCache.Service + const scope = yield* Scope.Scope + const materialized = new Map() + const state = State.create({ + initial: () => ({ sources: new Map() }), + draft: (draft) => ({ + add: (name, source) => draft.sources.set(name, source as Types.DeepMutable), + remove: (name) => draft.sources.delete(name), + list: () => Array.from(draft.sources.entries()) as [string, Source][], + }), + finalize: (draft) => + Effect.gen(function* () { + materialized.clear() + for (const [name, source] of draft.list()) { + if (source.type === "local") { + materialized.set( + name, + new Info({ + name, + path: source.path, + ...(source.description === undefined ? {} : { description: source.description }), + ...(source.hidden === undefined ? {} : { hidden: source.hidden }), + source, + }), + ) + continue + } + const repository = Repository.parse(source.repository) + if (!repository || !Repository.isRemote(repository)) continue + if (source.branch) { + try { + Repository.validateBranch(source.branch) + } catch { + continue + } + } + materialized.set( + name, + new Info({ + name, + path: AbsolutePath.make(Repository.cachePath(global.repos, repository, source.branch)), + ...(source.description === undefined ? {} : { description: source.description }), + ...(source.hidden === undefined ? {} : { hidden: source.hidden }), + source, + }), + ) + yield* cache.ensure({ reference: repository, branch: source.branch, refresh: true }).pipe( + Effect.catchCause((cause) => + Effect.logWarning("failed to materialize reference", { + name, + repository: source.repository, + cause, + }), + ), + Effect.forkIn(scope), + ) + } + yield* events.publish(Event.Updated, {}) + }), + }) + + return Service.of({ + transform: state.transform, + // kilocode_change start - reconcile Kilo's effective config without a request-scoped transform slot. + replace: (sources) => + state.mutate((editor) => + Effect.sync(() => { + for (const [name] of editor.list()) editor.remove(name) + for (const [name, source] of sources) editor.add(name, source) + }), + ), + // kilocode_change end + reload: state.reload, + list: Effect.fn("Reference.list")(function* () { + return Array.from(materialized.values()) + }), + }) + }), +) + +export const locationLayer = layer + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [Global.node, EventV2.node, RepositoryCache.node], +}) diff --git a/packages/core/src/reference/guidance.ts b/packages/core/src/reference/guidance.ts new file mode 100644 index 00000000000..25566e2f2f6 --- /dev/null +++ b/packages/core/src/reference/guidance.ts @@ -0,0 +1,69 @@ +export * as ReferenceGuidance from "./guidance" + +import { makeLocationNode } from "../effect/app-node" +import { Context, Effect, Layer, Schema } from "effect" +import { Reference } from "../reference" +import { SystemContext } from "../system-context/index" + +const Summary = Schema.Struct({ + name: Schema.String, + path: Schema.String, + description: Schema.String.pipe(Schema.optional), +}) + +const render = (references: ReadonlyArray) => + [ + "Project references provide additional directories that can be accessed when relevant.", + "", + ...references.flatMap((reference) => [ + " ", + ` ${reference.name}`, + ` ${reference.path}`, + ...(reference.description === undefined ? [] : [` ${reference.description}`]), + " ", + ]), + "", + ].join("\n") + +export interface Interface { + readonly load: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/ReferenceGuidance") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const references = yield* Reference.Service + + return Service.of({ + load: Effect.fn("ReferenceGuidance.load")(function* () { + const available = (yield* references.list()) + .filter((reference) => reference.description !== undefined) + .map((reference) => ({ + name: reference.name, + path: reference.path, + description: reference.description, + })) + .toSorted((a, b) => a.name.localeCompare(b.name)) + if (available.length === 0) return SystemContext.empty + return SystemContext.make({ + key: SystemContext.Key.make("core/reference-guidance"), + codec: Schema.toCodecJson(Schema.Array(Summary)), + load: Effect.succeed(available), + baseline: render, + update: (_previous, current) => + [ + "The available project references have changed. This list supersedes the previous reference list.", + render(current), + ].join("\n"), + removed: () => "Project reference guidance is no longer available. Do not use previously listed references.", + }) + }), + }) + }), +) + +export const locationLayer = layer + +export const node = makeLocationNode({ service: Service, layer, deps: [Reference.node] }) diff --git a/packages/core/src/repository-cache.ts b/packages/core/src/repository-cache.ts new file mode 100644 index 00000000000..baa9a93a086 --- /dev/null +++ b/packages/core/src/repository-cache.ts @@ -0,0 +1,264 @@ +/** + * Local tracking checkouts for remote Git references, one per remote and + * branch. Each checkout permanently tracks a single ref: the requested branch + * when the cache key has one, otherwise the remote default branch. Content + * follows "newest wins": refresh fetches and hard-resets, so readers may + * observe the checkout move underneath them. + */ +import path from "path" +import { Context, Effect, Layer, Schema } from "effect" +import { FSUtil } from "./fs-util" +import { Git } from "./git" +import { Global } from "./global" +import { Repository } from "./repository" +import { AbsolutePath } from "./schema" +import { makeGlobalNode } from "./effect/app-node" +import { EffectFlock } from "./util/effect-flock" + +export type Result = { + readonly repository: string + readonly host: string + readonly remote: string + readonly localPath: string + readonly status: "cached" | "cloned" | "refreshed" + readonly head?: string + readonly branch?: string +} + +export type EnsureInput = { + readonly reference: Repository.RemoteReference + readonly refresh?: boolean + readonly branch?: string +} + +export class InvalidRepositoryError extends Schema.TaggedErrorClass()( + "RepositoryCacheInvalidRepositoryError", + { + repository: Schema.String, + message: Schema.String, + }, +) {} + +export class InvalidBranchError extends Schema.TaggedErrorClass()( + "RepositoryCacheInvalidBranchError", + { + branch: Schema.String, + message: Schema.String, + }, +) {} + +export class CloneFailedError extends Schema.TaggedErrorClass()("RepositoryCacheCloneFailedError", { + repository: Schema.String, + message: Schema.String, +}) {} + +export class FetchFailedError extends Schema.TaggedErrorClass()("RepositoryCacheFetchFailedError", { + repository: Schema.String, + message: Schema.String, +}) {} + +export class CheckoutFailedError extends Schema.TaggedErrorClass()( + "RepositoryCacheCheckoutFailedError", + { + repository: Schema.String, + branch: Schema.String, + message: Schema.String, + }, +) {} + +export class ResetFailedError extends Schema.TaggedErrorClass()("RepositoryCacheResetFailedError", { + repository: Schema.String, + message: Schema.String, +}) {} + +export class LockFailedError extends Schema.TaggedErrorClass()("RepositoryCacheLockFailedError", { + localPath: Schema.String, + message: Schema.String, +}) {} + +export class CacheOperationError extends Schema.TaggedErrorClass()( + "RepositoryCacheOperationError", + { + operation: Schema.String, + path: Schema.String, + message: Schema.String, + }, +) {} + +export type Error = + | InvalidRepositoryError + | InvalidBranchError + | CloneFailedError + | FetchFailedError + | CheckoutFailedError + | ResetFailedError + | LockFailedError + | CacheOperationError + +export interface Interface { + readonly ensure: (input: EnsureInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/RepositoryCache") {} + +export function isError(error: unknown): error is Error { + return ( + error instanceof InvalidRepositoryError || + error instanceof InvalidBranchError || + error instanceof CloneFailedError || + error instanceof FetchFailedError || + error instanceof CheckoutFailedError || + error instanceof ResetFailedError || + error instanceof LockFailedError || + error instanceof CacheOperationError + ) +} + +export const parseRemote = Effect.fn("RepositoryCache.parseRemote")(function* (repository: string) { + return yield* Effect.try({ + try: () => Repository.parseRemote(repository), + catch: (error) => new InvalidRepositoryError({ repository, message: errorMessage(error) }), + }) +}) + +export const validateBranch = Effect.fn("RepositoryCache.validateBranch")(function* (branch: string) { + return yield* Effect.try({ + try: () => Repository.validateBranch(branch), + catch: (error) => new InvalidBranchError({ branch, message: errorMessage(error) }), + }) +}) + +const layer: Layer.Layer = + Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const git = yield* Git.Service + const flock = yield* EffectFlock.Service + const global = yield* Global.Service + + return Service.of({ + ensure: Effect.fn("RepositoryCache.ensure")(function* (input) { + if (input.branch) yield* validateBranch(input.branch) + + const repository = input.reference.label + const localPath = Repository.cachePath(global.repos, input.reference, input.branch) + const cloneTarget = Repository.parse(input.reference.remote) ?? input.reference + + return yield* flock + .withLock( + Effect.gen(function* () { + yield* cacheOperation(fs.ensureDir(path.dirname(localPath)), "ensure cache directory", localPath) + + const existing = yield* git.repo.discover(AbsolutePath.make(localPath)) + const origin = existing ? yield* git.remote.get(existing) : undefined + const originReference = origin ? Repository.parse(origin) : undefined + // Discovery walks upward, so an enclosing repository with a + // matching origin could masquerade as the cache entry; reuse + // requires the checkout to live exactly at the cache path. + const root = yield* fs.existsSafe(path.join(localPath, ".git")) // kilocode_change - avoid Windows path spelling differences + // kilocode_change start - canonicalize file remotes rewritten by Git on Windows + const match = + originReference && Repository.isFile(originReference) && Repository.isFile(cloneTarget) + ? (yield* fs.resolve(originReference.path)) === (yield* fs.resolve(cloneTarget.path)) + : originReference && Repository.same(originReference, cloneTarget) + // kilocode_change end + const reuse = Boolean( + existing && + root && // kilocode_change + match, // kilocode_change + ) + if (!reuse && (yield* fs.existsSafe(localPath))) { + yield* cacheOperation(fs.remove(localPath, { recursive: true }), "remove stale cache", localPath) + } + + const status = !reuse + ? ("cloned" as const) + : input.refresh + ? ("refreshed" as const) + : ("cached" as const) + + if (status === "cloned") { + yield* git.repo + .clone({ + remote: input.reference.remote, + directory: AbsolutePath.make(localPath), + branch: input.branch, + }) + .pipe(Effect.mapError((error) => new CloneFailedError({ repository, message: error.message }))) + } + + if (status === "refreshed") { + if (!existing) + return yield* new FetchFailedError({ repository, message: "Repository is unavailable" }) + yield* git.sync + .fetchRemotes(existing) + .pipe(Effect.mapError((error) => new FetchFailedError({ repository, message: error.message }))) + + if (input.branch) { + yield* git.sync + .fetchBranch(existing, { branch: input.branch }) + .pipe(Effect.mapError((error) => new FetchFailedError({ repository, message: error.message }))) + } + + // Checking out the tracked ref before resetting keeps the + // checkout self-healing even if it was left on another + // branch. + const branch = input.branch ?? (yield* git.history.defaultRemoteBranch(existing)) + if (branch) { + yield* git.sync + .checkoutRemoteBranch(existing, { branch }) + .pipe( + Effect.mapError( + (error) => new CheckoutFailedError({ repository, branch, message: error.message }), + ), + ) + } + + const target = branch ?? (yield* git.history.branch(existing)) + yield* git.sync + .resetHard(existing, target ? `origin/${target}` : "HEAD") + .pipe(Effect.mapError((error) => new ResetFailedError({ repository, message: error.message }))) + } + + const checkout = yield* git.repo.discover(AbsolutePath.make(localPath)) + + return { + repository, + host: input.reference.host, + remote: input.reference.remote, + localPath, + status, + head: checkout ? yield* git.history.head(checkout) : undefined, + branch: checkout ? yield* git.history.branch(checkout) : undefined, + } satisfies Result + }), + `repository-cache:${localPath}`, + ) + .pipe( + Effect.mapError((error) => + isError(error) ? error : new LockFailedError({ localPath, message: errorMessage(error) }), + ), + ) + }), + }) + }), + ) + +export const node = makeGlobalNode({ + service: Service, + layer, + deps: [EffectFlock.node, FSUtil.node, Git.node, Global.node], +}) + +function errorMessage(error: unknown) { + return error instanceof globalThis.Error ? error.message : String(error) +} + +function cacheOperation(effect: Effect.Effect, operation: string, target: string) { + return effect.pipe( + Effect.mapError((error) => new CacheOperationError({ operation, path: target, message: errorMessage(error) })), + ) +} + +export * as RepositoryCache from "./repository-cache" diff --git a/packages/core/src/repository.ts b/packages/core/src/repository.ts new file mode 100644 index 00000000000..c9b892733f2 --- /dev/null +++ b/packages/core/src/repository.ts @@ -0,0 +1,214 @@ +import path from "path" +import { fileURLToPath } from "url" +import { Schema } from "effect" + +type BaseReference = { + readonly host: string + readonly path: string + readonly segments: string[] + readonly owner?: string + readonly repo: string + readonly remote: string + readonly label: string +} + +export type RemoteReference = BaseReference & { + readonly protocol?: string +} + +export type FileReference = BaseReference & { + readonly host: "file" + readonly protocol: "file:" +} + +export type Reference = RemoteReference | FileReference + +export class InvalidReferenceError extends Schema.TaggedErrorClass()( + "RepositoryInvalidReferenceError", + { + repository: Schema.String, + message: Schema.String, + }, +) {} + +export class UnsupportedLocalRepositoryError extends Schema.TaggedErrorClass()( + "RepositoryUnsupportedLocalRepositoryError", + { + repository: Schema.String, + message: Schema.String, + }, +) {} + +export class InvalidBranchError extends Schema.TaggedErrorClass()("RepositoryInvalidBranchError", { + branch: Schema.String, + message: Schema.String, +}) {} + +export type Error = InvalidReferenceError | UnsupportedLocalRepositoryError | InvalidBranchError + +export function isError(error: unknown): error is Error { + return ( + error instanceof InvalidReferenceError || + error instanceof UnsupportedLocalRepositoryError || + error instanceof InvalidBranchError + ) +} + +export function parse(input: string): Reference | undefined { + const cleaned = normalizeInput(input) + if (!cleaned) return + + const githubPrefixed = cleaned.match(/^github:([^/\s]+)\/([^/\s]+)$/) + if (githubPrefixed) return buildRemote({ host: "github.com", segments: [githubPrefixed[1], githubPrefixed[2]] }) + + if (!cleaned.includes("://")) { + const scp = cleaned.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/) + if (scp) return buildRemote({ host: scp[1], segments: parts(scp[2]), remote: cleaned }) + + const direct = parts(cleaned) + if (direct.length >= 2 && hostLike(direct[0])) return buildRemote({ host: direct[0], segments: direct.slice(1) }) + if (direct.length === 2) return buildRemote({ host: "github.com", segments: direct }) + } + + try { + const url = new URL(cleaned) + if (url.protocol === "file:") return buildFile({ url, remote: cleaned }) + const segments = parts(url.pathname) + return buildRemote({ + host: url.host, + segments, + remote: url.host === "github.com" ? githubRemote(segments.join("/")) : cleaned, + protocol: url.protocol, + }) + } catch { + return + } +} + +export function parseRemote(input: string): RemoteReference { + const reference = parse(input) + if (!reference) { + throw new InvalidReferenceError({ + repository: input, + message: "Repository must be a git URL, host/path reference, or GitHub owner/repo shorthand", + }) + } + if (!isRemote(reference)) { + throw new UnsupportedLocalRepositoryError({ + repository: input, + message: "Local file repositories are not supported", + }) + } + return reference +} + +export function validateBranch(branch: string): void { + if (/^[A-Za-z0-9/_.-]+$/.test(branch) && !branch.startsWith("-") && !branch.includes("..")) return + throw new InvalidBranchError({ + branch, + message: "Branch must contain only alphanumeric characters, /, _, ., and -, and cannot start with - or contain ..", + }) +} + +export function isFile(reference: Reference): reference is FileReference { + return reference.protocol === "file:" +} + +export function isRemote(reference: Reference): reference is RemoteReference { + return !isFile(reference) +} + +/** + * Checkouts are keyed by remote and branch: a branch-specific reference gets + * its own directory so branchless refreshes can never move it. The branch is + * percent-encoded because valid branch names may contain `/`. + */ +export function cachePath(root: string, reference: Reference, branch?: string): string { + const base = path.join(root, ...reference.host.split(":"), ...reference.segments) + return branch ? `${base}@${encodeURIComponent(branch)}` : base +} + +export function cacheIdentity(reference: Reference): string { + return `${reference.host}/${reference.path}` +} + +export function same(left: Reference, right: Reference): boolean { + return cacheIdentity(left) === cacheIdentity(right) +} + +function normalizeInput(input: string) { + return input + .trim() + .replace(/^git\+/, "") + .replace(/#.*$/, "") + .replace(/\/+$/, "") +} + +function trimGitSuffix(input: string) { + return input.replace(/\.git$/, "") +} + +function parts(input: string) { + return input + .split("/") + .map((item) => trimGitSuffix(item.trim())) + .filter(Boolean) +} + +function safeHost(input: string) { + return Boolean(input) && !input.startsWith("-") && !/[\s/\\]/.test(input) +} + +function safeSegment(input: string) { + return input !== "." && input !== ".." && !input.includes(":") && !/[\s/\\]/.test(input) +} + +function hostLike(input: string) { + return input.includes(".") || input.includes(":") || input === "localhost" +} + +function withSlash(input: string) { + return input.endsWith("/") ? input : `${input}/` +} + +function githubRemote(pathname: string) { + const base = process.env.KILO_REPO_CLONE_GITHUB_BASE_URL + if (!base) return `https://github.com/${pathname}.git` + return new URL(`${pathname}.git`, withSlash(base)).href +} + +function buildRemote(input: { host: string; segments: string[]; remote?: string; protocol?: string }) { + const segments = input.segments.map(trimGitSuffix).filter(Boolean) + if (!safeHost(input.host) || !segments.length || segments.some((segment) => !safeSegment(segment))) return + const repositoryPath = segments.join("/") + const host = input.host.toLowerCase() + return { + host, + path: repositoryPath, + segments, + owner: segments.length === 2 ? segments[0] : undefined, + repo: segments[segments.length - 1], + remote: + input.remote ?? (host === "github.com" ? githubRemote(repositoryPath) : `https://${host}/${repositoryPath}.git`), + label: host === "github.com" && segments.length === 2 ? repositoryPath : `${host}/${repositoryPath}`, + protocol: input.protocol, + } satisfies RemoteReference +} + +function buildFile(input: { url: URL; remote: string }) { + const filePath = path.normalize(fileURLToPath(input.url)) + const segments = filePath.split(/[\\/]+/).filter(Boolean) + if (!segments.length) return + return { + host: "file", + path: filePath, + segments: segments.map((segment) => segment.replace(/:$/, "")), + owner: undefined, + repo: trimGitSuffix(segments[segments.length - 1]), + remote: input.remote, + label: filePath, + protocol: "file:", + } satisfies FileReference +} + +export * as Repository from "./repository" diff --git a/packages/core/src/ripgrep.ts b/packages/core/src/ripgrep.ts new file mode 100644 index 00000000000..71ee912e249 --- /dev/null +++ b/packages/core/src/ripgrep.ts @@ -0,0 +1,332 @@ +export * as Ripgrep from "./ripgrep" + +import { Context, Duration, Effect, Fiber, Layer, Schema, Stream } from "effect" +import { ChildProcess } from "effect/unstable/process" +import { makeGlobalNode } from "./effect/app-node" +import { Entry, Match } from "@opencode-ai/schema/filesystem" +import * as KiloGrep from "./kilocode/ripgrep-grep" // kilocode_change +import * as SpawnExit from "./kilocode/spawn-exit" // kilocode_change +import * as SpawnValidation from "./kilocode/spawn-validation" // kilocode_change +import { AppProcess, collectStream, waitForAbort } from "./process" +import { NonNegativeInt, PositiveInt, RelativePath } from "./schema" +import { RipgrepBinary } from "./ripgrep/binary" + +/** + * Small core-owned ripgrep execution adapter. It deliberately exposes raw + * process-oriented rows, not model text or permission behavior. Search maps + * these rows into filesystem results; leaf tools own + * presentation and permission prompts. + */ + +const ERROR_BYTES = 8 * 1024 +const MAX_RECORD_BYTES = 64 * 1024 +const MAX_SUBMATCHES = 100 + +const RawMatch = Schema.Struct({ + type: Schema.Literals(["match", "context"]), // kilocode_change - retain requested context records + data: Schema.Struct({ + path: Schema.Struct({ text: Schema.String }), + lines: Schema.Struct({ text: Schema.String }), + line_number: PositiveInt, + absolute_offset: NonNegativeInt, + submatches: Schema.Array( + Schema.Struct({ + match: Schema.Struct({ text: Schema.String }), + start: NonNegativeInt, + end: NonNegativeInt, + }), + ), + }), +}) + +type RawMatchData = (typeof RawMatch.Type)["data"] & { readonly context: boolean } // kilocode_change + +export class Error extends Schema.TaggedErrorClass()("Ripgrep.Error", { + message: Schema.String, + cause: Schema.optional(Schema.Defect()), +}) {} + +export class InvalidPatternError extends Schema.TaggedErrorClass()("Ripgrep.InvalidPatternError", { + pattern: Schema.String, + message: Schema.String, +}) {} + +export interface FindInput { + readonly cwd: string + readonly pattern: string + readonly limit: number + readonly hidden?: boolean + readonly follow?: boolean + readonly signal?: AbortSignal + readonly onEntry?: (entry: Entry) => Effect.Effect +} + +export interface GlobInput { + readonly cwd: string + readonly pattern: string + readonly limit: number + readonly hidden?: boolean + readonly follow?: boolean + readonly signal?: AbortSignal + readonly validate?: Effect.Effect // kilocode_change - bind approved searches at spawn +} + +export interface GrepInput extends KiloGrep.Options { + // kilocode_change + readonly cwd: string + readonly pattern: string + readonly file?: string + readonly include?: string + readonly limit: number + readonly signal?: AbortSignal + readonly validate?: Effect.Effect // kilocode_change - bind approved searches at spawn +} + +export interface Interface { + readonly find: (input: FindInput) => Effect.Effect + readonly glob: (input: GlobInput) => Effect.Effect, Error> // kilocode_change + readonly grep: (input: GrepInput) => Effect.Effect, Error | InvalidPatternError> // kilocode_change +} + +// kilocode_change start - retain truncation state through model-facing tools +export interface SearchResult { + readonly items: readonly A[] + readonly truncated: boolean + readonly partial: boolean +} +// kilocode_change end + +export class Service extends Context.Service()("@opencode/v2/Ripgrep") {} + +const failure = (message: string, cause?: unknown) => new Error({ message, cause }) + +const isInvalidPattern = (stderr: string) => + stderr.includes("regex parse error") || stderr.includes("error parsing regex") + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const process = yield* AppProcess.Service + const binary = yield* RipgrepBinary.Service + + const run = (input: { + readonly cwd: string + readonly args: string[] + readonly limit: number + readonly signal?: AbortSignal + readonly parse: (line: string) => Effect.Effect + readonly pattern?: string + readonly onItem?: (item: A) => Effect.Effect + readonly stop?: (item: A) => boolean // kilocode_change - stop bounded searches at the overflow match + readonly validate?: Effect.Effect // kilocode_change - spawn-bound target validation + }) => { + const program = Effect.scoped( + Effect.gen(function* () { + const filepath = yield* binary.filepath + // kilocode_change start - validate approved targets after all spawn preparation + const command = ChildProcess.make(filepath, input.args, { + cwd: input.cwd, + extendEnv: true, + stdin: "ignore", + forceKillAfter: input.stop ? Duration.seconds(1) : undefined, // kilocode_change - bound grep interruption + }) + const validated = input.validate ? SpawnValidation.attach(command, input.validate) : command + const spawned = input.stop ? SpawnExit.attach(validated) : validated // kilocode_change + const handle = yield* process.spawn(spawned) + // kilocode_change end + const stderrFiber = yield* collectStream(handle.stderr, ERROR_BYTES).pipe( + Effect.map((output) => output.buffer.toString("utf8")), + Effect.forkScoped, + ) + let observed = 0 + let stopped = false // kilocode_change + const take = input.stop // kilocode_change start + ? Stream.takeUntil((row) => { + stopped = input.stop?.(row) ?? false + return stopped + }) + : Stream.take(input.limit + 1) // kilocode_change end + const rows = yield* Stream.decodeText(handle.stdout).pipe( + Stream.splitLines, + Stream.filter((line) => line.length > 0), + Stream.mapEffect(input.parse), + Stream.filter((row): row is A => row !== undefined), + Stream.tap((row) => { + if (!input.onItem || observed++ >= input.limit) return Effect.void + return input.onItem(row) + }), + take, // kilocode_change + Stream.runCollect, + Effect.map((chunk) => [...chunk]), + ) + if (stopped) return { items: rows, truncated: true, partial: false } // kilocode_change + const truncated = input.stop ? false : rows.length > input.limit // kilocode_change - custom stop owns truncation + if (truncated) return { items: rows.slice(0, input.limit), truncated, partial: false } + + const code = yield* handle.exitCode + const stderr = yield* Fiber.join(stderrFiber) + if (input.pattern && code === 2 && isInvalidPattern(stderr)) { + return yield* new InvalidPatternError({ pattern: input.pattern, message: stderr.trim() }) + } + if (code !== 0 && code !== 1 && code !== 2) { + return yield* failure(stderr.trim() || `ripgrep failed with code ${code}`) + } + return { items: code === 1 ? [] : rows, truncated: false, partial: code === 2 } + }), + ) + const abortable = input.signal ? program.pipe(Effect.raceFirst(waitForAbort(input.signal))) : program + return abortable.pipe( + // kilocode_change start - surface the underlying reason instead of a bare wrapper message + Effect.mapError((cause) => { + if (cause instanceof Error || cause instanceof InvalidPatternError) return cause + const detail = cause instanceof globalThis.Error && cause.message.trim() ? `: ${cause.message.trim()}` : "" + return failure(`ripgrep execution failed${detail}`, cause) + }), + // kilocode_change end + ) + } + + return Service.of({ + glob: (input) => + run({ + cwd: input.cwd, + limit: input.limit, + signal: input.signal, + validate: input.validate, // kilocode_change - preserve spawn-bound target validation + args: [ + "--no-config", + "--files", + ...(input.hidden ? ["--hidden"] : []), + ...(input.follow ? ["--follow"] : []), + `--glob=${input.pattern}`, + "--glob=!**/.git/**", + ".", + ], + parse: (line) => + Effect.succeed( + line + .replace(/^(?:\.[\\/])+/u, "") + .replace(/^[\\/]+/u, "") + .replaceAll("\\", "/"), + ), + }).pipe( + // kilocode_change start - retain spawn metadata after mapping paths + Effect.map((result) => ({ + ...result, + items: result.items.map((relative) => + Entry.make({ + path: RelativePath.make(relative), + type: "file", + }), + ), + })), + // kilocode_change end + Effect.catchTag("Ripgrep.InvalidPatternError", (cause) => Effect.fail(failure(cause.message, cause))), + ), + find: (input) => + run({ + cwd: input.cwd, + limit: input.limit, + signal: input.signal, + args: [ + "--no-config", + "--files", + ...(input.hidden ? ["--hidden"] : []), + ...(input.follow ? ["--follow"] : []), + ...(input.pattern === "*" ? [] : [`--glob=${input.pattern}`]), + "--glob=!**/.git/**", + ".", + ], + parse: (line) => { + const relative = line + .replace(/^(?:\.[\\/])+/u, "") + .replace(/^[\\/]+/u, "") + .replaceAll("\\", "/") + return Effect.succeed( + Entry.make({ + path: RelativePath.make(relative), + type: "file", + }), + ) + }, + onItem: input.onEntry, + }).pipe( + Effect.map((result) => result.items), + Effect.catchTag("Ripgrep.InvalidPatternError", (cause) => Effect.fail(failure(cause.message, cause))), + ), + grep: (input) => + run({ + ...input, + stop: KiloGrep.stop(input.limit), // kilocode_change + args: [ + "--no-config", + "--json", + "--hidden", + "--no-messages", + ...KiloGrep.flags(input), // kilocode_change + ...(input.include ? [`--glob=${input.include}`] : []), + "--glob=!**/.git/**", + "--", + input.pattern, + input.file ?? ".", + ], + parse: (line) => + (Buffer.byteLength(line, "utf8") > MAX_RECORD_BYTES + ? Effect.fail(failure(`Ripgrep JSON record exceeded ${MAX_RECORD_BYTES} bytes`)) + : Effect.try({ + try: () => JSON.parse(line) as unknown, + catch: (cause) => failure("Invalid ripgrep JSON output", cause), + }) + ).pipe( + Effect.flatMap((json) => { + if ( + !json || + typeof json !== "object" || + !("type" in json) || + (json.type !== "match" && json.type !== "context") // kilocode_change + ) + return Effect.succeed(undefined) + return Schema.decodeUnknownEffect(RawMatch)(json).pipe( + Effect.map((match) => ({ + ...match.data, + path: { text: match.data.path.text.replace(/^\.[\\/]/, "") }, + submatches: match.data.submatches.slice(0, MAX_SUBMATCHES), + context: match.type === "context", // kilocode_change + })), + Effect.mapError((cause) => failure("Invalid ripgrep match output", cause)), + ) + }), + ), + }).pipe( + // kilocode_change start - retain spawn metadata after mapping matches + Effect.map((result) => ({ + ...result, + items: KiloGrep.select(input, result.items).map((match) => { + const relative = match.path.text + .replace(/^(?:\.[\\/])+/u, "") + .replace(/^[\\/]+/u, "") + .replaceAll("\\", "/") + const item = Match.make({ + entry: Entry.make({ + path: RelativePath.make(relative), + type: "file", + }), + line: match.line_number, + offset: match.absolute_offset, + text: match.lines.text.length > 2_000 ? match.lines.text.slice(0, 2_000) + "..." : match.lines.text, + submatches: match.submatches.map((submatch) => ({ + text: submatch.match.text, + start: submatch.start, + end: submatch.end, + })), + }) + return KiloGrep.decorate(item, match.context, match.lines.text.length > 2_000) + }), + })), + // kilocode_change end + ), + }) + }), +) + +export const node = makeGlobalNode({ service: Service, layer: layer, deps: [RipgrepBinary.node, AppProcess.node] }) diff --git a/packages/core/src/ripgrep/binary.ts b/packages/core/src/ripgrep/binary.ts new file mode 100644 index 00000000000..e1c7ec664b0 --- /dev/null +++ b/packages/core/src/ripgrep/binary.ts @@ -0,0 +1,133 @@ +import path from "path" +import { Context, Effect, Layer, Stream } from "effect" +import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" +import { ChildProcess } from "effect/unstable/process" +import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" +import { CrossSpawnSpawner } from "../cross-spawn-spawner" +import { makeGlobalNode } from "../effect/app-node" +import { httpClient } from "../effect/app-node-platform" +import { FSUtil } from "../fs-util" +import { Global } from "../global" +import { which } from "../util/which" + +export namespace RipgrepBinary { + const VERSION = "15.1.0" + const PLATFORM = { + "arm64-darwin": { platform: "aarch64-apple-darwin", extension: "tar.gz" }, + "arm64-linux": { platform: "aarch64-unknown-linux-gnu", extension: "tar.gz" }, + "x64-darwin": { platform: "x86_64-apple-darwin", extension: "tar.gz" }, + "x64-linux": { platform: "x86_64-unknown-linux-musl", extension: "tar.gz" }, + "arm64-win32": { platform: "aarch64-pc-windows-msvc", extension: "zip" }, + "ia32-win32": { platform: "i686-pc-windows-msvc", extension: "zip" }, + "x64-win32": { platform: "x86_64-pc-windows-msvc", extension: "zip" }, + } as const + + interface Interface { + readonly filepath: Effect.Effect + } + + export class Service extends Context.Service()("@opencode/RipgrepBinary") {} + + const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const http = HttpClient.filterStatusOk(yield* HttpClient.HttpClient) + const spawner = yield* ChildProcessSpawner + + const run = Effect.fnUntraced(function* (command: string, args: string[]) { + const handle = yield* spawner.spawn(ChildProcess.make(command, args, { extendEnv: true, stdin: "ignore" })) + const [stdout, stderr, code] = yield* Effect.all( + [ + Stream.mkString(Stream.decodeText(handle.stdout)), + Stream.mkString(Stream.decodeText(handle.stderr)), + handle.exitCode, + ], + { concurrency: "unbounded" }, + ) + return { stdout, stderr, code } + }, Effect.scoped) + + const extract = Effect.fnUntraced(function* ( + archive: string, + config: (typeof PLATFORM)[keyof typeof PLATFORM], + target: string, + ) { + const dir = yield* fs.makeTempDirectoryScoped({ directory: Global.Path.bin, prefix: "ripgrep-" }) + + if (config.extension === "zip") { + const shell = (yield* Effect.sync(() => which("powershell.exe") ?? which("pwsh.exe"))) ?? "powershell.exe" + const result = yield* run(shell, [ + "-NoProfile", + "-NonInteractive", + "-Command", + `$global:ProgressPreference = 'SilentlyContinue'; Expand-Archive -LiteralPath '${archive.replaceAll("'", "''")}' -DestinationPath '${dir.replaceAll("'", "''")}' -Force`, + ]) + if (result.code !== 0) + throw new Error( + result.stderr.trim() || result.stdout.trim() || `ripgrep extraction failed with code ${result.code}`, + ) + } + + if (config.extension === "tar.gz") { + const result = yield* run("tar", ["-xzf", archive, "-C", dir]) + if (result.code !== 0) + throw new Error( + result.stderr.trim() || result.stdout.trim() || `ripgrep extraction failed with code ${result.code}`, + ) + } + + const extracted = path.join( + dir, + `ripgrep-${VERSION}-${config.platform}`, + process.platform === "win32" ? "rg.exe" : "rg", + ) + if (!(yield* fs.isFile(extracted))) throw new Error(`ripgrep archive did not contain executable: ${extracted}`) + + yield* fs.copyFile(extracted, target) + if (process.platform !== "win32") yield* fs.chmod(target, 0o755) + }, Effect.scoped) + + return Service.of({ + filepath: yield* Effect.cached( + Effect.gen(function* () { + // kilocode_change - Git for Windows may expose an incompatible MSYS rg.exe + const system = yield* Effect.sync(() => (process.platform === "win32" ? undefined : which("rg"))) + if (system && (yield* fs.isFile(system).pipe(Effect.orDie))) return system + + const target = path.join(Global.Path.bin, `rg${process.platform === "win32" ? ".exe" : ""}`) + if (yield* fs.isFile(target).pipe(Effect.orDie)) return target + + const platformKey = `${process.arch}-${process.platform}` as keyof typeof PLATFORM + const config = PLATFORM[platformKey] + if (!config) throw new Error(`unsupported platform for ripgrep: ${platformKey}`) + + const filename = `ripgrep-${VERSION}-${config.platform}.${config.extension}` + const url = `https://github.com/BurntSushi/ripgrep/releases/download/${VERSION}/${filename}` + const archive = path.join(Global.Path.bin, filename) + + yield* Effect.logInfo("downloading ripgrep", { url }) + yield* fs.ensureDir(Global.Path.bin).pipe(Effect.orDie) + const bytes = yield* HttpClientRequest.get(url).pipe( + http.execute, + Effect.flatMap((response) => response.arrayBuffer), + Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))), + ) + if (bytes.byteLength === 0) throw new Error(`failed to download ripgrep from ${url}`) + + yield* fs.writeWithDirs(archive, new Uint8Array(bytes)) + yield* extract(archive, config, target) + yield* fs.remove(archive, { force: true }).pipe(Effect.ignore) + return target + }), + ), + }) + }), + ) + + export const node = makeGlobalNode({ + service: Service, + layer: layer, + deps: [FSUtil.node, httpClient, CrossSpawnSpawner.node], + }) +} diff --git a/packages/core/src/schema.ts b/packages/core/src/schema.ts index 523a4eace5d..345cc222450 100644 --- a/packages/core/src/schema.ts +++ b/packages/core/src/schema.ts @@ -1,32 +1,20 @@ -import { Option, Schema, SchemaGetter } from "effect" +import { Schema } from "effect" +import { + AbsolutePath, + DateTimeUtcFromMillis, + NonNegativeInt, + optional, + PositiveInt, + RelativePath, + statics, +} from "@opencode-ai/schema/schema" -export const AbsolutePath = Schema.String.pipe(Schema.brand("AbsolutePath")) -export type AbsolutePath = typeof AbsolutePath.Type +export { AbsolutePath, DateTimeUtcFromMillis, NonNegativeInt, optional, PositiveInt, RelativePath, statics } -export const RelativePath = Schema.String.pipe(Schema.brand("RelativePath")) -export type RelativePath = typeof RelativePath.Type - -/** - * Integer greater than zero. - */ -export const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0)) - -/** - * Integer greater than or equal to zero. - */ -export const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) - -/** - * Optional public JSON field that can hold explicit `undefined` on the type - * side but encodes it as an omitted key, matching legacy `JSON.stringify`. - */ -export const optionalOmitUndefined = (schema: S) => - Schema.optionalKey(schema).pipe( - Schema.decodeTo(Schema.optional(schema), { - decode: SchemaGetter.passthrough({ strict: false }), - encode: SchemaGetter.transformOptional(Option.filter((value) => value !== undefined)), - }), - ) +// kilocode_change start - compatibility aliases for Kilo-owned schemas +export const optionalOmitUndefined = optional +export const withStatics = statics +// kilocode_change end /** * Strip `readonly` from a nested type. Stand-in for `effect`'s `Types.DeepMutable` @@ -56,22 +44,6 @@ export type DeepMutable = T extends string | number | boolean | bigint | symb ? { -readonly [K in keyof T]: DeepMutable } : T -/** - * Attach static methods to a schema object. Designed to be used with `.pipe()`: - * - * @example - * export const Foo = fooSchema.pipe( - * withStatics((schema) => ({ - * zero: schema.make(0), - * from: Schema.decodeUnknownOption(schema), - * })) - * ) - */ -export const withStatics = - >(methods: (schema: S) => M) => - (schema: S): S & M => - Object.assign(schema, methods(schema)) - /** * Nominal wrapper for scalar types. The class itself is a valid schema — * pass it directly to `Schema.decode`, `Schema.decodeEffect`, etc. diff --git a/packages/core/src/session-event.ts b/packages/core/src/session-event.ts deleted file mode 100644 index a98d9cc0514..00000000000 --- a/packages/core/src/session-event.ts +++ /dev/null @@ -1,402 +0,0 @@ -import { Schema } from "effect" -import { EventV2 } from "./event" -import { ModelV2 } from "./model" -import { NonNegativeInt } from "./schema" -import { Session } from "./session" -import { FileAttachment, Prompt } from "./session-prompt" -import { ToolOutput } from "./tool-output" -import { V2Schema } from "./v2-schema" - -export { FileAttachment } - -export const Source = Schema.Struct({ - start: NonNegativeInt, - end: NonNegativeInt, - text: Schema.String, -}).annotate({ - identifier: "session.next.event.source", -}) -export type Source = typeof Source.Type - -const Base = { - timestamp: V2Schema.DateTimeUtcFromMillis, - sessionID: Session.ID, -} - -const options = { - aggregate: "sessionID", - version: 1, -} as const - -export const UnknownError = Schema.Struct({ - type: Schema.Literal("unknown"), - message: Schema.String, -}).annotate({ - identifier: "Session.Error.Unknown", -}) -export type UnknownError = typeof UnknownError.Type - -export const AgentSwitched = EventV2.define({ - type: "session.next.agent.switched", - ...options, - schema: { - ...Base, - agent: Schema.String, - }, -}) -export type AgentSwitched = typeof AgentSwitched.Type - -export const ModelSwitched = EventV2.define({ - type: "session.next.model.switched", - ...options, - schema: { - ...Base, - model: ModelV2.Ref, - }, -}) -export type ModelSwitched = typeof ModelSwitched.Type - -export const Prompted = EventV2.define({ - type: "session.next.prompted", - ...options, - schema: { - ...Base, - prompt: Prompt, - }, -}) -export type Prompted = typeof Prompted.Type - -export const Synthetic = EventV2.define({ - type: "session.next.synthetic", - ...options, - schema: { - ...Base, - text: Schema.String, - }, -}) -export type Synthetic = typeof Synthetic.Type - -export namespace Shell { - export const Started = EventV2.define({ - type: "session.next.shell.started", - ...options, - schema: { - ...Base, - callID: Schema.String, - command: Schema.String, - }, - }) - export type Started = typeof Started.Type - - export const Ended = EventV2.define({ - type: "session.next.shell.ended", - ...options, - schema: { - ...Base, - callID: Schema.String, - output: Schema.String, - }, - }) - export type Ended = typeof Ended.Type -} - -export namespace Step { - export const Started = EventV2.define({ - type: "session.next.step.started", - ...options, - schema: { - ...Base, - agent: Schema.String, - model: ModelV2.Ref, - snapshot: Schema.String.pipe(Schema.optional), - }, - }) - export type Started = typeof Started.Type - - export const Ended = EventV2.define({ - type: "session.next.step.ended", - ...options, - schema: { - ...Base, - finish: Schema.String, - cost: Schema.Finite, - tokens: Schema.Struct({ - input: Schema.Finite, - output: Schema.Finite, - reasoning: Schema.Finite, - cache: Schema.Struct({ - read: Schema.Finite, - write: Schema.Finite, - }), - }), - snapshot: Schema.String.pipe(Schema.optional), - }, - }) - export type Ended = typeof Ended.Type - - export const Failed = EventV2.define({ - type: "session.next.step.failed", - ...options, - schema: { - ...Base, - error: UnknownError, - }, - }) - export type Failed = typeof Failed.Type -} - -export namespace Text { - export const Started = EventV2.define({ - type: "session.next.text.started", - ...options, - schema: { - ...Base, - }, - }) - export type Started = typeof Started.Type - - export const Delta = EventV2.define({ - type: "session.next.text.delta", - ...options, - schema: { - ...Base, - delta: Schema.String, - }, - }) - export type Delta = typeof Delta.Type - - export const Ended = EventV2.define({ - type: "session.next.text.ended", - ...options, - schema: { - ...Base, - text: Schema.String, - }, - }) - export type Ended = typeof Ended.Type -} - -export namespace Reasoning { - export const Started = EventV2.define({ - type: "session.next.reasoning.started", - ...options, - schema: { - ...Base, - reasoningID: Schema.String, - }, - }) - export type Started = typeof Started.Type - - export const Delta = EventV2.define({ - type: "session.next.reasoning.delta", - ...options, - schema: { - ...Base, - reasoningID: Schema.String, - delta: Schema.String, - }, - }) - export type Delta = typeof Delta.Type - - export const Ended = EventV2.define({ - type: "session.next.reasoning.ended", - ...options, - schema: { - ...Base, - reasoningID: Schema.String, - text: Schema.String, - }, - }) - export type Ended = typeof Ended.Type -} - -export namespace Tool { - export namespace Input { - export const Started = EventV2.define({ - type: "session.next.tool.input.started", - ...options, - schema: { - ...Base, - callID: Schema.String, - name: Schema.String, - }, - }) - export type Started = typeof Started.Type - - export const Delta = EventV2.define({ - type: "session.next.tool.input.delta", - ...options, - schema: { - ...Base, - callID: Schema.String, - delta: Schema.String, - }, - }) - export type Delta = typeof Delta.Type - - export const Ended = EventV2.define({ - type: "session.next.tool.input.ended", - ...options, - schema: { - ...Base, - callID: Schema.String, - text: Schema.String, - }, - }) - export type Ended = typeof Ended.Type - } - - export const Called = EventV2.define({ - type: "session.next.tool.called", - ...options, - schema: { - ...Base, - callID: Schema.String, - tool: Schema.String, - input: Schema.Record(Schema.String, Schema.Unknown), - provider: Schema.Struct({ - executed: Schema.Boolean, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - }), - }, - }) - export type Called = typeof Called.Type - - export const Progress = EventV2.define({ - type: "session.next.tool.progress", - ...options, - schema: { - ...Base, - callID: Schema.String, - structured: ToolOutput.Structured, - content: Schema.Array(ToolOutput.Content), - }, - }) - export type Progress = typeof Progress.Type - - export const Success = EventV2.define({ - type: "session.next.tool.success", - ...options, - schema: { - ...Base, - callID: Schema.String, - structured: ToolOutput.Structured, - content: Schema.Array(ToolOutput.Content), - provider: Schema.Struct({ - executed: Schema.Boolean, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - }), - }, - }) - export type Success = typeof Success.Type - - export const Failed = EventV2.define({ - type: "session.next.tool.failed", - ...options, - schema: { - ...Base, - callID: Schema.String, - error: UnknownError, - provider: Schema.Struct({ - executed: Schema.Boolean, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - }), - }, - }) - export type Failed = typeof Failed.Type -} - -export const RetryError = Schema.Struct({ - message: Schema.String, - statusCode: Schema.Finite.pipe(Schema.optional), - isRetryable: Schema.Boolean, - responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), - responseBody: Schema.String.pipe(Schema.optional), - metadata: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), -}).annotate({ - identifier: "session.next.retry_error", -}) -export type RetryError = typeof RetryError.Type - -export const Retried = EventV2.define({ - type: "session.next.retried", - ...options, - schema: { - ...Base, - attempt: Schema.Finite, - error: RetryError, - }, -}) -export type Retried = typeof Retried.Type - -export namespace Compaction { - export const Started = EventV2.define({ - type: "session.next.compaction.started", - ...options, - schema: { - ...Base, - reason: Schema.Union([Schema.Literal("auto"), Schema.Literal("manual")]), - }, - }) - export type Started = typeof Started.Type - - export const Delta = EventV2.define({ - type: "session.next.compaction.delta", - ...options, - schema: { - ...Base, - text: Schema.String, - }, - }) - export type Delta = typeof Delta.Type - - export const Ended = EventV2.define({ - type: "session.next.compaction.ended", - ...options, - schema: { - ...Base, - text: Schema.String, - include: Schema.String.pipe(Schema.optional), - }, - }) - export type Ended = typeof Ended.Type -} - -export const All = Schema.Union( - [ - AgentSwitched, - ModelSwitched, - Prompted, - Synthetic, - Shell.Started, - Shell.Ended, - Step.Started, - Step.Ended, - Step.Failed, - Text.Started, - Text.Delta, - Text.Ended, - Tool.Input.Started, - Tool.Input.Delta, - Tool.Input.Ended, - Tool.Called, - Tool.Progress, - Tool.Success, - Tool.Failed, - Reasoning.Started, - Reasoning.Delta, - Reasoning.Ended, - Retried, - Compaction.Started, - Compaction.Delta, - Compaction.Ended, - ], - { - mode: "oneOf", - }, -).pipe(Schema.toTaggedUnion("type")) - -export type Event = typeof All.Type -export type Type = Event["type"] - -export * as SessionEvent from "./session-event" diff --git a/packages/core/src/session-message-updater.ts b/packages/core/src/session-message-updater.ts deleted file mode 100644 index bbdf59c555d..00000000000 --- a/packages/core/src/session-message-updater.ts +++ /dev/null @@ -1,417 +0,0 @@ -import { produce, type WritableDraft } from "immer" -import { SessionEvent } from "./session-event" -import { SessionMessage } from "./session-message" - -export type MemoryState = { - messages: SessionMessage.Message[] -} - -export interface Adapter { - readonly getCurrentAssistant: () => SessionMessage.Assistant | undefined - readonly getCurrentCompaction: () => SessionMessage.Compaction | undefined - readonly getCurrentShell: (callID: string) => SessionMessage.Shell | undefined - readonly updateAssistant: (assistant: SessionMessage.Assistant) => void - readonly updateCompaction: (compaction: SessionMessage.Compaction) => void - readonly updateShell: (shell: SessionMessage.Shell) => void - readonly appendMessage: (message: SessionMessage.Message) => void - readonly finish: () => Result -} - -export function memory(state: MemoryState): Adapter { - const activeAssistantIndex = () => - state.messages.findLastIndex((message) => message.type === "assistant" && !message.time.completed) - const activeCompactionIndex = () => state.messages.findLastIndex((message) => message.type === "compaction") - const activeShellIndex = (callID: string) => - state.messages.findLastIndex((message) => message.type === "shell" && message.callID === callID) - - return { - getCurrentAssistant() { - const index = activeAssistantIndex() - if (index < 0) return - const assistant = state.messages[index] - return assistant?.type === "assistant" ? assistant : undefined - }, - getCurrentCompaction() { - const index = activeCompactionIndex() - if (index < 0) return - const compaction = state.messages[index] - return compaction?.type === "compaction" ? compaction : undefined - }, - getCurrentShell(callID) { - const index = activeShellIndex(callID) - if (index < 0) return - const shell = state.messages[index] - return shell?.type === "shell" ? shell : undefined - }, - updateAssistant(assistant) { - const index = activeAssistantIndex() - if (index < 0) return - const current = state.messages[index] - if (current?.type !== "assistant") return - state.messages[index] = assistant - }, - updateCompaction(compaction) { - const index = activeCompactionIndex() - if (index < 0) return - const current = state.messages[index] - if (current?.type !== "compaction") return - state.messages[index] = compaction - }, - updateShell(shell) { - const index = activeShellIndex(shell.callID) - if (index < 0) return - const current = state.messages[index] - if (current?.type !== "shell") return - state.messages[index] = shell - }, - appendMessage(message) { - state.messages.push(message) - }, - finish() { - return state - }, - } -} - -export function update(adapter: Adapter, event: SessionEvent.Event): Result { - const currentAssistant = adapter.getCurrentAssistant() - type DraftAssistant = WritableDraft - type DraftTool = WritableDraft - type DraftText = WritableDraft - type DraftReasoning = WritableDraft - - const latestTool = (assistant: DraftAssistant | undefined, callID?: string) => - assistant?.content.findLast( - (item): item is DraftTool => item.type === "tool" && (callID === undefined || item.id === callID), - ) - - const latestText = (assistant: DraftAssistant | undefined) => - assistant?.content.findLast((item): item is DraftText => item.type === "text") - - const latestReasoning = (assistant: DraftAssistant | undefined, reasoningID: string) => - assistant?.content.findLast((item): item is DraftReasoning => item.type === "reasoning" && item.id === reasoningID) - - SessionEvent.All.match(event, { - "session.next.agent.switched": (event) => { - adapter.appendMessage( - new SessionMessage.AgentSwitched({ - id: event.id, - type: "agent-switched", - metadata: event.metadata, - agent: event.data.agent, - time: { created: event.data.timestamp }, - }), - ) - }, - "session.next.model.switched": (event) => { - adapter.appendMessage( - new SessionMessage.ModelSwitched({ - id: event.id, - type: "model-switched", - metadata: event.metadata, - model: event.data.model, - time: { created: event.data.timestamp }, - }), - ) - }, - "session.next.prompted": (event) => { - adapter.appendMessage( - new SessionMessage.User({ - id: event.id, - type: "user", - metadata: event.metadata, - text: event.data.prompt.text, - files: event.data.prompt.files, - agents: event.data.prompt.agents, - references: event.data.prompt.references, - time: { created: event.data.timestamp }, - }), - ) - }, - "session.next.synthetic": (event) => { - adapter.appendMessage( - new SessionMessage.Synthetic({ - sessionID: event.data.sessionID, - text: event.data.text, - id: event.id, - type: "synthetic", - time: { created: event.data.timestamp }, - }), - ) - }, - "session.next.shell.started": (event) => { - adapter.appendMessage( - new SessionMessage.Shell({ - id: event.id, - type: "shell", - metadata: event.metadata, - callID: event.data.callID, - command: event.data.command, - output: "", - time: { created: event.data.timestamp }, - }), - ) - }, - "session.next.shell.ended": (event) => { - const currentShell = adapter.getCurrentShell(event.data.callID) - if (currentShell) { - adapter.updateShell( - produce(currentShell, (draft) => { - draft.output = event.data.output - draft.time.completed = event.data.timestamp - }), - ) - } - }, - "session.next.step.started": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - draft.time.completed = event.data.timestamp - }), - ) - } - adapter.appendMessage( - new SessionMessage.Assistant({ - id: event.id, - type: "assistant", - agent: event.data.agent, - model: event.data.model, - time: { created: event.data.timestamp }, - content: [], - snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined, - }), - ) - }, - "session.next.step.ended": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - draft.time.completed = event.data.timestamp - draft.finish = event.data.finish - draft.cost = event.data.cost - draft.tokens = event.data.tokens - if (event.data.snapshot) draft.snapshot = { ...draft.snapshot, end: event.data.snapshot } - }), - ) - } - }, - "session.next.step.failed": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - draft.time.completed = event.data.timestamp - draft.finish = "error" - draft.error = event.data.error - }), - ) - } - }, - "session.next.text.started": () => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - draft.content.push({ - type: "text", - text: "", - }) - }), - ) - } - }, - "session.next.text.delta": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestText(draft) - if (match) match.text += event.data.delta - }), - ) - } - }, - "session.next.text.ended": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestText(draft) - if (match) match.text = event.data.text - }), - ) - } - }, - "session.next.tool.input.started": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - draft.content.push({ - type: "tool", - id: event.data.callID, - name: event.data.name, - time: { - created: event.data.timestamp, - }, - state: { - status: "pending", - input: "", - }, - }) - }), - ) - } - }, - "session.next.tool.input.delta": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestTool(draft, event.data.callID) - // oxlint-disable-next-line no-base-to-string -- event.delta is a Schema.String (runtime string) - if (match && match.state.status === "pending") match.state.input += event.data.delta - }), - ) - } - }, - "session.next.tool.input.ended": () => {}, - "session.next.tool.called": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestTool(draft, event.data.callID) - if (match) { - match.provider = event.data.provider - match.time.ran = event.data.timestamp - match.state = { - status: "running", - input: event.data.input, - structured: {}, - content: [], - } - } - }), - ) - } - }, - "session.next.tool.progress": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestTool(draft, event.data.callID) - if (match && match.state.status === "running") { - match.state.structured = event.data.structured - match.state.content = [...event.data.content] - } - }), - ) - } - }, - "session.next.tool.success": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestTool(draft, event.data.callID) - if (match && match.state.status === "running") { - match.provider = event.data.provider - match.time.completed = event.data.timestamp - match.state = { - status: "completed", - input: match.state.input, - structured: event.data.structured, - content: [...event.data.content], - } - } - }), - ) - } - }, - "session.next.tool.failed": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestTool(draft, event.data.callID) - if (match && match.state.status === "running") { - match.provider = event.data.provider - match.time.completed = event.data.timestamp - match.state = { - status: "error", - error: event.data.error, - input: match.state.input, - structured: match.state.structured, - content: match.state.content, - } - } - }), - ) - } - }, - "session.next.reasoning.started": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - draft.content.push({ - type: "reasoning", - id: event.data.reasoningID, - text: "", - }) - }), - ) - } - }, - "session.next.reasoning.delta": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestReasoning(draft, event.data.reasoningID) - if (match) match.text += event.data.delta - }), - ) - } - }, - "session.next.reasoning.ended": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestReasoning(draft, event.data.reasoningID) - if (match) match.text = event.data.text - }), - ) - } - }, - "session.next.retried": () => {}, - "session.next.compaction.started": (event) => { - adapter.appendMessage( - new SessionMessage.Compaction({ - id: event.id, - type: "compaction", - metadata: event.metadata, - reason: event.data.reason, - summary: "", - time: { created: event.data.timestamp }, - }), - ) - }, - "session.next.compaction.delta": (event) => { - const currentCompaction = adapter.getCurrentCompaction() - if (currentCompaction) { - adapter.updateCompaction( - produce(currentCompaction, (draft) => { - draft.summary += event.data.text - }), - ) - } - }, - "session.next.compaction.ended": (event) => { - const currentCompaction = adapter.getCurrentCompaction() - if (currentCompaction) { - adapter.updateCompaction( - produce(currentCompaction, (draft) => { - draft.summary = event.data.text - draft.include = event.data.include - }), - ) - } - }, - }) - - return adapter.finish() -} - -export * as SessionMessageUpdater from "./session-message-updater" diff --git a/packages/core/src/session-message.ts b/packages/core/src/session-message.ts deleted file mode 100644 index 73b6dd7da2b..00000000000 --- a/packages/core/src/session-message.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { Schema } from "effect" -import { Prompt } from "./session-prompt" -import { SessionEvent } from "./session-event" -import { EventV2 } from "./event" -import { ToolOutput } from "./tool-output" -import { V2Schema } from "./v2-schema" -import { ModelV2 } from "./model" - -export const ID = EventV2.ID -export type ID = Schema.Schema.Type - -const Base = { - id: ID, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - time: Schema.Struct({ - created: V2Schema.DateTimeUtcFromMillis, - }), -} - -export class AgentSwitched extends Schema.Class("Session.Message.AgentSwitched")({ - ...Base, - type: Schema.Literal("agent-switched"), - agent: SessionEvent.AgentSwitched.data.fields.agent, -}) {} - -export class ModelSwitched extends Schema.Class("Session.Message.ModelSwitched")({ - ...Base, - type: Schema.Literal("model-switched"), - model: ModelV2.Ref, -}) {} - -export class User extends Schema.Class("Session.Message.User")({ - ...Base, - text: Prompt.fields.text, - files: Prompt.fields.files, - agents: Prompt.fields.agents, - references: Prompt.fields.references, - type: Schema.Literal("user"), - time: Schema.Struct({ - created: V2Schema.DateTimeUtcFromMillis, - }), -}) {} - -export class Synthetic extends Schema.Class("Session.Message.Synthetic")({ - ...Base, - sessionID: SessionEvent.Synthetic.data.fields.sessionID, - text: SessionEvent.Synthetic.data.fields.text, - type: Schema.Literal("synthetic"), -}) {} - -export class Shell extends Schema.Class("Session.Message.Shell")({ - ...Base, - type: Schema.Literal("shell"), - callID: SessionEvent.Shell.Started.data.fields.callID, - command: SessionEvent.Shell.Started.data.fields.command, - output: Schema.String, - time: Schema.Struct({ - created: V2Schema.DateTimeUtcFromMillis, - completed: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional), - }), -}) {} - -export class ToolStatePending extends Schema.Class("Session.Message.ToolState.Pending")({ - status: Schema.Literal("pending"), - input: Schema.String, -}) {} - -export class ToolStateRunning extends Schema.Class("Session.Message.ToolState.Running")({ - status: Schema.Literal("running"), - input: Schema.Record(Schema.String, Schema.Unknown), - structured: ToolOutput.Structured, - content: ToolOutput.Content.pipe(Schema.Array), -}) {} - -export class ToolStateCompleted extends Schema.Class("Session.Message.ToolState.Completed")({ - status: Schema.Literal("completed"), - input: Schema.Record(Schema.String, Schema.Unknown), - attachments: SessionEvent.FileAttachment.pipe(Schema.Array, Schema.optional), - content: ToolOutput.Content.pipe(Schema.Array), - structured: ToolOutput.Structured, -}) {} - -export class ToolStateError extends Schema.Class("Session.Message.ToolState.Error")({ - status: Schema.Literal("error"), - input: Schema.Record(Schema.String, Schema.Unknown), - content: ToolOutput.Content.pipe(Schema.Array), - structured: ToolOutput.Structured, - error: SessionEvent.UnknownError, -}) {} - -export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe( - Schema.toTaggedUnion("status"), -) -export type ToolState = Schema.Schema.Type - -export class AssistantTool extends Schema.Class("Session.Message.Assistant.Tool")({ - type: Schema.Literal("tool"), - id: Schema.String, - name: Schema.String, - provider: Schema.Struct({ - executed: Schema.Boolean, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - }).pipe(Schema.optional), - state: ToolState, - time: Schema.Struct({ - created: V2Schema.DateTimeUtcFromMillis, - ran: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional), - completed: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional), - pruned: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional), - }), -}) {} - -export class AssistantText extends Schema.Class("Session.Message.Assistant.Text")({ - type: Schema.Literal("text"), - text: Schema.String, -}) {} - -export class AssistantReasoning extends Schema.Class("Session.Message.Assistant.Reasoning")({ - type: Schema.Literal("reasoning"), - id: Schema.String, - text: Schema.String, -}) {} - -export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]).pipe( - Schema.toTaggedUnion("type"), -) -export type AssistantContent = Schema.Schema.Type - -export class Assistant extends Schema.Class("Session.Message.Assistant")({ - ...Base, - type: Schema.Literal("assistant"), - agent: Schema.String, - model: SessionEvent.Step.Started.data.fields.model, - content: AssistantContent.pipe(Schema.Array), - snapshot: Schema.Struct({ - start: Schema.String.pipe(Schema.optional), - end: Schema.String.pipe(Schema.optional), - }).pipe(Schema.optional), - finish: Schema.String.pipe(Schema.optional), - cost: Schema.Finite.pipe(Schema.optional), - tokens: Schema.Struct({ - input: Schema.Finite, - output: Schema.Finite, - reasoning: Schema.Finite, - cache: Schema.Struct({ - read: Schema.Finite, - write: Schema.Finite, - }), - }).pipe(Schema.optional), - error: SessionEvent.Step.Failed.data.fields.error.pipe(Schema.optional), - time: Schema.Struct({ - created: V2Schema.DateTimeUtcFromMillis, - completed: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional), - }), -}) {} - -export class Compaction extends Schema.Class("Session.Message.Compaction")({ - type: Schema.Literal("compaction"), - reason: SessionEvent.Compaction.Started.data.fields.reason, - summary: Schema.String, - include: Schema.String.pipe(Schema.optional), - ...Base, -}) {} - -export const Message = Schema.Union([AgentSwitched, ModelSwitched, User, Synthetic, Shell, Assistant, Compaction]) - .pipe(Schema.toTaggedUnion("type")) - .annotate({ identifier: "Session.Message" }) - -export type Message = Schema.Schema.Type - -export type Type = Message["type"] - -export * as SessionMessage from "./session-message" diff --git a/packages/core/src/session-prompt.ts b/packages/core/src/session-prompt.ts deleted file mode 100644 index 14167fc2889..00000000000 --- a/packages/core/src/session-prompt.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as Schema from "effect/Schema" - -export class Source extends Schema.Class("Prompt.Source")({ - start: Schema.Finite, - end: Schema.Finite, - text: Schema.String, -}) {} - -export class FileAttachment extends Schema.Class("Prompt.FileAttachment")({ - uri: Schema.String, - mime: Schema.String, - name: Schema.String.pipe(Schema.optional), - description: Schema.String.pipe(Schema.optional), - source: Source.pipe(Schema.optional), -}) { - static create(input: FileAttachment) { - return new FileAttachment({ - uri: input.uri, - mime: input.mime, - name: input.name, - description: input.description, - source: input.source, - }) - } -} - -export class AgentAttachment extends Schema.Class("Prompt.AgentAttachment")({ - name: Schema.String, - source: Source.pipe(Schema.optional), -}) {} - -export class ReferenceAttachment extends Schema.Class("Prompt.ReferenceAttachment")({ - name: Schema.String, - kind: Schema.Literals(["local", "git", "invalid"]), - uri: Schema.String.pipe(Schema.optional), - repository: Schema.String.pipe(Schema.optional), - branch: Schema.String.pipe(Schema.optional), - target: Schema.String.pipe(Schema.optional), - targetUri: Schema.String.pipe(Schema.optional), - problem: Schema.String.pipe(Schema.optional), - source: Source.pipe(Schema.optional), -}) {} - -export class Prompt extends Schema.Class("Prompt")({ - text: Schema.String, - files: Schema.Array(FileAttachment).pipe(Schema.optional), - agents: Schema.Array(AgentAttachment).pipe(Schema.optional), - references: Schema.Array(ReferenceAttachment).pipe(Schema.optional), -}) {} diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 756531e3280..cb8bd1b4ce6 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -1,13 +1,494 @@ -export * as Session from "./session" +export * as SessionV2 from "./session" +export * from "./session/schema" -import { Schema } from "effect" -import { withStatics } from "./schema" -import { Identifier } from "./util/identifier" +import { DateTime, Effect, Layer, Schema, Context, Stream } from "effect" +import { ListAnchor } from "@opencode-ai/schema/session" +import { and, asc, desc, eq, gt, isNotNull, like, lt, or, type SQL } from "drizzle-orm" // kilocode_change +import { ProjectV2 } from "./project" +import { WorkspaceV2 } from "./workspace" +import { ModelV2 } from "./model" +import { Location } from "./location" +import { SessionMessage } from "./session/message" +import { Prompt } from "./session/prompt" +import { PromptInput } from "@opencode-ai/schema/prompt-input" +import { EventV2 } from "./event" +import { Database } from "./database/database" +import { SessionProjector } from "./session/projector" +import { SessionMessageTable, SessionTable } from "./session/sql" +import { SessionSchema } from "./session/schema" +import { AbsolutePath, PositiveInt, RelativePath } from "./schema" +import { AgentV2 } from "./agent" +import { SessionV1 } from "./v1/session" +import { InstallationVersion } from "./installation/version" +import { Slug } from "./util/slug" +import { ProjectTable } from "./project/sql" +import path from "path" +import { fromRow } from "./session/info" +import { SessionRunner } from "./session/runner/index" +import { SessionStore } from "./session/store" +import { SessionExecution } from "./session/execution" +import { makeGlobalNode } from "./effect/app-node" +import { LocationServiceMap } from "./location-service-map" +import { MessageDecodeError } from "./session/error" +import { SessionEvent } from "./session/event" +import { SessionInput } from "./session/input" +import { normalize } from "./kilocode/session-message" // kilocode_change +import { Snapshot } from "./snapshot" +import { SessionRevert } from "./session/revert" +import { Revert } from "@opencode-ai/schema/revert" +import { FSUtil } from "./fs-util" +import { SessionDurable, type SessionDurableEvent } from "@opencode-ai/schema/durable-event-manifest" // kilocode_change -export const ID = Schema.String.check(Schema.isStartsWith("ses")).pipe( - Schema.brand("SessionID"), - withStatics((schema) => ({ - descending: (id?: string) => schema.make(id ?? "ses_" + Identifier.descending()), - })), +export const RevertState = Revert.State +export type RevertState = Revert.State + +// get project -> project.locations +// +// get all sessions +// + +// - by project +// - by subpath +// - by workspace (home is special) + +export { ListAnchor } + +const ListInputBase = { + workspaceID: WorkspaceV2.ID.pipe(Schema.optional), + search: Schema.String.pipe(Schema.optional), + limit: PositiveInt.pipe(Schema.optional), + order: Schema.Literals(["asc", "desc"]).pipe(Schema.optional), + anchor: ListAnchor.pipe(Schema.optional), +} + +const ListDirectoryInput = Schema.Struct({ + ...ListInputBase, + directory: AbsolutePath, +}) + +const ListProjectInput = Schema.Struct({ + ...ListInputBase, + project: ProjectV2.ID, + subpath: RelativePath.pipe(Schema.optional), +}) + +const ListAllInput = Schema.Struct(ListInputBase) + +export const ListInput = Schema.Union([ListDirectoryInput, ListProjectInput, ListAllInput]) +export type ListInput = typeof ListInput.Type + +type CreateInput = { + id?: SessionSchema.ID + agent?: AgentV2.ID + model?: ModelV2.Ref + location: Location.Ref +} + +type CompactInput = { + sessionID: SessionSchema.ID + prompt?: Prompt +} + +export class NotFoundError extends Schema.TaggedErrorClass()("Session.NotFoundError", { + sessionID: SessionSchema.ID, +}) {} + +export class OperationUnavailableError extends Schema.TaggedErrorClass()( + "Session.OperationUnavailableError", + { + operation: Schema.Literals(["move", "shell", "skill", "switchAgent", "compact", "wait"]), + }, +) {} + +export { ContextSnapshotDecodeError, MessageDecodeError } from "./session/error" + +export class PromptConflictError extends Schema.TaggedErrorClass()("Session.PromptConflictError", { + sessionID: SessionSchema.ID, + messageID: SessionMessage.ID, +}) {} +export const MessageNotFoundError = SessionRevert.MessageNotFoundError +export type MessageNotFoundError = SessionRevert.MessageNotFoundError + +export type Error = NotFoundError | MessageDecodeError | OperationUnavailableError | PromptConflictError + +export interface Interface { + readonly list: (input?: ListInput) => Effect.Effect + readonly create: (input: CreateInput) => Effect.Effect + readonly get: (sessionID: SessionSchema.ID) => Effect.Effect + readonly messages: (input: { + sessionID: SessionSchema.ID + limit?: number + order?: "asc" | "desc" + cursor?: { + id: SessionMessage.ID + direction: "previous" | "next" + } + }) => Effect.Effect + readonly message: (input: { + sessionID: SessionSchema.ID + messageID: SessionMessage.ID + }) => Effect.Effect + readonly context: ( + sessionID: SessionSchema.ID, + ) => Effect.Effect + readonly events: (input: { + sessionID: SessionSchema.ID + after?: number + }) => Stream.Stream // kilocode_change - released durable event compatibility + readonly history: (input: { + sessionID: SessionSchema.ID + after?: number + limit: number + }) => Effect.Effect<{ events: ReadonlyArray; hasMore: boolean }, NotFoundError> // kilocode_change + readonly switchAgent: (input: { sessionID: SessionSchema.ID; agent: string }) => Effect.Effect + readonly switchModel: (input: { + sessionID: SessionSchema.ID + model: ModelV2.Ref + }) => Effect.Effect + readonly prompt: (input: { + id?: SessionMessage.ID + sessionID: SessionSchema.ID + prompt: PromptInput.Prompt + delivery?: SessionInput.Delivery + resume?: boolean + }) => Effect.Effect + readonly shell: (input: { + id?: EventV2.ID + sessionID: SessionSchema.ID + command: string + resume?: boolean + }) => Effect.Effect + readonly skill: (input: { + id?: EventV2.ID + sessionID: SessionSchema.ID + skill: string + resume?: boolean + }) => Effect.Effect + readonly compact: (input: CompactInput) => Effect.Effect + readonly wait: (id: SessionSchema.ID) => Effect.Effect + readonly active: Effect.Effect> + readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect + readonly interrupt: (sessionID: SessionSchema.ID) => Effect.Effect + readonly revert: { + readonly stage: (input: { + sessionID: SessionSchema.ID + messageID: SessionMessage.ID + files?: boolean + }) => Effect.Effect + readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect + readonly commit: (sessionID: SessionSchema.ID) => Effect.Effect + } +} + +export class Service extends Context.Service()("@opencode/v2/Session") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const database = yield* Database.Service + const db = database.db + const events = yield* EventV2.Service + const projects = yield* ProjectV2.Service + const execution = yield* SessionExecution.Service + const store = yield* SessionStore.Service + const locations = yield* LocationServiceMap.Service + const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message) + const isDurableSessionEvent = Schema.is(SessionDurable.schema) // kilocode_change - include released storage keys + const decode = (row: typeof SessionMessageTable.$inferSelect) => + decodeMessage(normalize({ ...row.data, id: row.id, type: row.type })).pipe( + // kilocode_change - normalize released tool content on paginated reads + Effect.mapError( + () => + new MessageDecodeError({ + sessionID: SessionSchema.ID.make(row.session_id), + messageID: SessionMessage.ID.make(row.id), + }), + ), + ) + + const result = Service.of({ + create: Effect.fn("V2Session.create")(function* (input) { + const sessionID = input.id ?? SessionSchema.ID.create() + const recorded = yield* store.get(sessionID) + if (recorded) return recorded + const project = yield* projects.resolve(input.location.directory) + yield* db + .insert(ProjectTable) + .values({ id: project.id, worktree: project.directory, vcs: project.vcs?.type, sandboxes: [] }) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie) + const now = Date.now() + const info = SessionV1.SessionInfo.make({ + id: sessionID, + slug: Slug.create(), + version: InstallationVersion, + projectID: project.id, + directory: input.location.directory, + path: path.relative(project.directory, input.location.directory).replaceAll("\\", "/"), + workspaceID: input.location.workspaceID ? WorkspaceV2.ID.make(input.location.workspaceID) : undefined, + title: `New session - ${new Date(now).toISOString()}`, + agent: input.agent, + model: input.model + ? { + id: ModelV2.ID.make(input.model.id), + providerID: input.model.providerID, + variant: input.model.variant, + } + : undefined, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { created: now, updated: now }, + }) + const projected = yield* events + .publish(SessionV1.Event.Created, { sessionID, info }, { location: input.location }) + .pipe( + Effect.as({ type: "created" } as const), + Effect.catchDefect((defect) => { + if (!(defect instanceof SessionProjector.SessionAlreadyProjected)) { + return Effect.die(defect) + } + // Concurrent creation lost the projection race. The existing Session identity wins. + return store + .get(sessionID) + .pipe( + Effect.flatMap((session) => + session ? Effect.succeed({ type: "existing", session } as const) : Effect.die(defect), + ), + ) + }), + ) + if (projected.type === "existing") return projected.session + // TODO: Restore recorded sessions onto replacement synchronized workspaces in a future API slice. + return yield* result.get(sessionID).pipe(Effect.orDie) + }), + get: Effect.fn("V2Session.get")(function* (sessionID) { + const session = yield* store.get(sessionID) + if (!session) return yield* new NotFoundError({ sessionID }) + return session + }), + list: Effect.fn("V2Session.list")(function* (input = {}) { + const direction = input.anchor?.direction ?? "next" + const requestedOrder = input.order ?? "desc" + const order = direction === "previous" ? (requestedOrder === "asc" ? "desc" : "asc") : requestedOrder + const sortColumn = SessionTable.time_created + const conditions: SQL[] = [] + if ("directory" in input) conditions.push(eq(SessionTable.directory, input.directory)) + if (input.workspaceID) conditions.push(eq(SessionTable.workspace_id, input.workspaceID)) + if ("project" in input) conditions.push(eq(SessionTable.project_id, input.project)) + if (input.search) conditions.push(like(SessionTable.title, `%${input.search}%`)) + if (input.anchor) { + conditions.push( + order === "asc" + ? or( + gt(sortColumn, input.anchor.time), + and(eq(sortColumn, input.anchor.time), gt(SessionTable.id, input.anchor.id)), + )! + : or( + lt(sortColumn, input.anchor.time), + and(eq(sortColumn, input.anchor.time), lt(SessionTable.id, input.anchor.id)), + )!, + ) + } + const query = db + .select() + .from(SessionTable) + .where(conditions.length > 0 ? and(...conditions) : undefined) + .orderBy( + order === "asc" ? asc(sortColumn) : desc(sortColumn), + order === "asc" ? asc(SessionTable.id) : desc(SessionTable.id), + ) + const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe( + Effect.orDie, + ) + return (direction === "previous" ? rows.toReversed() : rows).map((row) => fromRow(row)) + }), + messages: Effect.fn("V2Session.messages")(function* (input) { + yield* result.get(input.sessionID) + const direction = input.cursor?.direction ?? "next" + const requestedOrder = input.order ?? "desc" + const order = direction === "previous" ? (requestedOrder === "asc" ? "desc" : "asc") : requestedOrder + const anchor = input.cursor + ? yield* db + .select({ seq: SessionMessageTable.seq }) + .from(SessionMessageTable) + .where( + and( + eq(SessionMessageTable.session_id, input.sessionID), + eq(SessionMessageTable.id, input.cursor.id), + isNotNull(SessionMessageTable.seq), // kilocode_change + ), + ) + .get() + .pipe(Effect.orDie) + : undefined + const seq = anchor?.seq + if (input.cursor && seq == null) return [] + const boundary = + seq != null + ? order === "asc" + ? gt(SessionMessageTable.seq, seq) + : lt(SessionMessageTable.seq, seq) + : undefined + const where = boundary + ? and(eq(SessionMessageTable.session_id, input.sessionID), isNotNull(SessionMessageTable.seq), boundary) + : and(eq(SessionMessageTable.session_id, input.sessionID), isNotNull(SessionMessageTable.seq)) // kilocode_change + const query = db + .select() + .from(SessionMessageTable) + .where(where) + .orderBy(order === "asc" ? asc(SessionMessageTable.seq) : desc(SessionMessageTable.seq)) + const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe( + Effect.orDie, + ) + return yield* Effect.forEach(direction === "previous" ? rows.toReversed() : rows, decode) + }), + message: Effect.fn("V2Session.message")(function* (input) { + const stored = yield* store.message(input.messageID) + return stored?.sessionID === input.sessionID ? stored.message : undefined + }), + context: Effect.fn("V2Session.context")(function* (sessionID) { + yield* result.get(sessionID) + return yield* store.context(sessionID) + }), + events: (input) => + Stream.unwrap( + result + .get(input.sessionID) + .pipe(Effect.as(events.durable({ aggregateID: input.sessionID, after: input.after }))), + ).pipe(Stream.filter((event): event is SessionDurableEvent => isDurableSessionEvent(event))), // kilocode_change + history: Effect.fn("V2Session.history")(function* (input) { + yield* result.get(input.sessionID) + return yield* EventV2.readAggregate(db, { + ...input, + aggregateID: input.sessionID, + manifest: SessionDurable, + }) + }), + prompt: Effect.fn("V2Session.prompt")((input) => + Effect.uninterruptible( + Effect.gen(function* () { + yield* result.get(input.sessionID) + const prompt = resolvePrompt(input.prompt) + const messageID = input.id ?? SessionMessage.ID.create() + const delivery = input.delivery ?? "steer" + const expected = { sessionID: input.sessionID, messageID, prompt, delivery } + const admitted = yield* SessionInput.admit(db, events, { + id: messageID, + sessionID: input.sessionID, + prompt, + delivery, + }).pipe( + Effect.catchDefect((defect) => + defect instanceof SessionInput.LifecycleConflict + ? new PromptConflictError({ sessionID: input.sessionID, messageID }) + : Effect.die(defect), + ), + ) + if (!SessionInput.equivalent(admitted, expected)) + return yield* new PromptConflictError({ sessionID: input.sessionID, messageID }) + if (input.resume !== false) yield* execution.wake(admitted.sessionID) + return admitted + }), + ), + ), + shell: Effect.fn("V2Session.shell")(function* () { + return yield* new OperationUnavailableError({ operation: "shell" }) + }), + skill: Effect.fn("V2Session.skill")(function* () { + return yield* new OperationUnavailableError({ operation: "skill" }) + }), + switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) { + yield* result.get(input.sessionID) + yield* events.publish(SessionEvent.AgentSwitched, { + sessionID: input.sessionID, + messageID: SessionMessage.ID.create(), + timestamp: yield* DateTime.now, + agent: input.agent, + }) + }), + switchModel: Effect.fn("V2Session.switchModel")(function* (input) { + const session = yield* result.get(input.sessionID) + if ( + session.model?.providerID === input.model.providerID && + session.model.id === input.model.id && + (session.model.variant ?? "default") === (input.model.variant ?? "default") + ) + return + yield* events.publish(SessionEvent.ModelSwitched, { + sessionID: input.sessionID, + messageID: SessionMessage.ID.create(), + timestamp: yield* DateTime.now, + model: input.model, + }) + }), + compact: Effect.fn("V2Session.compact")(function* (input) { + yield* result.get(input.sessionID) + return yield* new OperationUnavailableError({ operation: "compact" }) + }), + wait: Effect.fn("V2Session.wait")(function* (sessionID) { + yield* result.get(sessionID) + return yield* new OperationUnavailableError({ operation: "wait" }) + }), + active: execution.active, + resume: Effect.fn("V2Session.resume")(function* (sessionID) { + yield* result.get(sessionID) + yield* execution.resume(sessionID) + }), + interrupt: Effect.fn("V2Session.interrupt")((sessionID) => + Effect.uninterruptible(execution.interrupt(sessionID)), + ), + revert: { + stage: Effect.fn("V2Session.revert.stage")(function* (input) { + const session = yield* result.get(input.sessionID) + return yield* SessionRevert.stage({ session, messageID: input.messageID, files: input.files }).pipe( + Effect.provideService(Database.Service, database), + Effect.provideService(EventV2.Service, events), + Effect.provide(locations.get(session.location)), + ) + }), + clear: Effect.fn("V2Session.revert.clear")(function* (sessionID) { + const session = yield* result.get(sessionID) + yield* SessionRevert.clear(session).pipe( + Effect.provideService(EventV2.Service, events), + Effect.provide(locations.get(session.location)), + ) + }), + commit: Effect.fn("V2Session.revert.commit")(function* (sessionID) { + const session = yield* result.get(sessionID) + yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events)) + }), + }, + }) + + return result + }), ) -export type ID = typeof ID.Type + +const resolvePrompt = (input: PromptInput.Prompt) => + Prompt.make({ + text: input.text, + agents: input.agents, + files: input.files?.map((file) => { + const dataMime = file.uri.match(/^data:([^;,]+)[;,]/i)?.[1] + const target = URL.canParse(file.uri) ? new URL(file.uri).pathname : (file.name ?? file.uri) + return { + ...file, + mime: dataMime ?? (target.endsWith("/") ? "application/x-directory" : FSUtil.mimeType(target)), + } + }), + }) + +export const node = makeGlobalNode({ + service: Service, + layer: layer.pipe(Layer.orDie), + deps: [ + Database.node, + EventV2.node, + ProjectV2.node, + SessionExecution.node, + SessionStore.node, + LocationServiceMap.node, + SessionProjector.node, + ], +}) diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts new file mode 100644 index 00000000000..0f9342e2581 --- /dev/null +++ b/packages/core/src/session/compaction.ts @@ -0,0 +1,242 @@ +export * as SessionCompaction from "./compaction" + +import { LLM, LLMError, LLMEvent, Message, type LLMRequest, type Model } from "@opencode-ai/llm" +import { DateTime, Effect, Stream } from "effect" +import type { Config } from "../config" +import type { EventV2 } from "../event" +import { SessionEvent } from "./event" +import { SessionMessage } from "./message" +import { SessionSchema } from "./schema" +import { Token } from "../util/token" + +const DEFAULT_BUFFER = 20_000 +const DEFAULT_KEEP_TOKENS = 8_000 +const TOOL_OUTPUT_MAX_CHARS = 2_000 +const SUMMARY_OUTPUT_TOKENS = 4_096 +const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside