diff --git a/.github/guidelines-check.yml b/.github/guidelines-check.yml
deleted file mode 100644
index 522e52a5b..000000000
--- a/.github/guidelines-check.yml
+++ /dev/null
@@ -1,57 +0,0 @@
-#
-# This file is intentionally in the wrong dir, will move and add later....
-#
-
-name: Guidelines Check
-
-on:
- # Disabled - uncomment to re-enable
- # pull_request_target:
- # types: [opened, synchronize]
-
-jobs:
- check-guidelines:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- pull-requests: write
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- fetch-depth: 1
-
- - name: Install opencode
- run: curl -fsSL https://opencode.ai/install | bash
-
- - name: Check PR guidelines compliance
- env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh pr review*": "deny", "*": "deny" } }'
- run: |
- opencode run -m anthropic/claude-sonnet-4-20250514 "A new pull request has been created: '${{ github.event.pull_request.title }}'
-
-
- ${{ github.event.pull_request.number }}
-
-
-
- ${{ github.event.pull_request.body }}
-
-
- Please check all the code changes in this pull request against the guidelines in AGENTS.md file in this repository. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do
-
- Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
-
- Command MUST be like this.
- ```
- gh api \
- --method POST \
- -H "Accept: application/vnd.github+json" \
- -H "X-GitHub-Api-Version: 2022-11-28" \
- /repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments \
- -f 'body=[summary of issue]' -f 'commit_id=${{ github.event.pull_request.head.sha }}' -f 'path=[path-to-file]' -F "line=[line]" -f 'side=RIGHT'
- ```
-
- Only create comments for actual violations. If the code follows all guidelines, don't run any gh commands."
diff --git a/.github/workflows/auto-label-tui.yml b/.github/workflows/auto-label-tui.yml
deleted file mode 100644
index 0f03bb68d..000000000
--- a/.github/workflows/auto-label-tui.yml
+++ /dev/null
@@ -1,63 +0,0 @@
-name: Auto-label TUI Issues
-
-on:
- issues:
- types: [opened]
-
-jobs:
- auto-label:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- issues: write
- steps:
- - name: Auto-label and assign issues
- uses: actions/github-script@v7
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- script: |
- const issue = context.payload.issue;
- const title = issue.title;
- const description = issue.body || '';
-
- // Check for "opencode web" keyword
- const webPattern = /(opencode web)/i;
- const isWebRelated = webPattern.test(title) || webPattern.test(description);
-
- // Check for version patterns like v1.0.x or 1.0.x
- const versionPattern = /[v]?1\.0\./i;
- const isVersionRelated = versionPattern.test(title) || versionPattern.test(description);
-
- // Check for "nix" keyword
- const nixPattern = /\bnix\b/i;
- const isNixRelated = nixPattern.test(title) || nixPattern.test(description);
-
- const labels = [];
-
- if (isWebRelated) {
- labels.push('web');
-
- // Assign to adamdotdevin
- await github.rest.issues.addAssignees({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: issue.number,
- assignees: ['adamdotdevin']
- });
- } else if (isVersionRelated) {
- // Only add opentui if NOT web-related
- labels.push('opentui');
- }
-
- if (isNixRelated) {
- labels.push('nix');
- }
-
- if (labels.length > 0) {
- await github.rest.issues.addLabels({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: issue.number,
- labels: labels
- });
- }
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 0cc239e31..25466a63e 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -11,7 +11,7 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
deploy:
- runs-on: ubuntu-latest
+ runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
diff --git a/.github/workflows/duplicate-issues.yml b/.github/workflows/duplicate-issues.yml
index 3b5c39193..5969d9d41 100644
--- a/.github/workflows/duplicate-issues.yml
+++ b/.github/workflows/duplicate-issues.yml
@@ -6,7 +6,7 @@ on:
jobs:
check-duplicates:
- runs-on: ubuntu-latest
+ runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
issues: write
@@ -55,4 +55,7 @@ jobs:
Feel free to ignore if none of these address your specific case.'
+ Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, please add a comment mentioning the pinned keybinds issue #4997:
+ 'For keybind-related issues, please also check our pinned keybinds documentation: #4997'
+
If no clear duplicates are found, do not comment."
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
deleted file mode 100644
index 718ab27c7..000000000
--- a/.github/workflows/format.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-name: format
-
-on:
- push:
- branches-ignore:
- - production
- pull_request:
- branches-ignore:
- - production
- workflow_dispatch:
-jobs:
- format:
- runs-on: ubuntu-latest
- permissions:
- contents: write
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- token: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Setup Bun
- uses: ./.github/actions/setup-bun
-
- - name: run
- run: |
- ./script/format.ts
- env:
- CI: true
diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml
new file mode 100644
index 000000000..326090f7a
--- /dev/null
+++ b/.github/workflows/generate.yml
@@ -0,0 +1,38 @@
+name: generate
+
+on:
+ push:
+ branches-ignore:
+ - production
+ pull_request:
+ branches-ignore:
+ - production
+ workflow_dispatch:
+
+jobs:
+ generate:
+ runs-on: blacksmith-4vcpu-ubuntu-2404
+ permissions:
+ contents: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
+ ref: ${{ github.event.pull_request.head.ref || github.ref_name }}
+
+ - name: Setup Bun
+ uses: ./.github/actions/setup-bun
+
+ - name: Generate SDK
+ run: |
+ bun ./packages/sdk/js/script/build.ts
+ (cd packages/opencode && bun dev generate > ../sdk/openapi.json)
+ bun x prettier --write packages/sdk/openapi.json
+
+ - name: Format
+ run: ./script/format.ts
+ env:
+ CI: true
+ PUSH_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }}
diff --git a/.github/workflows/notify-discord.yml b/.github/workflows/notify-discord.yml
index fde896410..d12cc7d73 100644
--- a/.github/workflows/notify-discord.yml
+++ b/.github/workflows/notify-discord.yml
@@ -6,7 +6,7 @@ on:
jobs:
notify:
- runs-on: ubuntu-latest
+ runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Send nicely-formatted embed to Discord
uses: SethCohen/github-releases-to-discord@v1
diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml
index ebf146395..37210191e 100644
--- a/.github/workflows/opencode.yml
+++ b/.github/workflows/opencode.yml
@@ -13,7 +13,7 @@ jobs:
startsWith(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, ' /opencode') ||
startsWith(github.event.comment.body, '/opencode')
- runs-on: ubuntu-latest
+ runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
id-token: write
contents: read
@@ -29,5 +29,6 @@ jobs:
uses: sst/opencode/github@latest
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
+ OPENCODE_PERMISSION: '{"bash": "deny"}'
with:
- model: opencode/claude-haiku-4-5
+ model: opencode/claude-opus-4-5
diff --git a/.github/workflows/publish-github-action.yml b/.github/workflows/publish-github-action.yml
index cfd14148c..d2789373a 100644
--- a/.github/workflows/publish-github-action.yml
+++ b/.github/workflows/publish-github-action.yml
@@ -14,7 +14,7 @@ permissions:
jobs:
publish:
- runs-on: ubuntu-latest
+ runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
with:
diff --git a/.github/workflows/publish-vscode.yml b/.github/workflows/publish-vscode.yml
index 48357813f..f49a10578 100644
--- a/.github/workflows/publish-vscode.yml
+++ b/.github/workflows/publish-vscode.yml
@@ -13,7 +13,7 @@ permissions:
jobs:
publish:
- runs-on: ubuntu-latest
+ runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
with:
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 710d7c0a1..ebfc5059b 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -2,11 +2,15 @@ name: publish
run-name: "${{ format('release {0}', inputs.bump) }}"
on:
+ push:
+ branches:
+ - dev
+ - snapshot-*
workflow_dispatch:
inputs:
bump:
description: "Bump major, minor, or patch"
- required: true
+ required: false
type: choice
options:
- major
@@ -17,15 +21,17 @@ on:
required: false
type: string
-concurrency: ${{ github.workflow }}-${{ github.ref }}
+concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }}
permissions:
+ id-token: write
contents: write
packages: write
jobs:
publish:
- runs-on: ubuntu-latest
+ runs-on: blacksmith-4vcpu-ubuntu-2404
+ if: github.repository == 'sst/opencode'
steps:
- uses: actions/checkout@v3
with:
@@ -33,20 +39,13 @@ jobs:
- run: git fetch --force --tags
- - uses: actions/setup-go@v5
- with:
- go-version: ">=1.24.0"
- cache: true
- cache-dependency-path: go.sum
-
- uses: ./.github/actions/setup-bun
- - name: Install makepkg
+ - name: Setup SSH for AUR
+ if: inputs.bump || inputs.version
run: |
sudo apt-get update
sudo apt-get install -y pacman-package-manager
- - name: Setup SSH for AUR
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
@@ -55,11 +54,8 @@ jobs:
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
- name: Install OpenCode
- run: curl -fsSL https://opencode.ai/install | bash
-
- - name: Setup npm auth
- run: |
- echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
+ if: inputs.bump || inputs.version
+ run: bun i -g opencode-ai@1.0.143
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
@@ -68,9 +64,97 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "24"
+ registry-url: "https://registry.npmjs.org"
+
- name: Publish
+ id: publish
+ run: ./script/publish.ts
+ env:
+ OPENCODE_BUMP: ${{ inputs.bump }}
+ OPENCODE_VERSION: ${{ inputs.version }}
+ OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
+ AUR_KEY: ${{ secrets.AUR_KEY }}
+ GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
+ NPM_CONFIG_PROVENANCE: false
+ outputs:
+ releaseId: ${{ steps.publish.outputs.releaseId }}
+ tagName: ${{ steps.publish.outputs.tagName }}
+
+ publish-tauri:
+ needs: publish
+ continue-on-error: true
+ strategy:
+ fail-fast: false
+ matrix:
+ settings:
+ - host: macos-latest
+ target: x86_64-apple-darwin
+ - host: macos-latest
+ target: aarch64-apple-darwin
+ - host: blacksmith-4vcpu-windows-2025
+ target: x86_64-pc-windows-msvc
+ - host: blacksmith-4vcpu-ubuntu-2404
+ target: x86_64-unknown-linux-gnu
+ runs-on: ${{ matrix.settings.host }}
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ ref: ${{ needs.publish.outputs.tagName }}
+
+ - uses: apple-actions/import-codesign-certs@v2
+ if: ${{ runner.os == 'macOS' }}
+ with:
+ keychain: build
+ p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
+ p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
+
+ - name: Verify Certificate
+ if: ${{ runner.os == 'macOS' }}
run: |
- ./script/publish.ts
+ CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application")
+ CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
+ echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
+ echo "Certificate imported."
+
+ - name: Setup Apple API Key
+ if: ${{ runner.os == 'macOS' }}
+ run: |
+ echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
+
+ - run: git fetch --force --tags
+
+ - uses: ./.github/actions/setup-bun
+
+ - name: install dependencies (ubuntu only)
+ if: contains(matrix.settings.host, 'ubuntu')
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
+
+ - name: install Rust stable
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: ${{ matrix.settings.target }}
+
+ - uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: packages/tauri/src-tauri
+ shared-key: ${{ matrix.settings.target }}
+
+ - name: Prepare
+ run: |
+ cd packages/tauri
+ bun ./scripts/prepare.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
@@ -79,3 +163,37 @@ jobs:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
+ RUST_TARGET: ${{ matrix.settings.target }}
+ GH_TOKEN: ${{ github.token }}
+
+ # Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
+ - name: Install tauri-cli from portable appimage branch
+ if: contains(matrix.settings.host, 'ubuntu')
+ run: |
+ cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage --force
+ echo "Installed tauri-cli version:"
+ cargo tauri --version
+
+ - name: Build and upload artifacts
+ timeout-minutes: 20
+ uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true
+ TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
+ TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
+ APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
+ APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
+ APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
+ APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
+ APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
+ APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
+ with:
+ projectPath: packages/tauri
+ uploadWorkflowArtifacts: true
+ tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
+ args: --target ${{ matrix.settings.target }}
+ updaterJsonPreferNsis: true
+ releaseId: ${{ needs.publish.outputs.releaseId }}
+ tagName: ${{ needs.publish.outputs.tagName }}
+ releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml
new file mode 100644
index 000000000..d974e2a76
--- /dev/null
+++ b/.github/workflows/review.yml
@@ -0,0 +1,79 @@
+name: Guidelines Check
+
+on:
+ issue_comment:
+ types: [created]
+
+jobs:
+ check-guidelines:
+ if: |
+ github.event.issue.pull_request &&
+ startsWith(github.event.comment.body, '/review') &&
+ contains(fromJson('["OWNER","MEMBER"]'), github.event.comment.author_association)
+ runs-on: blacksmith-4vcpu-ubuntu-2404
+ permissions:
+ contents: read
+ pull-requests: write
+ steps:
+ - name: Get PR number
+ id: pr-number
+ run: |
+ if [ "${{ github.event_name }}" = "pull_request_target" ]; then
+ echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
+ else
+ echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Install opencode
+ run: curl -fsSL https://opencode.ai/install | bash
+
+ - name: Get PR details
+ id: pr-details
+ run: |
+ gh api /repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }} > pr_data.json
+ echo "title=$(jq -r .title pr_data.json)" >> $GITHUB_OUTPUT
+ echo "sha=$(jq -r .head.sha pr_data.json)" >> $GITHUB_OUTPUT
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Check PR guidelines compliance
+ env:
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh pr review*": "deny", "*": "deny" } }'
+ PR_TITLE: ${{ steps.pr-details.outputs.title }}
+ run: |
+ PR_BODY=$(jq -r .body pr_data.json)
+ opencode run -m anthropic/claude-opus-4-5 "A new pull request has been created: '${PR_TITLE}'
+
+
+ ${{ steps.pr-number.outputs.number }}
+
+
+
+ $PR_BODY
+
+
+ Please check all the code changes in this pull request against the style guide, also look for any bugs if they exist. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do
+
+ When critiquing code against the style guide, be sure that the code is ACTUALLY in violation, don't complain about else statements if they already use early returns there. You may complain about excessive nesting though, regardless of else statement usage.
+ When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simpliest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts)
+
+ Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
+
+ Command MUST be like this.
+ \`\`\`
+ gh api \
+ --method POST \
+ -H \"Accept: application/vnd.github+json\" \
+ -H \"X-GitHub-Api-Version: 2022-11-28\" \
+ /repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }}/comments \
+ -f 'body=[summary of issue]' -f 'commit_id=${{ steps.pr-details.outputs.sha }}' -f 'path=[path-to-file]' -F \"line=[line]\" -f 'side=RIGHT'
+ \`\`\`
+
+ Only create comments for actual violations. If the code follows all guidelines, comment on the issue using gh cli: 'lgtm' AND NOTHING ELSE!!!!."
diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml
deleted file mode 100644
index bd98de164..000000000
--- a/.github/workflows/snapshot.yml
+++ /dev/null
@@ -1,37 +0,0 @@
-name: snapshot
-
-on:
- workflow_dispatch:
- push:
- branches:
- - dev
- - test-bedrock
- - v0
- - otui-diffs
-
-concurrency: ${{ github.workflow }}-${{ github.ref }}
-
-jobs:
- publish:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
- with:
- fetch-depth: 0
-
- - run: git fetch --force --tags
-
- - uses: actions/setup-go@v5
- with:
- go-version: ">=1.24.0"
- cache: true
- cache-dependency-path: go.sum
-
- - uses: ./.github/actions/setup-bun
-
- - name: Publish
- run: |
- ./script/publish.ts
- env:
- GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
- NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.github/workflows/stats.yml b/.github/workflows/stats.yml
index 01ddf314a..97e924517 100644
--- a/.github/workflows/stats.yml
+++ b/.github/workflows/stats.yml
@@ -7,7 +7,7 @@ on:
jobs:
stats:
- runs-on: ubuntu-latest
+ runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
diff --git a/.github/workflows/sync-zed-extension.yml b/.github/workflows/sync-zed-extension.yml
index c9dea7bb7..a504582c3 100644
--- a/.github/workflows/sync-zed-extension.yml
+++ b/.github/workflows/sync-zed-extension.yml
@@ -2,13 +2,13 @@ name: "sync-zed-extension"
on:
workflow_dispatch:
- release:
- types: [published]
+ # release:
+ # types: [published]
jobs:
zed:
name: Release Zed Extension
- runs-on: ubuntu-latest
+ runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
with:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ccce2aa25..ac1a24fd5 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -10,7 +10,7 @@ on:
workflow_dispatch:
jobs:
test:
- runs-on: ubuntu-latest
+ runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -28,9 +28,3 @@ jobs:
bun turbo test
env:
CI: true
-
- - name: Check SDK is up to date
- run: |
- bun ./packages/sdk/js/script/build.ts
- git diff --exit-code packages/sdk/js/src/gen packages/sdk/js/dist
- continue-on-error: false
diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml
new file mode 100644
index 000000000..9cc76973d
--- /dev/null
+++ b/.github/workflows/triage.yml
@@ -0,0 +1,34 @@
+name: Issue Triage
+
+on:
+ issues:
+ types: [opened]
+
+jobs:
+ triage:
+ runs-on: blacksmith-4vcpu-ubuntu-2404
+ permissions:
+ contents: read
+ issues: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Install opencode
+ run: curl -fsSL https://opencode.ai/install | bash
+
+ - name: Triage issue
+ env:
+ OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ ISSUE_NUMBER: ${{ github.event.issue.number }}
+ ISSUE_TITLE: ${{ github.event.issue.title }}
+ ISSUE_BODY: ${{ github.event.issue.body }}
+ run: |
+ opencode run --agent triage "The following issue was just opened, triage it:
+
+ Title: $ISSUE_TITLE
+
+ $ISSUE_BODY"
diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml
index 8943e10be..011e23f5f 100644
--- a/.github/workflows/typecheck.yml
+++ b/.github/workflows/typecheck.yml
@@ -7,7 +7,7 @@ on:
jobs:
typecheck:
- runs-on: ubuntu-latest
+ runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@v4
diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml
index 1f1ca0e80..d2c60b08f 100644
--- a/.github/workflows/update-nix-hashes.yml
+++ b/.github/workflows/update-nix-hashes.yml
@@ -19,7 +19,7 @@ on:
jobs:
update:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
- runs-on: ubuntu-latest
+ runs-on: blacksmith-4vcpu-ubuntu-2404
env:
SYSTEM: x86_64-linux
diff --git a/.gitignore b/.gitignore
index 62cb12717..3d4f9095a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,10 +6,10 @@ node_modules
.idea
.vscode
*~
-openapi.json
playground
tmp
dist
+ts-dist
.turbo
**/.serena
.serena/
@@ -18,3 +18,4 @@ refs
Session.vim
opencode.json
a.out
+target
diff --git a/.opencode/agent/triage.md b/.opencode/agent/triage.md
new file mode 100644
index 000000000..6a020532f
--- /dev/null
+++ b/.opencode/agent/triage.md
@@ -0,0 +1,12 @@
+---
+mode: primary
+hidden: true
+model: opencode/gpt-5-nano
+tools:
+ "*": false
+ "github-triage": true
+---
+
+You are a triage agent responsible for triaging github issues.
+
+Use your github-triage tool to triage issues.
diff --git a/.opencode/bun.lock b/.opencode/bun.lock
new file mode 100644
index 000000000..f152a1646
--- /dev/null
+++ b/.opencode/bun.lock
@@ -0,0 +1,49 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 0,
+ "workspaces": {
+ "": {
+ "dependencies": {
+ "@octokit/rest": "^22.0.1",
+ "@opencode-ai/plugin": "0.0.0-dev-202512160508",
+ },
+ },
+ },
+ "packages": {
+ "@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
+
+ "@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/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="],
+
+ "@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="],
+
+ "@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
+
+ "@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@14.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw=="],
+
+ "@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="],
+
+ "@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@17.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw=="],
+
+ "@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="],
+
+ "@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="],
+
+ "@octokit/rest": ["@octokit/rest@22.0.1", "", { "dependencies": { "@octokit/core": "^7.0.6", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^17.0.0" } }, "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw=="],
+
+ "@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
+
+ "@opencode-ai/plugin": ["@opencode-ai/plugin@0.0.0-dev-202512160508", "", { "dependencies": { "@opencode-ai/sdk": "0.0.0-dev-202512160508", "zod": "4.1.8" } }, "sha512-GLnvMQhEWRHG9E84FyyQKPKi54bGUkytXPfZYjwNy9W6djw8zAW/kpeYPrdIJHPdTHk4OjIHEwoB1SXZzGaLFQ=="],
+
+ "@opencode-ai/sdk": ["@opencode-ai/sdk@0.0.0-dev-202512160508", "", {}, "sha512-ICpZ1bX528yQKqYGGyUJQMu3RY0F1pQ6RCoTJ4ESLiYmcXUY1EldgIidiwPA+A/zpEXLu2lPwPZ1LYn/bX6aFA=="],
+
+ "before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
+
+ "fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="],
+
+ "universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
+
+ "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
+ }
+}
diff --git a/.opencode/command/commit.md b/.opencode/command/commit.md
index 2e3d759b6..c318ed54b 100644
--- a/.opencode/command/commit.md
+++ b/.opencode/command/commit.md
@@ -1,5 +1,6 @@
---
-description: Git commit and push
+description: git commit and push
+model: opencode/glm-4.6
---
commit and push
@@ -21,3 +22,6 @@ WHAT was done.
do not do generic messages like "improved agent experience" be very specific
about what user facing changes were made
+
+if there are changes do a git pull --rebase
+if there are conflicts DO NOT FIX THEM. notify me and I will fix them
diff --git a/.opencode/command/hello.md b/.opencode/command/hello.md
deleted file mode 100644
index 003bc4a76..000000000
--- a/.opencode/command/hello.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-description: hello world iaosd ioasjdoiasjd oisadjoisajd osiajd oisaj dosaij dsoajsajdaijdoisa jdoias jdoias jdoia jois jo jdois jdoias jdoias j djoasdj
----
-
-hey there $ARGUMENTS
-
-!`ls`
-check out @README.md
diff --git a/.opencode/command/issues.md b/.opencode/command/issues.md
index 793dce651..20ac4c180 100644
--- a/.opencode/command/issues.md
+++ b/.opencode/command/issues.md
@@ -1,5 +1,5 @@
---
-description: "Find issue(s) on github"
+description: "find issue(s) on github"
model: opencode/claude-haiku-4-5
---
diff --git a/.opencode/command/rmslop.md b/.opencode/command/rmslop.md
new file mode 100644
index 000000000..02c9fc084
--- /dev/null
+++ b/.opencode/command/rmslop.md
@@ -0,0 +1,15 @@
+---
+description: Remove AI code slop
+---
+
+Check the diff against dev, and remove all AI generated slop introduced in this branch.
+
+This includes:
+
+- Extra comments that a human wouldn't add or is inconsistent with the rest of the file
+- Extra defensive checks or try/catch blocks that are abnormal for that area of the codebase (especially if called by trusted / validated codepaths)
+- Casts to any to get around type issues
+- Any other style that is inconsistent with the file
+- Unnecessary emoji usage
+
+Report at the end with only a 1-3 sentence summary of what you changed
diff --git a/.opencode/command/spellcheck.md b/.opencode/command/spellcheck.md
index afa1970b7..0abf23c4f 100644
--- a/.opencode/command/spellcheck.md
+++ b/.opencode/command/spellcheck.md
@@ -1,5 +1,5 @@
---
-description: Spellcheck all markdown file changes
+description: spellcheck all markdown file changes
---
Look at all the unstaged changes to markdown (.md, .mdx) files, pull out the lines that have changed, and check for spelling and grammar errors.
diff --git a/.opencode/env.d.ts b/.opencode/env.d.ts
new file mode 100644
index 000000000..f2b13a934
--- /dev/null
+++ b/.opencode/env.d.ts
@@ -0,0 +1,4 @@
+declare module "*.txt" {
+ const content: string
+ export default content
+}
diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc
index ce4a6658b..cbcbb0c65 100644
--- a/.opencode/opencode.jsonc
+++ b/.opencode/opencode.jsonc
@@ -1,27 +1,17 @@
{
"$schema": "https://opencode.ai/config.json",
- "plugin": ["opencode-openai-codex-auth"],
+ // "plugin": ["opencode-openai-codex-auth"],
// "enterprise": {
// "url": "https://enterprise.dev.opencode.ai",
// },
+ "instructions": ["STYLE_GUIDE.md"],
"provider": {
"opencode": {
- "options": {
- // "baseURL": "http://localhost:8080",
- },
+ "options": {},
},
},
- "mcp": {
- "exa": {
- "type": "remote",
- "url": "https://mcp.exa.ai/mcp",
- },
- "morph": {
- "type": "local",
- "command": ["bunx", "@morphllm/morphmcp"],
- "environment": {
- "ENABLED_TOOLS": "warp_grep",
- },
- },
+ "mcp": {},
+ "tools": {
+ "github-triage": false,
},
}
diff --git a/.opencode/package.json b/.opencode/package.json
new file mode 100644
index 000000000..88fd8e891
--- /dev/null
+++ b/.opencode/package.json
@@ -0,0 +1,6 @@
+{
+ "dependencies": {
+ "@octokit/rest": "^22.0.1",
+ "@opencode-ai/plugin": "0.0.0-dev-202512160508"
+ }
+}
diff --git a/.opencode/tool/github-triage.ts b/.opencode/tool/github-triage.ts
new file mode 100644
index 000000000..f0437e623
--- /dev/null
+++ b/.opencode/tool/github-triage.ts
@@ -0,0 +1,51 @@
+import { Octokit } from "@octokit/rest"
+import { tool } from "@opencode-ai/plugin"
+import DESCRIPTION from "./github-triage.txt"
+
+function getIssueNumber(): number {
+ const issue = parseInt(process.env.ISSUE_NUMBER ?? "", 10)
+ if (!issue) throw new Error("ISSUE_NUMBER env var not set")
+ return issue
+}
+
+export default tool({
+ description: DESCRIPTION,
+ args: {
+ assignee: tool.schema
+ .enum(["thdxr", "adamdotdevin", "rekram1-node", "fwang", "jayair", "kommander"])
+ .describe("The username of the assignee")
+ .default("rekram1-node"),
+ labels: tool.schema
+ .array(tool.schema.enum(["nix", "opentui", "perf", "web", "zen", "docs"]))
+ .describe("The labels(s) to add to the issue")
+ .optional(),
+ },
+ async execute(args) {
+ const issue = getIssueNumber()
+ const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })
+ const owner = "sst"
+ const repo = "opencode"
+
+ const results: string[] = []
+
+ await octokit.rest.issues.addAssignees({
+ owner,
+ repo,
+ issue_number: issue,
+ assignees: [args.assignee],
+ })
+ results.push(`Assigned @${args.assignee} to issue #${issue}`)
+
+ if (args.labels && args.labels.length > 0) {
+ await octokit.rest.issues.addLabels({
+ owner,
+ repo,
+ issue_number: issue,
+ labels: args.labels,
+ })
+ results.push(`Added labels: ${args.labels.join(", ")}`)
+ }
+
+ return results.join("\n")
+ },
+})
diff --git a/.opencode/tool/github-triage.txt b/.opencode/tool/github-triage.txt
new file mode 100644
index 000000000..14844a19f
--- /dev/null
+++ b/.opencode/tool/github-triage.txt
@@ -0,0 +1,80 @@
+Use this tool to assign and/or label a Github issue.
+
+You can assign the following users:
+- thdxr
+- adamdotdevin
+- fwang
+- jayair
+- kommander
+- rekram1-node
+
+
+You can use the following labels:
+- nix
+- opentui
+- perf
+- web
+- zen
+- docs
+
+Always try to assign an issue, if in doubt, assign rekram1-node to it.
+
+## Breakdown of responsibilities:
+
+### thdxr
+
+Dax is responsible for managing core parts of the application, for large feature requests, api changes, or things that require significant changes to the codebase assign him.
+
+This relates to OpenCode server primarily but has overlap with just about anything
+
+### adamdotdevin
+
+Adam is responsible for managing the Desktop/Web app. If there is an issue relating to the desktop app or `opencode web` command. Assign him.
+
+
+### fwang
+
+Frank is responsible for managing Zen, if you see complaints about OpenCode Zen, maybe it's the dashboard, the model quality, billing issues, etc. Assign him to the issue.
+
+### jayair
+
+Jay is responsible for documentation. If there is an issue relating to documentation assign him.
+
+### kommander
+
+Sebastian is responsible for managing an OpenTUI (a library for building terminal user interfaces). OpenCode's TUI is built with OpenTUI. If there are issues about:
+- random characters on screen
+- keybinds not working on different terminals
+- general terminal stuff
+Then assign the issue to Him.
+
+### rekram1-node
+
+Assign Aiden to an issue as a catch all, if you can't assign anyone else. Most of the time this will be bugs/polish things.
+If no one else makes sense to assign, assign rekram1-node to it.
+
+## Breakdown of Labels:
+
+### nix
+
+Any issue that mentions nix, or nixos should have a nix label
+
+### opentui
+
+Anything relating to the TUI itself should have an opentui label
+
+### perf
+
+Anything related to slow performance, high ram, high cpu usage, or any other performance related issue should have a perf label
+
+### web
+
+Anything related to `opencode web` or the desktop app should have a web label. Never add this label for anything terminal/tui related
+
+### zen
+
+Anything related to OpenCode Zen, billing, or model quality from Zen should have a zen label
+
+### docs
+
+Anything related to the documentation should have a docs label
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 000000000..aa3a7ce23
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1 @@
+sst-env.d.ts
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
index 22b305dac..5a95fc509 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,16 +1,3 @@
-## IMPORTANT
-
-- Try to keep things in one function unless composable or reusable
-- DO NOT do unnecessary destructuring of variables
-- DO NOT use `else` statements unless necessary
-- DO NOT use `try`/`catch` if it can be avoided
-- AVOID `try`/`catch` where possible
-- AVOID `else` statements
-- AVOID using `any` type
-- AVOID `let` statements
-- PREFER single word variable names where possible
-- Use as many bun apis as possible like Bun.file()
-
## Debugging
- To test opencode in the `packages/opencode` directory you can run `bun dev`
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2fc5737d7..6a24995e8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -42,6 +42,8 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
> [!NOTE]
> After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk.
+Please try to follow the [style guide](./STYLE_GUIDE.md)
+
### Setting up a Debugger
Bun debugging is currently rough around the edges. We hope this guide helps you get set up and avoid some pain points.
diff --git a/README.md b/README.md
index 799cf00a2..eb0295c9c 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
-The AI coding agent built for the terminal.
+The open source AI coding agent.
@@ -30,7 +30,7 @@ scoop bucket add extras; scoop install extras/opencode # Windows
choco install opencode # Windows
brew install opencode # macOS and Linux
paru -S opencode-bin # Arch Linux
-mise use --pin -g ubi:sst/opencode # Any OS
+mise use -g ubi:sst/opencode # Any OS
nix run nixpkgs#opencode # or github:sst/opencode for latest dev branch
```
diff --git a/STATS.md b/STATS.md
index d9f348f91..9d60266d2 100644
--- a/STATS.md
+++ b/STATS.md
@@ -1,157 +1,173 @@
# Download Stats
-| Date | GitHub Downloads | npm Downloads | Total |
-| ---------- | ----------------- | ----------------- | ------------------- |
-| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
-| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
-| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
-| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
-| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
-| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
-| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
-| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
-| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
-| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
-| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
-| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
-| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
-| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
-| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
-| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
-| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
-| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
-| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
-| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
-| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
-| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
-| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
-| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
-| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
-| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
-| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
-| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
-| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
-| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
-| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
-| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
-| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
-| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
-| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
-| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
-| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
-| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
-| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
-| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
-| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
-| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
-| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
-| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
-| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
-| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
-| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
-| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
-| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
-| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
-| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
-| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
-| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
-| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
-| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
-| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
-| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
-| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
-| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
-| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
-| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
-| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
-| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
-| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
-| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
-| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
-| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
-| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
-| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
-| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
-| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
-| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
-| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
-| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
-| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
-| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
-| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
-| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
-| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
-| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
-| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
-| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
-| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
-| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
-| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
-| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
-| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
-| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
-| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
-| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
-| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
-| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
-| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
-| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
-| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
-| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
-| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
-| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
-| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
-| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
-| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
-| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
-| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
-| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
-| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
-| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
-| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
-| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
-| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
-| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
-| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
-| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
-| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
-| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
-| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
-| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
-| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
-| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
-| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
-| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
-| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
-| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
-| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
-| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
-| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
-| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
-| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
-| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
-| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
-| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
-| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
-| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
-| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
-| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
-| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
-| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
-| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
-| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
-| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
-| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
-| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
-| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
-| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
-| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
-| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
-| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
-| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
-| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
-| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
-| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
-| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
-| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
-| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
+| Date | GitHub Downloads | npm Downloads | Total |
+| ---------- | ------------------- | ------------------- | ------------------- |
+| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
+| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
+| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
+| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
+| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
+| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
+| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
+| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
+| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
+| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
+| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
+| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
+| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
+| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
+| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
+| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
+| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
+| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
+| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
+| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
+| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
+| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
+| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
+| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
+| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
+| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
+| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
+| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
+| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
+| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
+| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
+| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
+| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
+| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
+| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
+| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
+| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
+| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
+| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
+| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
+| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
+| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
+| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
+| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
+| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
+| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
+| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
+| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
+| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
+| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
+| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
+| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
+| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
+| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
+| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
+| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
+| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
+| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
+| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
+| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
+| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
+| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
+| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
+| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
+| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
+| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
+| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
+| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
+| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
+| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
+| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
+| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
+| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
+| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
+| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
+| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
+| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
+| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
+| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
+| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
+| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
+| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
+| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
+| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
+| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
+| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
+| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
+| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
+| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
+| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
+| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
+| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
+| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
+| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
+| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
+| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
+| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
+| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
+| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
+| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
+| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
+| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
+| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
+| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
+| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
+| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
+| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
+| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
+| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
+| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
+| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
+| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
+| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
+| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
+| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
+| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
+| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
+| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
+| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
+| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
+| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
+| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
+| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
+| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
+| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
+| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
+| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
+| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
+| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
+| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
+| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
+| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
+| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
+| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
+| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
+| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
+| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
+| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
+| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
+| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
+| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
+| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
+| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
+| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
+| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
+| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
+| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
+| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
+| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
+| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
+| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
+| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
+| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
+| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
+| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
+| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
+| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
+| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
+| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
+| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
+| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
+| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
+| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
+| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
+| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
+| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
+| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
+| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
+| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md
new file mode 100644
index 000000000..164f69bd4
--- /dev/null
+++ b/STYLE_GUIDE.md
@@ -0,0 +1,12 @@
+## Style Guide
+
+- Try to keep things in one function unless composable or reusable
+- DO NOT do unnecessary destructuring of variables
+- DO NOT use `else` statements unless necessary
+- DO NOT use `try`/`catch` if it can be avoided
+- AVOID `try`/`catch` where possible
+- AVOID `else` statements
+- AVOID using `any` type
+- AVOID `let` statements
+- PREFER single word variable names where possible
+- Use as many bun apis as possible like Bun.file()
diff --git a/bun.lock b/bun.lock
index 4f49abe70..90450d399 100644
--- a/bun.lock
+++ b/bun.lock
@@ -20,7 +20,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
- "version": "1.0.120",
+ "version": "1.0.162",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -48,7 +48,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
- "version": "1.0.120",
+ "version": "1.0.162",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -75,7 +75,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
- "version": "1.0.120",
+ "version": "1.0.162",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -99,7 +99,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
- "version": "1.0.120",
+ "version": "1.0.162",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -123,7 +123,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
- "version": "1.0.120",
+ "version": "1.0.162",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -131,15 +131,19 @@
"@opencode-ai/util": "workspace:*",
"@shikijs/transformers": "3.9.2",
"@solid-primitives/active-element": "2.1.3",
+ "@solid-primitives/audio": "1.4.2",
"@solid-primitives/event-bus": "1.1.2",
+ "@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3",
"@solid-primitives/scroll": "2.1.3",
"@solid-primitives/storage": "4.3.3",
+ "@solid-primitives/websocket": "1.3.1",
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
"@thisbeyond/solid-dnd": "0.7.5",
"diff": "catalog:",
"fuzzysort": "catalog:",
+ "ghostty-web": "0.3.0",
"luxon": "catalog:",
"marked": "16.2.0",
"marked-shiki": "1.2.1",
@@ -151,8 +155,10 @@
"virtua": "catalog:",
},
"devDependencies": {
+ "@happy-dom/global-registrator": "20.0.11",
"@tailwindcss/vite": "catalog:",
"@tsconfig/bun": "1.0.9",
+ "@types/bun": "catalog:",
"@types/luxon": "catalog:",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
@@ -164,17 +170,18 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
- "version": "1.0.120",
+ "version": "1.0.162",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
- "@pierre/precision-diffs": "catalog:",
+ "@pierre/diffs": "catalog:",
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
"@solidjs/start": "catalog:",
"aws4fetch": "^1.0.20",
"hono": "catalog:",
"hono-openapi": "catalog:",
+ "js-base64": "3.7.7",
"luxon": "catalog:",
"nitro": "3.0.1-alpha.1",
"solid-js": "catalog:",
@@ -192,10 +199,10 @@
},
"packages/function": {
"name": "@opencode-ai/function",
- "version": "1.0.120",
+ "version": "1.0.162",
"dependencies": {
"@octokit/auth-app": "8.0.1",
- "@octokit/rest": "22.0.0",
+ "@octokit/rest": "catalog:",
"hono": "catalog:",
"jose": "6.0.11",
},
@@ -208,7 +215,7 @@
},
"packages/opencode": {
"name": "opencode",
- "version": "1.0.120",
+ "version": "1.0.162",
"bin": {
"opencode": "./bin/opencode",
},
@@ -224,26 +231,29 @@
"@ai-sdk/mcp": "0.0.8",
"@ai-sdk/openai": "2.0.71",
"@ai-sdk/openai-compatible": "1.0.27",
+ "@ai-sdk/provider": "2.0.0",
+ "@ai-sdk/provider-utils": "3.0.18",
"@clack/prompts": "1.0.0-alpha.1",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
"@modelcontextprotocol/sdk": "1.15.1",
"@octokit/graphql": "9.0.2",
- "@octokit/rest": "22.0.0",
+ "@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
- "@openrouter/ai-sdk-provider": "1.2.8",
- "@opentui/core": "0.1.51",
- "@opentui/solid": "0.1.51",
+ "@openrouter/ai-sdk-provider": "1.5.2",
+ "@opentui/core": "0.0.0-20251211-4403a69a",
+ "@opentui/solid": "0.0.0-20251211-4403a69a",
"@parcel/watcher": "2.5.1",
- "@pierre/precision-diffs": "catalog:",
+ "@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
"@standard-schema/spec": "1.0.0",
"@zip.js/zip.js": "2.7.62",
"ai": "catalog:",
+ "bun-pty": "0.4.2",
"chokidar": "4.0.3",
"clipboardy": "4.0.0",
"decimal.js": "10.5.0",
@@ -278,7 +288,9 @@
"@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-x64": "2.5.1",
"@standard-schema/spec": "1.0.0",
"@tsconfig/bun": "catalog:",
@@ -295,7 +307,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
- "version": "1.0.120",
+ "version": "1.0.162",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -315,9 +327,9 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
- "version": "1.0.120",
+ "version": "1.0.162",
"devDependencies": {
- "@hey-api/openapi-ts": "0.81.0",
+ "@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
@@ -326,7 +338,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
- "version": "1.0.120",
+ "version": "1.0.162",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -339,26 +351,40 @@
},
"packages/tauri": {
"name": "@opencode-ai/tauri",
- "version": "1.0.120",
+ "version": "1.0.162",
"dependencies": {
+ "@opencode-ai/desktop": "workspace:*",
"@tauri-apps/api": "^2",
+ "@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-opener": "^2",
+ "@tauri-apps/plugin-os": "~2",
+ "@tauri-apps/plugin-process": "~2",
+ "@tauri-apps/plugin-shell": "~2",
+ "@tauri-apps/plugin-store": "~2",
+ "@tauri-apps/plugin-updater": "~2",
+ "@tauri-apps/plugin-window-state": "~2",
+ "solid-js": "catalog:",
},
"devDependencies": {
+ "@actions/artifact": "4.0.0",
"@tauri-apps/cli": "^2",
+ "@types/bun": "catalog:",
+ "@typescript/native-preview": "catalog:",
"typescript": "~5.6.2",
- "vite": "^6.0.3",
+ "vite": "catalog:",
},
},
"packages/ui": {
"name": "@opencode-ai/ui",
- "version": "1.0.120",
+ "version": "1.0.162",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
- "@pierre/precision-diffs": "catalog:",
+ "@pierre/diffs": "catalog:",
"@shikijs/transformers": "3.9.2",
+ "@solid-primitives/bounds": "0.1.3",
+ "@solid-primitives/resize-observer": "2.1.3",
"@solidjs/meta": "catalog:",
"@typescript/native-preview": "catalog:",
"fuzzysort": "catalog:",
@@ -375,6 +401,7 @@
"@tailwindcss/vite": "catalog:",
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
+ "@types/luxon": "catalog:",
"tailwindcss": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
@@ -384,17 +411,18 @@
},
"packages/util": {
"name": "@opencode-ai/util",
- "version": "1.0.120",
+ "version": "1.0.162",
"dependencies": {
"zod": "catalog:",
},
"devDependencies": {
+ "@types/bun": "catalog:",
"typescript": "catalog:",
},
},
"packages/web": {
"name": "@opencode-ai/web",
- "version": "1.0.120",
+ "version": "1.0.162",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -431,6 +459,9 @@
"web-tree-sitter",
"tree-sitter-bash",
],
+ "patchedDependencies": {
+ "ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch",
+ },
"overrides": {
"@types/bun": "catalog:",
"@types/node": "catalog:",
@@ -439,23 +470,24 @@
"@cloudflare/workers-types": "4.20251008.0",
"@hono/zod-validator": "0.4.2",
"@kobalte/core": "0.13.11",
+ "@octokit/rest": "22.0.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
- "@pierre/precision-diffs": "0.5.7",
+ "@pierre/diffs": "1.0.0-beta.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",
"@tsconfig/bun": "1.0.9",
"@tsconfig/node22": "22.0.2",
- "@types/bun": "1.3.3",
+ "@types/bun": "1.3.4",
"@types/luxon": "3.7.1",
"@types/node": "22.13.9",
- "@typescript/native-preview": "7.0.0-dev.20251014.1",
+ "@typescript/native-preview": "7.0.0-dev.20251207.1",
"ai": "5.0.97",
"diff": "8.0.2",
"fuzzysort": "3.1.0",
- "hono": "4.7.10",
- "hono-openapi": "1.1.1",
+ "hono": "4.10.7",
+ "hono-openapi": "1.1.2",
"luxon": "3.6.1",
"remeda": "2.26.0",
"solid-js": "1.9.10",
@@ -469,6 +501,8 @@
"zod": "4.1.8",
},
"packages": {
+ "@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="],
+
"@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=="],
@@ -503,7 +537,7 @@
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
- "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
+ "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
@@ -611,6 +645,34 @@
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.1", "", {}, "sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww=="],
+ "@azure/abort-controller": ["@azure/abort-controller@1.1.0", "", { "dependencies": { "tslib": "^2.2.0" } }, "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw=="],
+
+ "@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.10.1", "", { "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-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w=="],
+
+ "@azure/core-http": ["@azure/core-http@3.0.5", "", { "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-tracing": "1.0.0-preview.13", "@azure/core-util": "^1.1.1", "@azure/logger": "^1.0.0", "@types/node-fetch": "^2.5.0", "@types/tunnel": "^0.0.3", "form-data": "^4.0.0", "node-fetch": "^2.6.7", "process": "^0.11.10", "tslib": "^2.2.0", "tunnel": "^0.0.6", "uuid": "^8.3.0", "xml2js": "^0.5.0" } }, "sha512-T8r2q/c3DxNu6mEJfPuJtptUVqwchxzjj32gKcnMi06rdiVONS9rar7kT9T2Am+XvER7uOzpsP79WsqNbdgdWg=="],
+
+ "@azure/core-http-compat": ["@azure/core-http-compat@2.3.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g=="],
+
+ "@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-paging": ["@azure/core-paging@1.6.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA=="],
+
+ "@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.22.2", "", { "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.0", "tslib": "^2.6.2" } }, "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg=="],
+
+ "@azure/core-tracing": ["@azure/core-tracing@1.0.0-preview.13", "", { "dependencies": { "@opentelemetry/api": "^1.0.1", "tslib": "^2.2.0" } }, "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ=="],
+
+ "@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-xml": ["@azure/core-xml@1.5.0", "", { "dependencies": { "fast-xml-parser": "^5.0.7", "tslib": "^2.8.1" } }, "sha512-D/sdlJBMJfx7gqoj66PKVmhDDaU6TKA49ptcolxdas29X7AfvLTmfAGLjAcIMBK7UZ2o4lygHIqVckOlQU3xWw=="],
+
+ "@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/storage-blob": ["@azure/storage-blob@12.29.1", "", { "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.1.1", "events": "^3.0.0", "tslib": "^2.8.1" } }, "sha512-7ktyY0rfTM0vo7HvtK6E3UvYnI9qfd6Oz6z/+92VhGRveWng3kJwMKeUpqmW/NmwcDNbxHpSlldG+vsUnRFnBg=="],
+
+ "@azure/storage-common": ["@azure/storage-common@12.1.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.1.4", "events": "^3.3.0", "tslib": "^2.8.1" } }, "sha512-eIOH1pqFwI6UmVNnDQvmFeSg0XppuzDLFeUNO/Xht7ODAzRLgGDh7h550pSxoA+lPDxBl1+D2m/KG3jWzCUjTg=="],
+
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
@@ -673,6 +735,10 @@
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
+ "@bufbuild/protobuf": ["@bufbuild/protobuf@2.10.1", "", {}, "sha512-ckS3+vyJb5qGpEYv/s1OebUHDi/xSNtfgw1wqKZo7MR9F2z+qXr0q5XagafAG/9O0QPVIUfST0smluYSTpYFkg=="],
+
+ "@bufbuild/protoplugin": ["@bufbuild/protoplugin@2.10.1", "", { "dependencies": { "@bufbuild/protobuf": "2.10.1", "@typescript/vfs": "^1.6.2", "typescript": "5.4.5" } }, "sha512-imB8dKEjrOnG5+XqVS+CeYn924WGLU/g3wogKhk11XtX9y9NJ7432OS6h24asuBbLrQcPdEZ6QkfM7KeOCeeyQ=="],
+
"@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="],
"@clack/core": ["@clack/core@1.0.0-alpha.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rFbCU83JnN7l3W1nfgCqqme4ZZvTTgsiKQ6FM0l+r0P+o2eJpExcocBUWUIwnDzL76Aca9VhUdWmB2MbUv+Qyg=="],
@@ -797,9 +863,13 @@
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
- "@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.0.6", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0", "lodash": "^4.17.21" } }, "sha512-yktiFZoWPtEW8QKS65eqKwA5MTKp88CyiL8q72WynrBs/73SAaxlSWlA2zW/DZlywZ5hX1OYzrCC0wFdvO9c2w=="],
+ "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
- "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.81.0", "", { "dependencies": { "@hey-api/json-schema-ref-parser": "1.0.6", "ansi-colors": "4.1.3", "c12": "2.0.1", "color-support": "1.1.3", "commander": "13.0.0", "handlebars": "4.7.8", "js-yaml": "4.1.0", "open": "10.1.2", "semver": "7.7.2" }, "peerDependencies": { "typescript": "^5.5.3" }, "bin": { "openapi-ts": "bin/index.cjs" } }, "sha512-PoJukNBkUfHOoMDpN33bBETX49TUhy7Hu8Sa0jslOvFndvZ5VjQr4Nl/Dzjb9LG1Lp5HjybyTJMA6a1zYk/q6A=="],
+ "@hey-api/codegen-core": ["@hey-api/codegen-core@0.3.3", "", { "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-vArVDtrvdzFewu1hnjUm4jX1NBITlSCeO81EdWq676MxQbyxsGcDPAgohaSA+Wvr4HjPSvsg2/1s2zYxUtXebg=="],
+
+ "@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.2", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA=="],
+
+ "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.88.1", "", { "dependencies": { "@hey-api/codegen-core": "^0.3.3", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.2", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-x/nDTupOnV9VuSeNIiJpgIpc915GHduhyseJeMTnI0JMsXaObmpa0rgPr3ASVEYMLgpvqozIEG1RTOOnal6zLQ=="],
"@hono/standard-validator": ["@hono/standard-validator@0.1.5", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="],
@@ -1033,6 +1103,8 @@
"@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=="],
+ "@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@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="],
"@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="],
@@ -1079,27 +1151,27 @@
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
- "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.2.8", "", { "dependencies": { "@openrouter/sdk": "^0.1.8" }, "peerDependencies": { "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" } }, "sha512-pQT8AzZBKg9f4bkt4doF486ZlhK0XjKkevrLkiqYgfh1Jplovieu28nK4Y+xy3sF18/mxjqh9/2y6jh01qzLrA=="],
+ "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.5.2", "", { "dependencies": { "@openrouter/sdk": "^0.1.27" }, "peerDependencies": { "@toon-format/toon": "^2.0.0", "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" }, "optionalPeers": ["@toon-format/toon"] }, "sha512-3Th0vmJ9pjnwcPc2H1f59Mb0LFvwaREZAScfOQIpUxAHjZ7ZawVKDP27qgsteZPmMYqccNMy4r4Y3kgUnNcKAg=="],
"@openrouter/sdk": ["@openrouter/sdk@0.1.27", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RH//L10bSmc81q25zAZudiI4kNkLgxF2E+WU42vghp3N6TEvZ6F0jK7uT3tOxkEn91gzmMw9YVmDENy7SJsajQ=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
- "@opentui/core": ["@opentui/core@0.1.51", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.51", "@opentui/core-darwin-x64": "0.1.51", "@opentui/core-linux-arm64": "0.1.51", "@opentui/core-linux-x64": "0.1.51", "@opentui/core-win32-arm64": "0.1.51", "@opentui/core-win32-x64": "0.1.51", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-9w9vg2nYC4eTKdh5en7WpBB44Nrib3uMtcPNXr2JxftjzDXU5Qmcv3vbKbxHqgzgk7FYm2Z9OFAOk7innbnplA=="],
+ "@opentui/core": ["@opentui/core@0.0.0-20251211-4403a69a", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20251211-4403a69a", "@opentui/core-darwin-x64": "0.0.0-20251211-4403a69a", "@opentui/core-linux-arm64": "0.0.0-20251211-4403a69a", "@opentui/core-linux-x64": "0.0.0-20251211-4403a69a", "@opentui/core-win32-arm64": "0.0.0-20251211-4403a69a", "@opentui/core-win32-x64": "0.0.0-20251211-4403a69a", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-wTZKcokyU9yiDqyC0Pvf9eRSdT73s4Ynerkit/z8Af++tynqrTlZHZCXK3o42Ff7itCSILmijcTU94n69aEypA=="],
- "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.51", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5EicEs5JQiMmr3rzKdGfHnsRXJ7cv/pxz0/C2Xcg+jKMSUmUvS7LE3Mi3HBY05GjZyB/EZ3bYyHqXB4nanYTNg=="],
+ "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20251211-4403a69a", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VAYjTa+Eiauy8gETXadD8y0PE6ppnKasDK1X354VoexZiWFR3r7rkL+TfDfk7whhqXDYyT44JDT1QmCAhVXRzQ=="],
- "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.51", "", { "os": "darwin", "cpu": "x64" }, "sha512-mG9WlKdv0yWCIldFEAzgnm1NCyOtfiVG8zVZLiGaLhsL7Jii+f4RpOmlvFlb2sSSTxjC3HxRzWPC7WBSzk4txA=="],
+ "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20251211-4403a69a", "", { "os": "darwin", "cpu": "x64" }, "sha512-n9oVMpsojlILj1soORZzZ2Mjh8Zl73ZNcY7ot0iRmOjBDccrjDTsqKfxoGjKNd/xJSphLeu1LYGlcI5O5OczWQ=="],
- "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.51", "", { "os": "linux", "cpu": "arm64" }, "sha512-CNUx8nvkKRCsoLg/z7W8tD0hBFEUE9yEpqgyACc/ODdaaRLrUPQkBgUYWM/XGV9OnoqnpqLSKf95+DoUaTZ0lA=="],
+ "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20251211-4403a69a", "", { "os": "linux", "cpu": "arm64" }, "sha512-vf4eUjPMI4ANitK4MpTGenZFddKgQD/K21aN6cZjusnH3mTEJAoIR7GbNtMdz3qclU43ajpzTID9sAwhshwdVQ=="],
- "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.51", "", { "os": "linux", "cpu": "x64" }, "sha512-GpZ8vqX2dPyvBKvCRkBJp5x7zsmbixNCsCfzSVG9VIAVyT6qhylT2wrTOTGJLzDfc12ugdeL/1WUIR80YvwARA=="],
+ "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20251211-4403a69a", "", { "os": "linux", "cpu": "x64" }, "sha512-61635Up0YvVJ8gZ2eMiL1c8OfA+U6wAzT++LoaurNjbmsUAlKHws6MZdqTLw7aspJJVGsRFbA6d1Y+gXFxbDrQ=="],
- "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.51", "", { "os": "win32", "cpu": "arm64" }, "sha512-eIc13B9dmoJ2x0EEsd4JWKCLzgVPBv+6Gn6vbjaSL9T/+14XRKMQuEdnQmU6+/KruAumfK+qSXlFdmjhFKrDOA=="],
+ "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20251211-4403a69a", "", { "os": "win32", "cpu": "arm64" }, "sha512-3lUddTJGKZ6uU388eU79MY//IEbgGENCITetDrrRp7v9L1AxMntE1ihf6HniziwBvKKJcsUfqLiJWcq0WPZw2w=="],
- "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.51", "", { "os": "win32", "cpu": "x64" }, "sha512-ytpWgA3oNLehI5s9pxLAtg5kVO7Npq6onGPTZUikM5LlH71bX3Bm4PP8qLkD0AG9zDiO7ndp93ZFuT9yGqKPiQ=="],
+ "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20251211-4403a69a", "", { "os": "win32", "cpu": "x64" }, "sha512-Xwc1gqYsn8UZNTzNKkigZozAhBNBGbfX2B/I/aSbyqL0h8+XIInOodI0urzJWc0B6aEv/IDiT6Rm3coXFikLIg=="],
- "@opentui/solid": ["@opentui/solid@0.1.51", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.51", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-CBohbgFjUVG2P6/iAN52OCGaK0s5Wc2VWKyNrs7Fd9mFDMrC/IfrGAeaDeJXdQ8T0YhZ0PiUVijzFILCi2yOUw=="],
+ "@opentui/solid": ["@opentui/solid@0.0.0-20251211-4403a69a", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20251211-4403a69a", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-vuLppAdd1Qgaqhie3q2TuEr+8udjT4d8uVg5arvCe1AUDVs19I8kvadVCfzGUVmtXgFIOEakbiv6AxDq5v9Zig=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -1215,7 +1287,7 @@
"@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="],
- "@pierre/precision-diffs": ["@pierre/precision-diffs@0.5.7", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/transformers": "3.15.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.15.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-Y+e4kJ9pT2I4NS5fE39KdoiXtwMkVPRvrwLM6O2IqO7PDCRWLBS7CYxcSgSyngEndccUll2krx66I2QnfO0Ovg=="],
+ "@pierre/diffs": ["@pierre/diffs@1.0.0-beta.3", "", { "dependencies": { "@shikijs/core": "3.19.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/transformers": "3.19.0", "diff": "8.0.2", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.19.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-W3dFWdFOBZ9OskGSOgN16aci8dsUyAavCxz3ZvbbVLTb2qRzMZ7H90qdfON13/N2l1HTyh84lkrCs1/sDvnRjQ=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
@@ -1227,6 +1299,14 @@
"@poppinss/exception": ["@poppinss/exception@1.2.2", "", {}, "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg=="],
+ "@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=="],
+
"@radix-ui/colors": ["@radix-ui/colors@1.0.1", "", {}, "sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw=="],
@@ -1475,6 +1555,10 @@
"@solid-primitives/active-element": ["@solid-primitives/active-element@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9t5K4aR2naVDj950XU8OjnLgOg94a8k5wr6JNOPK+N5ESLsJDq42c1ZP8UKpewi1R+wplMMxiM6OPKRzbxJY7A=="],
+ "@solid-primitives/audio": ["@solid-primitives/audio@1.4.2", "", { "dependencies": { "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UMD3ORQfI5Ky8yuKPxidDiEazsjv/dsoiKK5yZxLnsgaeNR1Aym3/77h/qT1jBYeXUgj4DX6t7NMpFUSVr14OQ=="],
+
+ "@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.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="],
@@ -1503,6 +1587,8 @@
"@solid-primitives/utils": ["@solid-primitives/utils@6.3.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ=="],
+ "@solid-primitives/websocket": ["@solid-primitives/websocket@1.3.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-F06tA2FKa5VsnS4E4WEc3jHpsJfXRlMTGOtolugTzCqV3JmJTyvk9UVg1oz6PgGHKGi1CQ91OP8iW34myyJgaQ=="],
+
"@solidjs/meta": ["@solidjs/meta@0.29.4", "", { "peerDependencies": { "solid-js": ">=1.8.4" } }, "sha512-zdIWBGpR9zGx1p1bzIPqF5Gs+Ks/BH8R6fWhmUa/dcK1L2rUC8BAcZJzNRYBQv74kScf1TSOs0EY//Vd/I0V8g=="],
"@solidjs/router": ["@solidjs/router@0.15.4", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ=="],
@@ -1581,8 +1667,22 @@
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-EdYd4c9wGvtPB95kqtEyY+bUR+k4kRw3IA30mAQ1jPH6z57AftT8q84qwv0RDp6kkEqOBKxeInKfqi4BESYuqg=="],
+ "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ=="],
+
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="],
+ "@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="],
+
+ "@tauri-apps/plugin-process": ["@tauri-apps/plugin-process@2.3.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA=="],
+
+ "@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ=="],
+
+ "@tauri-apps/plugin-store": ["@tauri-apps/plugin-store@2.4.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ckGSEzZ5Ii4Hf2D5x25Oqnm2Zf9MfDWAzR+volY0z/OOBz6aucPKEY0F649JvQ0Vupku6UJo7ugpGRDOFOunkA=="],
+
+ "@tauri-apps/plugin-updater": ["@tauri-apps/plugin-updater@2.9.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg=="],
+
+ "@tauri-apps/plugin-window-state": ["@tauri-apps/plugin-window-state@2.4.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-OuvdrzyY8Q5Dbzpj+GcrnV1iCeoZbcFdzMjanZMMcAEUNy/6PH5pxZPXpaZLOR7whlzXiuzx0L9EKZbH7zpdRw=="],
+
"@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
@@ -1605,7 +1705,7 @@
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
- "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
+ "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
@@ -1677,31 +1777,39 @@
"@types/tsscmp": ["@types/tsscmp@1.0.2", "", {}, "sha512-cy7BRSU8GYYgxjcx0Py+8lo5MthuDhlyu076KUcYzVNXL23luYgRHkMG2fIFEc6neckeh/ntP82mw+U4QjZq+g=="],
+ "@types/tunnel": ["@types/tunnel@0.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA=="],
+
"@types/turndown": ["@types/turndown@5.0.5", "", {}, "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
+ "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
+
"@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="],
"@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="],
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
- "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20251014.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251014.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251014.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20251014.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251014.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20251014.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251014.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20251014.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-IqmX5CYCBqXbfL+HKlcQAMaDlfJ0Z8OhUxvADFV2TENnzSYI4CuhvKxwOB2wFSLXufVsgtAlf3Fjwn24KmMyPQ=="],
+ "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20251207.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20251207.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20251207.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-4QcRnzB0pi9rS0AOvg8kWbmuwHv5X7B2EXHbgcms9+56hsZ8SZrZjNgBJb2rUIodJ4kU5mrkj/xlTTT4r9VcpQ=="],
- "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20251014.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7rQoLlerWnwnvrM56hP4rdEbo4xDE4zr7cch+EzgENq/tbXYereGq1fmnR83UNglb1Eyy53OvJZ3O2csYBa2vg=="],
+ "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20251207.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-waWJnuuvkXh4WdpbTjYf7pyahJzx0ycesV2BylyHrE9OxU9FSKcD/cRLQYvbq3YcBSdF7sZwRLDBer7qTeLsYA=="],
- "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20251014.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-SF29o9NFRGDM23Jz0nVO4/yS78GQ81rtOemmCVNXuJotoY4bP3npGDyEmfkZQHZgDOXogs2OWy3t7NUJ235ANQ=="],
+ "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20251207.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-3bkD9QuIjxETtp6J1l5X2oKgudJ8z+8fwUq0izCjK1JrIs2vW1aQnbzxhynErSyHWH7URGhHHzcsXHbikckAsg=="],
- "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20251014.1", "", { "os": "linux", "cpu": "arm" }, "sha512-o5cu7h+BBAp6V4qxYY5RWuaYouN3j+MGFLrrUtvvNj4XKM+kbq5qwsgVRsmJZ1LfUvHmzyQs86vt9djAWedzjQ=="],
+ "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20251207.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OjrZBq8XJkB7uCQvT1AZ1FPsp+lT0cHxY5SisE+ZTAU6V0IHAZMwJ7J/mnwlGsBcCKRLBT+lX3hgEuOTSwHr9w=="],
- "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20251014.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+YWbW/JF4uggEUBr+vflqI5i7bL4Z3XInCOyUO1qQEY7VmfDCsPEzIwGi37O1mixfxw9Qj8LQsptCkU+fqKwGw=="],
+ "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20251207.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qhp06OObkwy5B+PlAhAmq+Ls3GVt4LHAovrTRcpLB3Mk3yJ0h9DnIQwPQiayp16TdvTsGHI3jdIX4MGm5L/ghA=="],
- "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20251014.1", "", { "os": "linux", "cpu": "x64" }, "sha512-3LC4tgcgi6zWJWBUpBNXOGSY3yISJrQezSP/T+v+mQRApkdoIpTSHIyQAhgaagcs3MOQRaqiIPaLOVrdHXdU6A=="],
+ "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20251207.1", "", { "os": "linux", "cpu": "x64" }, "sha512-fPRw0zfTBeVmrkgi5Le+sSwoeAz6pIdvcsa1OYZcrspueS9hn3qSC5bLEc5yX4NJP1vItadBqyGLUQ7u8FJjow=="],
- "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20251014.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-P0D4UEXwzFZh3pHexe2Ky1tW/HjY/HxTBTIajz2ViDCNPw7uDSEsXSB4H9TTiFJw8gVdTUFbsoAQp1MteTeORA=="],
+ "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20251207.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-KxY1i+HxeSFfzZ+HVsKwMGBM79laTRZv1ibFqHu22CEsfSPDt4yiV1QFis8Nw7OBXswNqJG/UGqY47VP8FeTvw=="],
- "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251014.1", "", { "os": "win32", "cpu": "x64" }, "sha512-fi53g2ihH7tkQLlz8hZGAb2V+3aNZpcxrZ530CQ4xcWwAqssEj0EaZJX0VLEtIQBar1ttGVK9Pz/wJU9sYyVzg=="],
+ "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251207.1", "", { "os": "win32", "cpu": "x64" }, "sha512-5l51HlXjX7lXwo65DEl1IaCFLjmkMtL6K3NrSEamPNeNTtTQwZRa3pQ9V65dCglnnCQ0M3+VF1RqzC7FU0iDKg=="],
+
+ "@typescript/vfs": ["@typescript/vfs@1.6.2", "", { "dependencies": { "debug": "^4.1.1" }, "peerDependencies": { "typescript": "*" } }, "sha512-hoBwJwcbKHmvd2QVebiytN1aELvpk9B74B4L1mFm/XT1Q/VOYAWl2vQ9AWRFtQq8zmz6enTpfTV8WRc4ATjW/g=="],
+
+ "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.2", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
@@ -1763,6 +1871,10 @@
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
+ "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=="],
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
@@ -1793,6 +1905,8 @@
"astro-expressive-code": ["astro-expressive-code@0.41.3", "", { "dependencies": { "rehype-expressive-code": "^0.41.3" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0" } }, "sha512-u+zHMqo/QNLE2eqYRCrK3+XMlKakv33Bzuz+56V1gs8H0y6TZ0hIi3VNbIxeTn51NLn+mJfUV/A0kMNfE4rANw=="],
+ "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
+
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
@@ -1853,6 +1967,8 @@
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
+ "binary": ["binary@0.3.0", "", { "dependencies": { "buffers": "~0.1.1", "chainsaw": "~0.1.0" } }, "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg=="],
+
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
@@ -1867,6 +1983,8 @@
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
+ "bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="],
+
"bowser": ["bowser@2.12.1", "", {}, "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw=="],
"boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="],
@@ -1881,13 +1999,19 @@
"buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="],
+ "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-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
+ "buffers": ["buffers@0.1.1", "", {}, "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="],
+
"bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
- "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
+ "bun-pty": ["bun-pty@0.4.2", "", {}, "sha512-sHImDz6pJDsHAroYpC9ouKVgOyqZ7FP3N+stX5IdMddHve3rf9LIZBDomQcXrACQ7sQDNuwZQHG8BKR7w8krkQ=="],
+
+ "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
@@ -1903,7 +2027,7 @@
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
- "c12": ["c12@2.0.1", "", { "dependencies": { "chokidar": "^4.0.1", "confbox": "^0.1.7", "defu": "^6.1.4", "dotenv": "^16.4.5", "giget": "^1.2.3", "jiti": "^2.3.0", "mlly": "^1.7.1", "ohash": "^1.1.4", "pathe": "^1.1.2", "perfect-debounce": "^1.0.0", "pkg-types": "^1.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A=="],
+ "c12": ["c12@3.3.2", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-QkikB2X5voO1okL3QsES0N690Sn/K9WokXqUsDQsWy5SnYb+psYQFGA10iy1bZHj3fjISKsI67Q90gruvWWM3A=="],
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
@@ -1923,6 +2047,8 @@
"chai": ["chai@6.2.1", "", {}, "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg=="],
+ "chainsaw": ["chainsaw@0.1.0", "", { "dependencies": { "traverse": ">=0.3.0 <0.4" } }, "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ=="],
+
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
@@ -1983,13 +2109,15 @@
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
- "commander": ["commander@13.0.0", "", {}, "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ=="],
+ "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],
"common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="],
+ "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=="],
+
"condense-newlines": ["condense-newlines@0.2.1", "", { "dependencies": { "extend-shallow": "^2.0.1", "is-whitespace": "^0.3.0", "kind-of": "^3.0.2" } }, "sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg=="],
- "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
+ "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
"config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="],
@@ -2007,8 +2135,14 @@
"cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="],
+ "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
+
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
+ "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=="],
@@ -2107,7 +2241,7 @@
"dot-prop": ["dot-prop@8.0.2", "", { "dependencies": { "type-fest": "^3.8.0" } }, "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ=="],
- "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
+ "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
"drizzle-kit": ["drizzle-kit@0.30.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l6dMSE100u7sDaTbLczibrQZjA35jLsHNqIV+jmhNVO3O8jzM6kywMOmV9uOz9ZVSCMPQhAZEFjL/qDPVrqpUA=="],
@@ -2225,6 +2359,8 @@
"expressive-code": ["expressive-code@0.41.3", "", { "dependencies": { "@expressive-code/core": "^0.41.3", "@expressive-code/plugin-frames": "^0.41.3", "@expressive-code/plugin-shiki": "^0.41.3", "@expressive-code/plugin-text-markers": "^0.41.3" } }, "sha512-YLnD62jfgBZYrXIPQcJ0a51Afv9h8VlWqEGK9uU2T5nL/5rb8SnA86+7+mgCZe5D34Tff5RNEA5hjNVJYHzrFg=="],
+ "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
+
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
@@ -2287,8 +2423,6 @@
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
- "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
-
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -2331,9 +2465,11 @@
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
+ "ghostty-web": ["ghostty-web@0.3.0", "", {}, "sha512-SAdSHWYF20GMZUB0n8kh1N6Z4ljMnuUqT8iTB2n5FAPswEV10MejEpLlhW/769GL5+BQa1NYwEg9y/XCckV5+A=="],
+
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
- "giget": ["giget@1.2.5", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.5.4", "pathe": "^2.0.3", "tar": "^6.2.1" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug=="],
+ "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
@@ -2363,7 +2499,7 @@
"h3": ["h3@2.0.1-rc.4", "", { "dependencies": { "rou3": "^0.7.8", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw=="],
- "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="],
+ "happy-dom": ["happy-dom@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "@types/whatwg-mimetype": "^3.0.2", "whatwg-mimetype": "^3.0.0" } }, "sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g=="],
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
@@ -2425,9 +2561,9 @@
"hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="],
- "hono": ["hono@4.7.10", "", {}, "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ=="],
+ "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="],
- "hono-openapi": ["hono-openapi@1.1.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.8", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-AC3HNhZYPHhnZdSy2Je7GDoTTNxPos6rKRQKVDBbSilY3cWJPqsxRnN6zA4pU7tfxmQEMTqkiLXbw6sAaemB8Q=="],
+ "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=="],
"html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="],
@@ -2449,6 +2585,8 @@
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
+ "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
+
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="],
@@ -2533,6 +2671,8 @@
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
+ "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="],
+
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
@@ -2627,6 +2767,8 @@
"jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="],
+ "jwt-decode": ["jwt-decode@3.1.2", "", {}, "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="],
+
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
@@ -2637,6 +2779,8 @@
"language-map": ["language-map@1.5.0", "", {}, "sha512-n7gFZpe+DwEAX9cXVTw43i3wiudWDDtSn28RmdnS/HCPr284dQI/SztsamWanRr75oSlKSaGbV2nmWCTzGCoVg=="],
+ "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="],
+
"leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="],
"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=="],
@@ -2701,6 +2845,8 @@
"lru.min": ["lru.min@1.1.3", "", {}, "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q=="],
+ "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="],
+
"luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
@@ -2863,12 +3009,10 @@
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
- "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
+ "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=="],
- "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
-
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@@ -2887,8 +3031,6 @@
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
- "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
-
"neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="],
"nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="],
@@ -2927,7 +3069,7 @@
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
- "nypm": ["nypm@0.5.4", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "tinyexec": "^0.3.2", "ufo": "^1.5.4" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA=="],
+ "nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
@@ -3043,7 +3185,7 @@
"peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="],
- "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
+ "perfect-debounce": ["perfect-debounce@2.0.0", "", {}, "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow=="],
"piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="],
@@ -3061,7 +3203,7 @@
"pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="],
- "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
+ "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
"pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="],
@@ -3089,6 +3231,8 @@
"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
+ "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
+
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
@@ -3099,6 +3243,8 @@
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
+ "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
+
"promise.allsettled": ["promise.allsettled@1.0.7", "", { "dependencies": { "array.prototype.map": "^1.0.5", "call-bind": "^1.0.2", "define-properties": "^1.2.0", "es-abstract": "^1.22.1", "get-intrinsic": "^1.2.1", "iterate-value": "^1.0.2" } }, "sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA=="],
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
@@ -3151,10 +3297,12 @@
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
- "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=="],
+ "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=="],
"readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="],
+ "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=="],
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
@@ -3339,7 +3487,7 @@
"solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="],
- "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
+ "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
@@ -3485,6 +3633,8 @@
"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=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
@@ -3537,8 +3687,6 @@
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
- "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="],
-
"ulid": ["ulid@3.0.1", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q=="],
"ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="],
@@ -3591,6 +3739,8 @@
"unstorage": ["unstorage@2.0.0-alpha.4", "", { "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "chokidar": "^4.0.3", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "lru-cache": "^11.2.2", "mongodb": "^6.20.0", "ofetch": "*", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "chokidar", "db0", "idb-keyval", "ioredis", "lru-cache", "mongodb", "ofetch", "uploadthing"] }, "sha512-ywXZMZRfrvmO1giJeMTCw6VUn0ALYxVl8pFqJPStiyQUvgJImejtAHrKvXPj4QGJAoS/iLGcVGF6ljN/lkh1bw=="],
+ "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.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
@@ -3645,6 +3795,8 @@
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
+ "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
+
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
@@ -3663,8 +3815,6 @@
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
- "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="],
-
"workerd": ["workerd@1.20251118.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251118.0", "@cloudflare/workerd-darwin-arm64": "1.20251118.0", "@cloudflare/workerd-linux-64": "1.20251118.0", "@cloudflare/workerd-linux-arm64": "1.20251118.0", "@cloudflare/workerd-windows-64": "1.20251118.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Om5ns0Lyx/LKtYI04IV0bjIrkBgoFNg0p6urzr2asekJlfP18RqFzyqMFZKf0i9Gnjtz/JfAS/Ol6tjCe5JJsQ=="],
"wrangler": ["wrangler@4.50.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.7.11", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20251118.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20251118.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20251118.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-+nuZuHZxDdKmAyXOSrHlciGshCoAPiy5dM+t6mEohWm7HpXvTHmWQGUf/na9jjWlWJHCJYOWzkA1P5HBJqrIEA=="],
@@ -3677,6 +3827,8 @@
"ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
+ "wsl-utils": ["wsl-utils@0.3.0", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-3sFIGLiaDP7rTO4xh3g+b3AzhYDIUGGywE/WsmqzJWDxus5aJXVnPTNC/6L+r2WzrwXqVOdD262OaO+cEyPMSQ=="],
+
"xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="],
"xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="],
@@ -3709,6 +3861,8 @@
"youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
+ "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=="],
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
@@ -3717,6 +3871,14 @@
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
+ "@actions/artifact/@octokit/core": ["@octokit/core@5.2.2", "", { "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" } }, "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg=="],
+
+ "@actions/artifact/@octokit/plugin-request-log": ["@octokit/plugin-request-log@1.0.4", "", { "peerDependencies": { "@octokit/core": ">=3" } }, "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA=="],
+
+ "@actions/artifact/@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=="],
+
+ "@actions/artifact/@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=="],
+
"@actions/github/@octokit/core": ["@octokit/core@5.2.2", "", { "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" } }, "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg=="],
"@actions/github/@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=="],
@@ -3737,20 +3899,22 @@
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
+ "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
+
"@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@2.0.71", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tg+gj+R0z/On9P4V7hy7/7o04cQPjKGayMCL3gzWD/aNGjAKkhEnaocuNDidSnghizt8g2zJn16cAuAolnW+qQ=="],
"@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
- "@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
-
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],
- "@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
-
"@ai-sdk/mcp/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
+ "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
+
+ "@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
+
"@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
"@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
@@ -3759,8 +3923,6 @@
"@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.9", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.5", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.13.0", "smol-toml": "^1.4.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-hX2cLC/KW74Io1zIbn92kI482j9J7LleBLGCVU9EP3BeH5MVrnFawOnqD0t/q6D1Z+ZNeQG2gNKMslCcO36wng=="],
- "@astrojs/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
-
"@astrojs/sitemap/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@astrojs/solid-js/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
@@ -3797,6 +3959,40 @@
"@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=="],
+ "@azure/core-auth/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="],
+
+ "@azure/core-client/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="],
+
+ "@azure/core-client/@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="],
+
+ "@azure/core-http/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
+
+ "@azure/core-http/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
+
+ "@azure/core-http-compat/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="],
+
+ "@azure/core-lro/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="],
+
+ "@azure/core-rest-pipeline/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="],
+
+ "@azure/core-rest-pipeline/@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="],
+
+ "@azure/core-util/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="],
+
+ "@azure/core-xml/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
+
+ "@azure/storage-blob/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="],
+
+ "@azure/storage-blob/@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="],
+
+ "@azure/storage-blob/events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
+
+ "@azure/storage-common/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="],
+
+ "@azure/storage-common/@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="],
+
+ "@azure/storage-common/events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
+
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
@@ -3805,12 +4001,18 @@
"@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=="],
+
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"@expressive-code/plugin-shiki/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="],
+ "@hey-api/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
+
+ "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
+
"@hono/zod-validator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@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=="],
@@ -3857,8 +4059,6 @@
"@jsx-email/doiuse-email/htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="],
- "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
-
"@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.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-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
"@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
@@ -3885,6 +4085,8 @@
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
+ "@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@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
"@octokit/request-error/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
@@ -3895,8 +4097,6 @@
"@opencode-ai/tauri/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
- "@opencode-ai/tauri/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
-
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="],
"@opencode-ai/web/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
@@ -3913,14 +4113,18 @@
"@parcel/watcher/node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
- "@pierre/precision-diffs/@shikijs/core": ["@shikijs/core@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg=="],
+ "@pierre/diffs/@shikijs/core": ["@shikijs/core@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-L7SrRibU7ZoYi1/TrZsJOFAnnHyLTE1SwHG1yNWjZIVCqjOEmCSuK2ZO9thnRbJG6TOkPp+Z963JmpCNw5nzvA=="],
- "@pierre/precision-diffs/@shikijs/transformers": ["@shikijs/transformers@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/types": "3.15.0" } }, "sha512-Hmwip5ovvSkg+Kc41JTvSHHVfCYF+C8Cp1omb5AJj4Xvd+y9IXz2rKJwmFRGsuN0vpHxywcXJ1+Y4B9S7EG1/A=="],
+ "@pierre/diffs/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-ZfWJNm2VMhKkQIKT9qXbs76RRcT0SF/CAvEz0+RkpUDAoDaCx0uFdCGzSRiD9gSlhm6AHkjdieOBJMaO2eC1rQ=="],
- "@pierre/precision-diffs/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="],
+ "@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.19.0", "", { "dependencies": { "@shikijs/core": "3.19.0", "@shikijs/types": "3.19.0" } }, "sha512-e6vwrsyw+wx4OkcrDbL+FVCxwx8jgKiCoXzakVur++mIWVcgpzIi8vxf4/b4dVTYrV/nUx5RjinMf4tq8YV8Fw=="],
+
+ "@pierre/diffs/shiki": ["shiki@3.19.0", "", { "dependencies": { "@shikijs/core": "3.19.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/engine-oniguruma": "3.19.0", "@shikijs/langs": "3.19.0", "@shikijs/themes": "3.19.0", "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-77VJr3OR/VUZzPiStyRhADmO2jApMM0V2b1qf0RpfWya8Zr1PeZev5AEpPGAAKWdiYUtcZGBE4F5QvJml1PvWA=="],
"@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
+ "@protobuf-ts/plugin/typescript": ["typescript@3.9.10", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q=="],
+
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@slack/bolt/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
@@ -3967,6 +4171,10 @@
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
+ "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=="],
+
"astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
"astro/diff": ["diff@5.2.0", "", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="],
@@ -3987,6 +4195,8 @@
"bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
+ "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
+
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
@@ -3995,9 +4205,9 @@
"boxen/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
- "c12/ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="],
+ "clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
- "c12/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
+ "compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"condense-newlines/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="],
@@ -4015,8 +4225,6 @@
"esbuild-plugin-copy/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=="],
- "estree-util-to-js/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
-
"execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],
"express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
@@ -4031,14 +4239,10 @@
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
- "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
-
"gaxios/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
- "giget/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
-
"glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
@@ -4053,6 +4257,8 @@
"jsonwebtoken/jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
+ "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=="],
+
"md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="],
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
@@ -4073,6 +4279,8 @@
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
+ "nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
+
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.71", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tg+gj+R0z/On9P4V7hy7/7o04cQPjKGayMCL3gzWD/aNGjAKkhEnaocuNDidSnghizt8g2zJn16cAuAolnW+qQ=="],
@@ -4091,6 +4299,10 @@
"openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
+ "opentui-spinner/@opentui/core": ["@opentui/core@0.1.60", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.60", "@opentui/core-darwin-x64": "0.1.60", "@opentui/core-linux-arm64": "0.1.60", "@opentui/core-linux-x64": "0.1.60", "@opentui/core-win32-arm64": "0.1.60", "@opentui/core-win32-x64": "0.1.60", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-28jphd0AJo48uvEuKXcT9pJhgAu8I2rEJhPt25cc5ipJ2iw/eDk1uoxrbID80MPDqgOEzN21vXmzXwCd6ao+hg=="],
+
+ "opentui-spinner/@opentui/solid": ["@opentui/solid@0.1.60", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.60", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-pn91stzAHNGWaNL6h39q55bq3G1/DLqxKtT3wVsRAV68dHfPpwmqikX1nEJZK8OU84ZTPS9Ly9fz8po2Mot2uQ=="],
+
"p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"parse-bmfont-xml/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
@@ -4113,7 +4325,11 @@
"raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
- "readable-web-to-node-stream/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=="],
+ "readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
+
+ "readable-stream/events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
+
+ "readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
"router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
@@ -4129,6 +4345,8 @@
"sitemap/sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="],
+ "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
+
"sst/aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="],
"sst/jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="],
@@ -4183,6 +4401,24 @@
"zod-to-ts/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+ "@actions/artifact/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="],
+
+ "@actions/artifact/@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=="],
+
+ "@actions/artifact/@octokit/core/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
+
+ "@actions/artifact/@octokit/core/before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="],
+
+ "@actions/artifact/@octokit/core/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="],
+
+ "@actions/artifact/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@9.0.6", "", { "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw=="],
+
+ "@actions/artifact/@octokit/request/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
+
+ "@actions/artifact/@octokit/request/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="],
+
+ "@actions/artifact/@octokit/request-error/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
+
"@actions/github/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="],
"@actions/github/@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=="],
@@ -4243,6 +4479,10 @@
"@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="],
+ "@azure/core-http/xml2js/sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="],
+
+ "@azure/core-xml/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="],
+
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
@@ -4409,6 +4649,8 @@
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
+ "@octokit/plugin-retry/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@12.11.0", "", {}, "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ=="],
+
"@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
@@ -4431,19 +4673,19 @@
"@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
- "@pierre/precision-diffs/@shikijs/core/@shikijs/types": ["@shikijs/types@3.15.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw=="],
+ "@pierre/diffs/@shikijs/core/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="],
- "@pierre/precision-diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.15.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw=="],
+ "@pierre/diffs/@shikijs/engine-javascript/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="],
- "@pierre/precision-diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg=="],
+ "@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="],
- "@pierre/precision-diffs/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA=="],
+ "@pierre/diffs/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1hRxtYIJfJSZeM5ivbUXv9hcJP3PWRo5prG/V2sWwiubUKTa+7P62d2qxCW8jiVFX4pgRHhnHNp+qeR7Xl+6kg=="],
- "@pierre/precision-diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0" } }, "sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A=="],
+ "@pierre/diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0" } }, "sha512-dBMFzzg1QiXqCVQ5ONc0z2ebyoi5BKz+MtfByLm0o5/nbUu3Iz8uaTCa5uzGiscQKm7lVShfZHU1+OG3t5hgwg=="],
- "@pierre/precision-diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0" } }, "sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ=="],
+ "@pierre/diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0" } }, "sha512-H36qw+oh91Y0s6OlFfdSuQ0Ld+5CgB/VE6gNPK+Hk4VRbVG/XQgkjnt4KzfnnoO6tZPtKJKHPjwebOCfjd6F8A=="],
- "@pierre/precision-diffs/shiki/@shikijs/types": ["@shikijs/types@3.15.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw=="],
+ "@pierre/diffs/shiki/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="],
"@slack/web-api/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
@@ -4521,6 +4763,12 @@
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+ "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.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
+
+ "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=="],
+
"astro/shiki/@shikijs/core": ["@shikijs/core@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg=="],
"astro/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg=="],
@@ -4603,12 +4851,6 @@
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
- "giget/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
-
- "giget/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
-
- "giget/tar/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
-
"gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"js-beautify/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
@@ -4619,7 +4861,9 @@
"jsonwebtoken/jws/jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
- "opencode/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
+ "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
+
+ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
@@ -4635,6 +4879,22 @@
"opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
+ "opentui-spinner/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.60", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N4feqnOBDA4O4yocpat5vOiV06HqJVwJGx8rEZE9DiOtl1i+1cPQ1Lx6+zWdLhbrVBJ0ENhb7Azox8sXkm/+5Q=="],
+
+ "opentui-spinner/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.60", "", { "os": "darwin", "cpu": "x64" }, "sha512-+z3q4WaoIs7ANU8+eTFlvnfCjAS81rk81TOdZm4TJ53Ti3/B+yheWtnV/mLpLLhvZDz2VUVxxRmfDrGMnJb4fQ=="],
+
+ "opentui-spinner/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.60", "", { "os": "linux", "cpu": "arm64" }, "sha512-/Q65sjqVGB9ygJ6lStI8n1X6RyfmJZC8XofRGEuFiMLiWcWC/xoBtztdL8LAIvHQy42y2+pl9zIiW0fWSQ0wjw=="],
+
+ "opentui-spinner/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.60", "", { "os": "linux", "cpu": "x64" }, "sha512-AegF+g7OguIpjZKN+PS55sc3ZFY6fj+fLwfETbSRGw6NqX+aiwpae0Y3gXX1s298Yq5yQEzMXnARTCJTGH4uzg=="],
+
+ "opentui-spinner/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.60", "", { "os": "win32", "cpu": "arm64" }, "sha512-fbkq8MOZJgT3r9q3JWqsfVxRpQ1SlbmhmvB35BzukXnZBK8eA178wbSadGH6irMDrkSIYye9WYddHI/iXjmgVQ=="],
+
+ "opentui-spinner/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.60", "", { "os": "win32", "cpu": "x64" }, "sha512-OebCL7f9+CKodBw0G+NvKIcc74bl6/sBEHfb73cACdJDJKh+T3C3Vt9H3kQQ0m1C8wRAqX6rh706OArk1pUb2A=="],
+
+ "opentui-spinner/@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
+
+ "opentui-spinner/@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.9", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.8" }, "optionalPeers": ["solid-js"] }, "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw=="],
+
"parse-bmfont-xml/xml2js/sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="],
"pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
@@ -4643,9 +4903,7 @@
"prebuild-install/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=="],
- "readable-web-to-node-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
-
- "readable-web-to-node-stream/readable-stream/events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
+ "readable-stream/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
@@ -4669,6 +4927,12 @@
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+ "@actions/artifact/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
+
+ "@actions/artifact/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
+
+ "@actions/artifact/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
+
"@actions/github/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
"@actions/github/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
@@ -4767,6 +5031,8 @@
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+ "archiver-utils/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
+
"astro/unstorage/h3/cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
"astro/unstorage/h3/crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
@@ -4777,8 +5043,6 @@
"esbuild-plugin-copy/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
- "giget/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
-
"js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
@@ -4809,11 +5073,13 @@
"opencontrol/@modelcontextprotocol/sdk/raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
+ "opentui-spinner/@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
"pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
"pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
- "readable-web-to-node-stream/readable-stream/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
+ "prebuild-install/tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"tw-to-css/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
diff --git a/flake.lock b/flake.lock
index 33aae3812..580e0af73 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
- "lastModified": 1764290847,
- "narHash": "sha256-VwPgoDgnd628GdE3KyLqTyPF1WWh0VwT5UoKygoi8sg=",
+ "lastModified": 1765772535,
+ "narHash": "sha256-aq+dQoaPONOSjtFIBnAXseDm9TUhIbe215TPmkfMYww=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "cd5fedfc384cb98d9fd3827b55f4522f49efda42",
+ "rev": "09b8fda8959d761445f12b55f380d90375a1d6bb",
"type": "github"
},
"original": {
diff --git a/github/action.yml b/github/action.yml
index 0b7367ded..fe6a32206 100644
--- a/github/action.yml
+++ b/github/action.yml
@@ -13,13 +13,41 @@ inputs:
description: "Share the opencode session (defaults to true for public repos)"
required: false
+ prompt:
+ description: "Custom prompt to override the default prompt"
+ required: false
+
+ use_github_token:
+ description: "Use GITHUB_TOKEN directly instead of OpenCode App token exchange. When true, skips OIDC and uses the GITHUB_TOKEN env var."
+ required: false
+ default: "false"
+
runs:
using: "composite"
steps:
+ - name: Get opencode version
+ id: version
+ shell: bash
+ run: |
+ VERSION=$(curl -sf https://api.github.com/repos/sst/opencode/releases/latest | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4)
+ echo "version=${VERSION:-latest}" >> $GITHUB_OUTPUT
+
+ - name: Cache opencode
+ id: cache
+ uses: actions/cache@v4
+ with:
+ path: ~/.opencode/bin
+ key: opencode-${{ runner.os }}-${{ runner.arch }}-${{ steps.version.outputs.version }}
+
- name: Install opencode
+ if: steps.cache.outputs.cache-hit != 'true'
shell: bash
run: curl -fsSL https://opencode.ai/install | bash
+ - name: Add opencode to PATH
+ shell: bash
+ run: echo "$HOME/.opencode/bin" >> $GITHUB_PATH
+
- name: Run opencode
shell: bash
id: run_opencode
@@ -27,3 +55,5 @@ runs:
env:
MODEL: ${{ inputs.model }}
SHARE: ${{ inputs.share }}
+ PROMPT: ${{ inputs.prompt }}
+ USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}
diff --git a/github/package.json b/github/package.json
index 1a6598d6b..4d447716f 100644
--- a/github/package.json
+++ b/github/package.json
@@ -13,7 +13,7 @@
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@octokit/graphql": "9.0.1",
- "@octokit/rest": "22.0.0",
+ "@octokit/rest": "catalog:",
"@opencode-ai/sdk": "workspace:*"
}
}
diff --git a/github/sst-env.d.ts b/github/sst-env.d.ts
index 6b69016e7..f742a1200 100644
--- a/github/sst-env.d.ts
+++ b/github/sst-env.d.ts
@@ -6,4 +6,4 @@
///
import "sst"
-export {}
+export {}
\ No newline at end of file
diff --git a/infra/console.ts b/infra/console.ts
index eddc97d2f..8f54823f8 100644
--- a/infra/console.ts
+++ b/infra/console.ts
@@ -102,6 +102,7 @@ const ZEN_MODELS = [
new sst.Secret("ZEN_MODELS2"),
new sst.Secret("ZEN_MODELS3"),
new sst.Secret("ZEN_MODELS4"),
+ new sst.Secret("ZEN_MODELS5"),
]
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
@@ -116,7 +117,7 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
// CONSOLE
////////////////
-const bucket = new sst.cloudflare.Bucket("ConsoleData")
+const bucket = new sst.cloudflare.Bucket("ZenData")
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
diff --git a/infra/enterprise.ts b/infra/enterprise.ts
index 70693846a..22b4c6f44 100644
--- a/infra/enterprise.ts
+++ b/infra/enterprise.ts
@@ -1,10 +1,10 @@
import { SECRET } from "./secret"
-import { domain } from "./stage"
+import { domain, shortDomain } from "./stage"
const storage = new sst.cloudflare.Bucket("EnterpriseStorage")
-const enterprise = new sst.cloudflare.x.SolidStart("Enterprise", {
- domain: "enterprise." + domain,
+const teams = new sst.cloudflare.x.SolidStart("Teams", {
+ domain: shortDomain,
path: "packages/enterprise",
buildCommand: "bun run build:cloudflare",
environment: {
diff --git a/infra/stage.ts b/infra/stage.ts
index 729422905..f9a6fd755 100644
--- a/infra/stage.ts
+++ b/infra/stage.ts
@@ -11,3 +11,9 @@ new cloudflare.RegionalHostname("RegionalHostname", {
regionKey: "us",
zoneId: zoneID,
})
+
+export const shortDomain = (() => {
+ if ($app.stage === "production") return "opncd.ai"
+ if ($app.stage === "dev") return "dev.opncd.ai"
+ return `${$app.stage}.dev.opncd.ai`
+})()
diff --git a/install b/install
index 77ecf34b9..67690b9a3 100755
--- a/install
+++ b/install
@@ -4,7 +4,7 @@ APP=opencode
MUTED='\033[0;2m'
RED='\033[0;31m'
-ORANGE='\033[38;2;255;140;0m'
+ORANGE='\033[38;5;214m'
NC='\033[0m' # No Color
requested_version=${VERSION:-}
@@ -240,22 +240,23 @@ download_with_progress() {
download_and_install() {
print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version"
- mkdir -p opencodetmp && cd opencodetmp
+ local tmp_dir="${TMPDIR:-/tmp}/opencode_install_$$"
+ mkdir -p "$tmp_dir"
- if [[ "$os" == "windows" ]] || ! download_with_progress "$url" "$filename"; then
- # Fallback to standard curl on Windows or if custom progress fails
- curl -# -L -o "$filename" "$url"
+ if [[ "$os" == "windows" ]] || ! [ -t 2 ] || ! download_with_progress "$url" "$tmp_dir/$filename"; then
+ # Fallback to standard curl on Windows, non-TTY environments, or if custom progress fails
+ curl -# -L -o "$tmp_dir/$filename" "$url"
fi
if [ "$os" = "linux" ]; then
- tar -xzf "$filename"
+ tar -xzf "$tmp_dir/$filename" -C "$tmp_dir"
else
- unzip -q "$filename"
+ unzip -q "$tmp_dir/$filename" -d "$tmp_dir"
fi
- mv opencode "$INSTALL_DIR"
+ mv "$tmp_dir/opencode" "$INSTALL_DIR"
chmod 755 "${INSTALL_DIR}/opencode"
- cd .. && rm -rf opencodetmp
+ rm -rf "$tmp_dir"
}
check_version
@@ -353,10 +354,10 @@ echo -e "${MUTED}█░░█ █░░█ █▀▀▀ █░░█ ${NC}█░
echo -e "${MUTED}▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ${NC}▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"
echo -e ""
echo -e ""
-echo -e "${MUTED}To get started, navigate to a project and run:${NC}"
-echo -e "opencode ${MUTED}Use free models${NC}"
-echo -e "opencode auth login ${MUTED}Add paid provider API keys${NC}"
-echo -e "opencode help ${MUTED}List commands and options${NC}"
+echo -e "${MUTED}OpenCode includes free models, to start:${NC}"
+echo -e ""
+echo -e "cd ${MUTED}# Open directory${NC}"
+echo -e "opencode ${MUTED}# Run command${NC}"
echo -e ""
echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/docs"
echo -e ""
diff --git a/nix/hashes.json b/nix/hashes.json
index 1a2d21588..4db956bb3 100644
--- a/nix/hashes.json
+++ b/nix/hashes.json
@@ -1,3 +1,3 @@
{
- "nodeModules": "sha256-4OrnnZy44edgShLMUrYFsO+07gAnL//pGKO/ehWu5P4="
+ "nodeModules": "sha256-PyoVOza+3WnwZbtpPF6uSN1zkyLsSG2VsgBfIMvIFAs="
}
diff --git a/nix/opencode.nix b/nix/opencode.nix
index 8c4e9fb57..87b3f17ba 100644
--- a/nix/opencode.nix
+++ b/nix/opencode.nix
@@ -1,4 +1,4 @@
-{ lib, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
+{ lib, stdenvNoCC, bun, ripgrep, makeBinaryWrapper }:
args:
let
scripts = args.scripts;
@@ -97,7 +97,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
makeWrapper ${bun}/bin/bun $out/bin/opencode \
--add-flags "run" \
--add-flags "$out/lib/opencode/dist/src/index.js" \
- --prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]} \
+ --prefix PATH : ${lib.makeBinPath [ ripgrep ]} \
--argv0 opencode
runHook postInstall
diff --git a/package.json b/package.json
index e85f08e4e..2be8bfe76 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"description": "AI-powered development tool",
"private": true,
"type": "module",
- "packageManager": "bun@1.3.3",
+ "packageManager": "bun@1.3.4",
"scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"typecheck": "bun turbo typecheck",
@@ -20,7 +20,8 @@
"packages/slack"
],
"catalog": {
- "@types/bun": "1.3.3",
+ "@types/bun": "1.3.4",
+ "@octokit/rest": "22.0.0",
"@hono/zod-validator": "0.4.2",
"ulid": "3.0.1",
"@kobalte/core": "0.13.11",
@@ -30,16 +31,16 @@
"@tsconfig/bun": "1.0.9",
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
- "@pierre/precision-diffs": "0.5.7",
+ "@pierre/diffs": "1.0.0-beta.3",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
"ai": "5.0.97",
- "hono": "4.7.10",
- "hono-openapi": "1.1.1",
+ "hono": "4.10.7",
+ "hono-openapi": "1.1.2",
"fuzzysort": "3.1.0",
"luxon": "3.6.1",
"typescript": "5.8.2",
- "@typescript/native-preview": "7.0.0-dev.20251014.1",
+ "@typescript/native-preview": "7.0.0-dev.20251207.1",
"zod": "4.1.8",
"remeda": "2.26.0",
"solid-list": "0.3.0",
@@ -86,5 +87,8 @@
"overrides": {
"@types/bun": "catalog:",
"@types/node": "catalog:"
+ },
+ "patchedDependencies": {
+ "ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch"
}
}
diff --git a/packages/console/app/package.json b/packages/console/app/package.json
index 2b2b02135..d0f7aac82 100644
--- a/packages/console/app/package.json
+++ b/packages/console/app/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
- "version": "1.0.120",
+ "version": "1.0.162",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
diff --git a/packages/console/app/public/social-share-zen.png b/packages/console/app/public/social-share-zen.png
deleted file mode 100644
index 33e941441..000000000
Binary files a/packages/console/app/public/social-share-zen.png and /dev/null differ
diff --git a/packages/console/app/public/social-share-zen.png b/packages/console/app/public/social-share-zen.png
new file mode 120000
index 000000000..2cb95c718
--- /dev/null
+++ b/packages/console/app/public/social-share-zen.png
@@ -0,0 +1 @@
+../../../ui/src/assets/images/social-share-zen.png
\ No newline at end of file
diff --git a/packages/console/app/public/social-share.png b/packages/console/app/public/social-share.png
deleted file mode 100644
index 92224f54c..000000000
Binary files a/packages/console/app/public/social-share.png and /dev/null differ
diff --git a/packages/console/app/public/social-share.png b/packages/console/app/public/social-share.png
new file mode 120000
index 000000000..deb3346c2
--- /dev/null
+++ b/packages/console/app/public/social-share.png
@@ -0,0 +1 @@
+../../../ui/src/assets/images/social-share.png
\ No newline at end of file
diff --git a/packages/console/app/src/app.tsx b/packages/console/app/src/app.tsx
index bc94b443e..cde2f0187 100644
--- a/packages/console/app/src/app.tsx
+++ b/packages/console/app/src/app.tsx
@@ -3,6 +3,7 @@ import { Router } from "@solidjs/router"
import { FileRoutes } from "@solidjs/start/router"
import { Suspense } from "solid-js"
import { Favicon } from "@opencode-ai/ui/favicon"
+import { Font } from "@opencode-ai/ui/font"
import "@ibm/plex/css/ibm-plex.css"
import "./app.css"
@@ -13,8 +14,9 @@ export default function App() {
root={(props) => (
opencode
-
+
+
{props.children}
)}
diff --git a/packages/console/app/src/asset/lander/desktop-app-icon.png b/packages/console/app/src/asset/lander/desktop-app-icon.png
new file mode 100644
index 000000000..a35c28f51
Binary files /dev/null and b/packages/console/app/src/asset/lander/desktop-app-icon.png differ
diff --git a/packages/console/app/src/asset/lander/opencode-desktop-icon.png b/packages/console/app/src/asset/lander/opencode-desktop-icon.png
new file mode 100644
index 000000000..f2c8d4f5a
Binary files /dev/null and b/packages/console/app/src/asset/lander/opencode-desktop-icon.png differ
diff --git a/packages/console/app/src/asset/lander/opencode-min.mp4 b/packages/console/app/src/asset/lander/opencode-min.mp4
index 47468bedf..ffd6c4f7a 100644
Binary files a/packages/console/app/src/asset/lander/opencode-min.mp4 and b/packages/console/app/src/asset/lander/opencode-min.mp4 differ
diff --git a/packages/console/app/src/component/email-signup.tsx b/packages/console/app/src/component/email-signup.tsx
index 4943921e7..65f81b5fc 100644
--- a/packages/console/app/src/component/email-signup.tsx
+++ b/packages/console/app/src/component/email-signup.tsx
@@ -25,11 +25,8 @@ export function EmailSignup() {
const submission = useSubmission(emailSignup)
return (
-
-

-
-
OpenCode will be available on desktop soon
+
Be the first to know when we release new products
Join the waitlist for early access.
diff --git a/packages/desktop/src/components/terminal.tsx b/packages/desktop/src/components/terminal.tsx
new file mode 100644
index 000000000..082525e28
--- /dev/null
+++ b/packages/desktop/src/components/terminal.tsx
@@ -0,0 +1,159 @@
+import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
+import { ComponentProps, createEffect, onCleanup, onMount, splitProps } from "solid-js"
+import { useSDK } from "@/context/sdk"
+import { SerializeAddon } from "@/addons/serialize"
+import { LocalPTY } from "@/context/terminal"
+import { usePrefersDark } from "@solid-primitives/media"
+
+export interface TerminalProps extends ComponentProps<"div"> {
+ pty: LocalPTY
+ onSubmit?: () => void
+ onCleanup?: (pty: LocalPTY) => void
+ onConnectError?: (error: unknown) => void
+}
+
+export const Terminal = (props: TerminalProps) => {
+ const sdk = useSDK()
+ let container!: HTMLDivElement
+ const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
+ let ws: WebSocket
+ let term: Term
+ let ghostty: Ghostty
+ let serializeAddon: SerializeAddon
+ let fitAddon: FitAddon
+ let handleResize: () => void
+ const prefersDark = usePrefersDark()
+
+ onMount(async () => {
+ ghostty = await Ghostty.load()
+
+ ws = new WebSocket(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
+ term = new Term({
+ cursorBlink: true,
+ fontSize: 14,
+ fontFamily: "TX-02, monospace",
+ allowTransparency: true,
+ theme: prefersDark()
+ ? {
+ background: "#191515",
+ foreground: "#d4d4d4",
+ cursor: "#d4d4d4",
+ }
+ : {
+ background: "#fcfcfc",
+ foreground: "#211e1e",
+ cursor: "#211e1e",
+ },
+ scrollback: 10_000,
+ ghostty,
+ })
+ term.attachCustomKeyEventHandler((event) => {
+ // allow for ctrl-` to toggle terminal in parent
+ if (event.ctrlKey && event.key.toLowerCase() === "`") {
+ event.preventDefault()
+ return true
+ }
+ return false
+ })
+
+ fitAddon = new FitAddon()
+ serializeAddon = new SerializeAddon()
+ term.loadAddon(serializeAddon)
+ term.loadAddon(fitAddon)
+
+ term.open(container)
+
+ if (local.pty.buffer) {
+ if (local.pty.rows && local.pty.cols) {
+ term.resize(local.pty.cols, local.pty.rows)
+ }
+ term.reset()
+ term.write(local.pty.buffer)
+ if (local.pty.scrollY) {
+ term.scrollToLine(local.pty.scrollY)
+ }
+ fitAddon.fit()
+ }
+
+ container.focus()
+
+ fitAddon.observeResize()
+ handleResize = () => fitAddon.fit()
+ window.addEventListener("resize", handleResize)
+ term.onResize(async (size) => {
+ if (ws && ws.readyState === WebSocket.OPEN) {
+ await sdk.client.pty.update({
+ ptyID: local.pty.id,
+ size: {
+ cols: size.cols,
+ rows: size.rows,
+ },
+ })
+ }
+ })
+ term.onData((data) => {
+ if (ws && ws.readyState === WebSocket.OPEN) {
+ ws.send(data)
+ }
+ })
+ term.onKey((key) => {
+ if (key.key == "Enter") {
+ props.onSubmit?.()
+ }
+ })
+ // term.onScroll((ydisp) => {
+ // console.log("Scroll position:", ydisp)
+ // })
+ ws.addEventListener("open", () => {
+ console.log("WebSocket connected")
+ sdk.client.pty.update({
+ ptyID: local.pty.id,
+ size: {
+ cols: term.cols,
+ rows: term.rows,
+ },
+ })
+ })
+ ws.addEventListener("message", (event) => {
+ term.write(event.data)
+ })
+ ws.addEventListener("error", (error) => {
+ console.error("WebSocket error:", error)
+ props.onConnectError?.(error)
+ })
+ ws.addEventListener("close", () => {
+ console.log("WebSocket disconnected")
+ })
+ })
+
+ onCleanup(() => {
+ if (handleResize) {
+ window.removeEventListener("resize", handleResize)
+ }
+ if (serializeAddon && props.onCleanup) {
+ const buffer = serializeAddon.serialize()
+ props.onCleanup({
+ ...local.pty,
+ buffer,
+ rows: term.rows,
+ cols: term.cols,
+ scrollY: term.getViewportY(),
+ })
+ }
+ ws?.close()
+ term?.dispose()
+ })
+
+ return (
+
+ )
+}
diff --git a/packages/desktop/src/context/command.tsx b/packages/desktop/src/context/command.tsx
new file mode 100644
index 000000000..8fd76ee21
--- /dev/null
+++ b/packages/desktop/src/context/command.tsx
@@ -0,0 +1,239 @@
+import { createMemo, createSignal, onCleanup, onMount, Show, type Accessor } from "solid-js"
+import { createSimpleContext } from "@opencode-ai/ui/context"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { Dialog } from "@opencode-ai/ui/dialog"
+import { List } from "@opencode-ai/ui/list"
+
+const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
+
+export type KeybindConfig = string
+
+export interface Keybind {
+ key: string
+ ctrl: boolean
+ meta: boolean
+ shift: boolean
+ alt: boolean
+}
+
+export interface CommandOption {
+ id: string
+ title: string
+ description?: string
+ category?: string
+ keybind?: KeybindConfig
+ slash?: string
+ suggested?: boolean
+ disabled?: boolean
+ onSelect?: (source?: "palette" | "keybind" | "slash") => void
+}
+
+export function parseKeybind(config: string): Keybind[] {
+ if (!config || config === "none") return []
+
+ return config.split(",").map((combo) => {
+ const parts = combo.trim().toLowerCase().split("+")
+ const keybind: Keybind = {
+ key: "",
+ ctrl: false,
+ meta: false,
+ shift: false,
+ alt: false,
+ }
+
+ for (const part of parts) {
+ switch (part) {
+ case "ctrl":
+ case "control":
+ keybind.ctrl = true
+ break
+ case "meta":
+ case "cmd":
+ case "command":
+ keybind.meta = true
+ break
+ case "mod":
+ if (IS_MAC) keybind.meta = true
+ else keybind.ctrl = true
+ break
+ case "alt":
+ case "option":
+ keybind.alt = true
+ break
+ case "shift":
+ keybind.shift = true
+ break
+ default:
+ keybind.key = part
+ break
+ }
+ }
+
+ return keybind
+ })
+}
+
+export function matchKeybind(keybinds: Keybind[], event: KeyboardEvent): boolean {
+ const eventKey = event.key.toLowerCase()
+
+ for (const kb of keybinds) {
+ const keyMatch = kb.key === eventKey
+ const ctrlMatch = kb.ctrl === (event.ctrlKey || false)
+ const metaMatch = kb.meta === (event.metaKey || false)
+ const shiftMatch = kb.shift === (event.shiftKey || false)
+ const altMatch = kb.alt === (event.altKey || false)
+
+ if (keyMatch && ctrlMatch && metaMatch && shiftMatch && altMatch) {
+ return true
+ }
+ }
+
+ return false
+}
+
+export function formatKeybind(config: string): string {
+ if (!config || config === "none") return ""
+
+ const keybinds = parseKeybind(config)
+ if (keybinds.length === 0) return ""
+
+ const kb = keybinds[0]
+ const parts: string[] = []
+
+ if (kb.ctrl) parts.push(IS_MAC ? "⌃" : "Ctrl")
+ if (kb.alt) parts.push(IS_MAC ? "⌥" : "Alt")
+ if (kb.shift) parts.push(IS_MAC ? "⇧" : "Shift")
+ if (kb.meta) parts.push(IS_MAC ? "⌘" : "Meta")
+
+ if (kb.key) {
+ const displayKey = kb.key.length === 1 ? kb.key.toUpperCase() : kb.key.charAt(0).toUpperCase() + kb.key.slice(1)
+ parts.push(displayKey)
+ }
+
+ return IS_MAC ? parts.join("") : parts.join("+")
+}
+
+function DialogCommand(props: { options: CommandOption[] }) {
+ const dialog = useDialog()
+
+ return (
+
+ )
+}
+
+export const { use: useCommand, provider: CommandProvider } = createSimpleContext({
+ name: "Command",
+ init: () => {
+ const [registrations, setRegistrations] = createSignal[]>([])
+ const [suspendCount, setSuspendCount] = createSignal(0)
+ const dialog = useDialog()
+
+ const options = createMemo(() => {
+ const all = registrations().flatMap((x) => x())
+ const suggested = all.filter((x) => x.suggested && !x.disabled)
+ return [
+ ...suggested.map((x) => ({
+ ...x,
+ id: "suggested." + x.id,
+ category: "Suggested",
+ })),
+ ...all,
+ ]
+ })
+
+ const suspended = () => suspendCount() > 0
+
+ const showPalette = () => {
+ if (!dialog.active) {
+ dialog.show(() => !x.disabled)} />)
+ }
+ }
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (suspended()) return
+
+ const paletteKeybinds = parseKeybind("mod+shift+p")
+ if (matchKeybind(paletteKeybinds, event)) {
+ event.preventDefault()
+ showPalette()
+ return
+ }
+
+ for (const option of options()) {
+ if (option.disabled) continue
+ if (!option.keybind) continue
+
+ const keybinds = parseKeybind(option.keybind)
+ if (matchKeybind(keybinds, event)) {
+ event.preventDefault()
+ option.onSelect?.("keybind")
+ return
+ }
+ }
+ }
+
+ onMount(() => {
+ document.addEventListener("keydown", handleKeyDown)
+ })
+
+ onCleanup(() => {
+ document.removeEventListener("keydown", handleKeyDown)
+ })
+
+ return {
+ register(cb: () => CommandOption[]) {
+ const results = createMemo(cb)
+ setRegistrations((arr) => [results, ...arr])
+ onCleanup(() => {
+ setRegistrations((arr) => arr.filter((x) => x !== results))
+ })
+ },
+ trigger(id: string, source?: "palette" | "keybind" | "slash") {
+ for (const option of options()) {
+ if (option.id === id || option.id === "suggested." + id) {
+ option.onSelect?.(source)
+ return
+ }
+ }
+ },
+ show: showPalette,
+ keybinds(enabled: boolean) {
+ setSuspendCount((count) => count + (enabled ? -1 : 1))
+ },
+ suspended,
+ get options() {
+ return options()
+ },
+ }
+ },
+})
diff --git a/packages/desktop/src/context/global-sdk.tsx b/packages/desktop/src/context/global-sdk.tsx
index b9c72afcb..34e731ac9 100644
--- a/packages/desktop/src/context/global-sdk.tsx
+++ b/packages/desktop/src/context/global-sdk.tsx
@@ -1,4 +1,4 @@
-import { createOpencodeClient, type Event } from "@opencode-ai/sdk/client"
+import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { onCleanup } from "solid-js"
@@ -19,7 +19,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
sdk.global.event().then(async (events) => {
for await (const event of events.stream) {
// console.log("event", event)
- emitter.emit(event.directory, event.payload)
+ emitter.emit(event.directory ?? "global", event.payload)
}
})
diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx
index a8a6b9937..ad3a3bf18 100644
--- a/packages/desktop/src/context/global-sync.tsx
+++ b/packages/desktop/src/context/global-sync.tsx
@@ -1,28 +1,33 @@
-import type {
- Message,
- Agent,
- Provider,
- Session,
- Part,
- Config,
- Path,
- File,
- FileNode,
- Project,
- FileDiff,
- Todo,
- SessionStatus,
-} from "@opencode-ai/sdk"
+import {
+ type Message,
+ type Agent,
+ type Session,
+ type Part,
+ type Config,
+ type Path,
+ type File,
+ type FileNode,
+ type Project,
+ type FileDiff,
+ type Todo,
+ type SessionStatus,
+ type ProviderListResponse,
+ type ProviderAuthResponse,
+ type Command,
+ createOpencodeClient,
+} from "@opencode-ai/sdk/v2/client"
import { createStore, produce, reconcile } from "solid-js/store"
import { Binary } from "@opencode-ai/util/binary"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSDK } from "./global-sdk"
+import { onMount } from "solid-js"
type State = {
ready: boolean
- provider: Provider[]
agent: Agent[]
- project: Project
+ command: Command[]
+ project: string
+ provider: ProviderListResponse
config: Config
path: Path
session: Session[]
@@ -49,52 +54,137 @@ type State = {
export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimpleContext({
name: "GlobalSync",
init: () => {
+ const globalSDK = useGlobalSDK()
const [globalStore, setGlobalStore] = createStore<{
ready: boolean
- defaultProject?: Project // TODO: remove this when we can select projects
- projects: Project[]
+ path: Path
+ project: Project[]
+ provider: ProviderListResponse
+ provider_auth: ProviderAuthResponse
children: Record
}>({
ready: false,
- projects: [],
+ path: { state: "", config: "", worktree: "", directory: "", home: "" },
+ project: [],
+ provider: { all: [], connected: [], default: {} },
+ provider_auth: {},
children: {},
})
const children: Record>> = {}
-
function child(directory: string) {
if (!children[directory]) {
setGlobalStore("children", directory, {
- project: { id: "", worktree: "", time: { created: 0, initialized: 0 } },
+ project: "",
+ provider: { all: [], connected: [], default: {} },
config: {},
- path: { state: "", config: "", worktree: "", directory: "" },
+ path: { state: "", config: "", worktree: "", directory: "", home: "" },
ready: false,
agent: [],
- provider: [],
+ command: [],
session: [],
session_status: {},
session_diff: {},
todo: {},
- limit: 10,
+ limit: 5,
message: {},
part: {},
node: [],
changes: [],
})
children[directory] = createStore(globalStore.children[directory])
+ bootstrapInstance(directory)
}
return children[directory]
}
- const sdk = useGlobalSDK()
- sdk.event.listen((e) => {
- const directory = e.name
- const [store, setStore] = child(directory)
+ async function loadSessions(directory: string) {
+ globalSDK.client.session.list({ directory }).then((x) => {
+ const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
+ const nonArchived = (x.data ?? [])
+ .slice()
+ .filter((s) => !s.time.archived)
+ .sort((a, b) => a.id.localeCompare(b.id))
+ // Include at least 5 sessions, plus any updated in the last hour
+ const sessions = nonArchived.filter((s, i) => {
+ if (i < 5) return true
+ const updated = new Date(s.time.updated).getTime()
+ return updated > fourHoursAgo
+ })
+ const [, setStore] = child(directory)
+ setStore("session", sessions)
+ })
+ }
+ async function bootstrapInstance(directory: string) {
+ const [, setStore] = child(directory)
+ const sdk = createOpencodeClient({
+ baseUrl: globalSDK.url,
+ directory,
+ })
+ const load = {
+ project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
+ provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)),
+ path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
+ agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
+ command: () => sdk.command.list().then((x) => setStore("command", x.data ?? [])),
+ session: () => loadSessions(directory),
+ status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
+ config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
+ changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
+ node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
+ }
+ await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
+ }
+
+ globalSDK.event.listen((e) => {
+ console.log(e)
+ const directory = e.name
const event = e.details
+
+ if (directory === "global") {
+ switch (event?.type) {
+ case "global.disposed": {
+ bootstrap()
+ break
+ }
+ case "project.updated": {
+ const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
+ if (result.found) {
+ setGlobalStore("project", result.index, reconcile(event.properties))
+ return
+ }
+ setGlobalStore(
+ "project",
+ produce((draft) => {
+ draft.splice(result.index, 0, event.properties)
+ }),
+ )
+ break
+ }
+ }
+ return
+ }
+
+ const [store, setStore] = child(directory)
switch (event.type) {
+ case "server.instance.disposed": {
+ bootstrapInstance(directory)
+ break
+ }
case "session.updated": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
+ if (event.properties.info.time.archived) {
+ if (result.found) {
+ setStore(
+ "session",
+ produce((draft) => {
+ draft.splice(result.index, 1)
+ }),
+ )
+ }
+ break
+ }
if (result.found) {
setStore("session", result.index, reconcile(event.properties.info))
break
@@ -137,6 +227,21 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
)
break
}
+ case "message.removed": {
+ const messages = store.message[event.properties.sessionID]
+ if (!messages) break
+ const result = Binary.search(messages, event.properties.messageID, (m) => m.id)
+ if (result.found) {
+ setStore(
+ "message",
+ event.properties.sessionID,
+ produce((draft) => {
+ draft.splice(result.index, 1)
+ }),
+ )
+ }
+ break
+ }
case "message.part.updated": {
const part = event.properties.part
const parts = store.part[part.messageID]
@@ -158,19 +263,47 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
)
break
}
+ case "message.part.removed": {
+ const parts = store.part[event.properties.messageID]
+ if (!parts) break
+ const result = Binary.search(parts, event.properties.partID, (p) => p.id)
+ if (result.found) {
+ setStore(
+ "part",
+ event.properties.messageID,
+ produce((draft) => {
+ draft.splice(result.index, 1)
+ }),
+ )
+ }
+ break
+ }
}
})
- Promise.all([
- sdk.client.project.list().then((x) =>
- setGlobalStore(
- "projects",
- x.data!.filter((x) => !x.worktree.includes("opencode-test")),
- ),
- ),
- // TODO: remove this when we can select projects
- sdk.client.project.current().then((x) => setGlobalStore("defaultProject", x.data)),
- ]).then(() => setGlobalStore("ready", true))
+ async function bootstrap() {
+ return Promise.all([
+ globalSDK.client.path.get().then((x) => {
+ setGlobalStore("path", x.data!)
+ }),
+ globalSDK.client.project.list().then(async (x) => {
+ setGlobalStore(
+ "project",
+ x.data!.filter((p) => !p.worktree.includes("opencode-test")).sort((a, b) => a.id.localeCompare(b.id)),
+ )
+ }),
+ globalSDK.client.provider.list().then((x) => {
+ setGlobalStore("provider", x.data ?? {})
+ }),
+ globalSDK.client.provider.auth().then((x) => {
+ setGlobalStore("provider_auth", x.data ?? {})
+ }),
+ ]).then(() => setGlobalStore("ready", true))
+ }
+
+ onMount(() => {
+ bootstrap()
+ })
return {
data: globalStore,
@@ -178,6 +311,10 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
return globalStore.ready
},
child,
+ bootstrap,
+ project: {
+ loadSessions,
+ },
}
},
})
diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx
index 81e8b537a..af71c6a00 100644
--- a/packages/desktop/src/context/layout.tsx
+++ b/packages/desktop/src/context/layout.tsx
@@ -1,48 +1,123 @@
-import { createStore } from "solid-js/store"
-import { createMemo } from "solid-js"
+import { createStore, produce } from "solid-js/store"
+import { batch, createMemo, onMount } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { makePersisted } from "@solid-primitives/storage"
import { useGlobalSync } from "./global-sync"
+import { useGlobalSDK } from "./global-sdk"
+import { Project } from "@opencode-ai/sdk/v2"
+
+const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
+export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
+
+export function getAvatarColors(key?: string) {
+ if (key && AVATAR_COLOR_KEYS.includes(key as AvatarColorKey)) {
+ return {
+ background: `var(--avatar-background-${key})`,
+ foreground: `var(--avatar-text-${key})`,
+ }
+ }
+ return {
+ background: "var(--surface-info-base)",
+ foreground: "var(--text-base)",
+ }
+}
+
+type SessionTabs = {
+ active?: string
+ all: string[]
+}
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
name: "Layout",
init: () => {
+ const globalSdk = useGlobalSDK()
const globalSync = useGlobalSync()
const [store, setStore] = makePersisted(
createStore({
- projects: [] as { directory: string; expanded: boolean }[],
+ projects: [] as { worktree: string; expanded: boolean }[],
sidebar: {
- opened: true,
+ opened: false,
width: 280,
},
+ terminal: {
+ opened: false,
+ height: 280,
+ },
review: {
state: "pane" as "pane" | "tab",
},
+ sessionTabs: {} as Record,
}),
{
- name: "___default-layout",
+ name: "layout.v3",
},
)
+ const usedColors = new Set()
+
+ function pickAvailableColor(): AvatarColorKey {
+ const available = AVATAR_COLOR_KEYS.filter((c) => !usedColors.has(c))
+ if (available.length === 0) return AVATAR_COLOR_KEYS[Math.floor(Math.random() * AVATAR_COLOR_KEYS.length)]
+ return available[Math.floor(Math.random() * available.length)]
+ }
+
+ function enrich(project: { worktree: string; expanded: boolean }) {
+ const metadata = globalSync.data.project.find((x) => x.worktree === project.worktree)
+ if (!metadata) return []
+ return [
+ {
+ ...project,
+ ...metadata,
+ },
+ ]
+ }
+
+ function colorize(project: Project & { expanded: boolean }) {
+ if (project.icon?.color) return project
+ const color = pickAvailableColor()
+ usedColors.add(color)
+ project.icon = { ...project.icon, color }
+ globalSdk.client.project.update({ projectID: project.id, icon: { color } })
+ return project
+ }
+
+ const enriched = createMemo(() => store.projects.flatMap(enrich))
+ const list = createMemo(() => enriched().flatMap(colorize))
+
+ onMount(() => {
+ Promise.all(
+ store.projects.map((project) => {
+ return globalSync.project.loadSessions(project.worktree)
+ }),
+ )
+ })
+
return {
projects: {
- list: createMemo(() =>
- globalSync.data.defaultProject
- ? [{ directory: globalSync.data.defaultProject!.worktree, expanded: true }, ...store.projects]
- : store.projects,
- ),
+ list,
open(directory: string) {
- if (store.projects.find((x) => x.directory === directory)) return
- setStore("projects", (x) => [...x, { directory, expanded: true }])
+ if (store.projects.find((x) => x.worktree === directory)) return
+ globalSync.project.loadSessions(directory)
+ setStore("projects", (x) => [{ worktree: directory, expanded: true }, ...x])
},
close(directory: string) {
- setStore("projects", (x) => x.filter((x) => x.directory !== directory))
+ setStore("projects", (x) => x.filter((x) => x.worktree !== directory))
},
expand(directory: string) {
- setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, expanded: true } : x)))
+ setStore("projects", (x) => x.map((x) => (x.worktree === directory ? { ...x, expanded: true } : x)))
},
collapse(directory: string) {
- setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, expanded: false } : x)))
+ setStore("projects", (x) => x.map((x) => (x.worktree === directory ? { ...x, expanded: false } : x)))
+ },
+ move(directory: string, toIndex: number) {
+ setStore("projects", (projects) => {
+ const fromIndex = projects.findIndex((x) => x.worktree === directory)
+ if (fromIndex === -1 || fromIndex === toIndex) return projects
+ const result = [...projects]
+ const [item] = result.splice(fromIndex, 1)
+ result.splice(toIndex, 0, item)
+ return result
+ })
},
},
sidebar: {
@@ -61,6 +136,22 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("sidebar", "width", width)
},
},
+ terminal: {
+ opened: createMemo(() => store.terminal.opened),
+ open() {
+ setStore("terminal", "opened", true)
+ },
+ close() {
+ setStore("terminal", "opened", false)
+ },
+ toggle() {
+ setStore("terminal", "opened", (x) => !x)
+ },
+ height: createMemo(() => store.terminal.height),
+ resize(height: number) {
+ setStore("terminal", "height", height)
+ },
+ },
review: {
state: createMemo(() => store.review?.state ?? "closed"),
pane() {
@@ -70,6 +161,86 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("review", "state", "tab")
},
},
+ tabs(sessionKey: string) {
+ const tabs = createMemo(() => store.sessionTabs[sessionKey] ?? { all: [] })
+ return {
+ tabs,
+ active: createMemo(() => tabs().active),
+ all: createMemo(() => tabs().all),
+ setActive(tab: string | undefined) {
+ if (!store.sessionTabs[sessionKey]) {
+ setStore("sessionTabs", sessionKey, { all: [], active: tab })
+ } else {
+ setStore("sessionTabs", sessionKey, "active", tab)
+ }
+ },
+ setAll(all: string[]) {
+ if (!store.sessionTabs[sessionKey]) {
+ setStore("sessionTabs", sessionKey, { all, active: undefined })
+ } else {
+ setStore("sessionTabs", sessionKey, "all", all)
+ }
+ },
+ async open(tab: string) {
+ if (tab === "chat") {
+ if (!store.sessionTabs[sessionKey]) {
+ setStore("sessionTabs", sessionKey, { all: [], active: undefined })
+ } else {
+ setStore("sessionTabs", sessionKey, "active", undefined)
+ }
+ return
+ }
+ const current = store.sessionTabs[sessionKey] ?? { all: [] }
+ if (tab !== "review") {
+ if (!current.all.includes(tab)) {
+ if (!store.sessionTabs[sessionKey]) {
+ setStore("sessionTabs", sessionKey, { all: [tab], active: tab })
+ } else {
+ setStore("sessionTabs", sessionKey, "all", [...current.all, tab])
+ setStore("sessionTabs", sessionKey, "active", tab)
+ }
+ return
+ }
+ }
+ if (!store.sessionTabs[sessionKey]) {
+ setStore("sessionTabs", sessionKey, { all: [], active: tab })
+ } else {
+ setStore("sessionTabs", sessionKey, "active", tab)
+ }
+ },
+ close(tab: string) {
+ const current = store.sessionTabs[sessionKey]
+ if (!current) return
+ batch(() => {
+ setStore(
+ "sessionTabs",
+ sessionKey,
+ "all",
+ current.all.filter((x) => x !== tab),
+ )
+ if (current.active === tab) {
+ const index = current.all.findIndex((f) => f === tab)
+ const previous = current.all[Math.max(0, index - 1)]
+ setStore("sessionTabs", sessionKey, "active", previous)
+ }
+ })
+ },
+ move(tab: string, to: number) {
+ const current = store.sessionTabs[sessionKey]
+ if (!current) return
+ const index = current.all.findIndex((f) => f === tab)
+ if (index === -1) return
+ setStore(
+ "sessionTabs",
+ sessionKey,
+ "all",
+ produce((opened) => {
+ opened.splice(to, 0, opened.splice(index, 1)[0])
+ }),
+ )
+ },
+ }
+ },
}
},
})
diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx
index 68da03438..b12679210 100644
--- a/packages/desktop/src/context/local.tsx
+++ b/packages/desktop/src/context/local.tsx
@@ -1,11 +1,14 @@
import { createStore, produce, reconcile } from "solid-js/store"
import { batch, createEffect, createMemo } from "solid-js"
-import { uniqueBy } from "remeda"
-import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk"
+import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
+import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useSDK } from "./sdk"
import { useSync } from "./sync"
-import { base64Encode } from "@/utils"
+import { base64Encode } from "@opencode-ai/util/encode"
+import { useProviders } from "@/hooks/use-providers"
+import { makePersisted } from "@solid-primitives/storage"
+import { DateTime } from "luxon"
export type LocalFile = FileNode &
Partial<{
@@ -25,6 +28,7 @@ export type View = LocalFile["view"]
export type LocalModel = Omit & {
provider: Provider
+ latest?: boolean
}
export type ModelKey = { providerID: string; modelID: string }
@@ -36,10 +40,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
init: () => {
const sdk = useSDK()
const sync = useSync()
+ const providers = useProviders()
function isModelValid(model: ModelKey) {
- const provider = sync.data.provider.find((x) => x.id === model.providerID)
- return !!provider?.models[model.modelID]
+ const provider = providers.all().find((x) => x.id === model.providerID)
+ return (
+ !!provider?.models[model.modelID] &&
+ providers
+ .connected()
+ .map((p) => p.id)
+ .includes(model.providerID)
+ )
}
function getFirstValidModel(...modelFns: (() => ModelKey | undefined)[]) {
@@ -69,7 +80,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
const agent = (() => {
- const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent"))
+ const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
const [store, setStore] = createStore<{
current: string
}>({
@@ -99,23 +110,62 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})()
const model = (() => {
- const [store, setStore] = createStore<{
+ const [store, setStore] = makePersisted(
+ createStore<{
+ user: (ModelKey & { visibility: "show" | "hide"; favorite?: boolean })[]
+ recent: ModelKey[]
+ }>({
+ user: [],
+ recent: [],
+ }),
+ { name: "model.v1" },
+ )
+
+ const [ephemeral, setEphemeral] = createStore<{
model: Record
- recent: ModelKey[]
}>({
model: {},
- recent: [],
})
- const value = localStorage.getItem("model")
- setStore("recent", JSON.parse(value ?? "[]"))
- createEffect(() => {
- localStorage.setItem("model", JSON.stringify(store.recent))
- })
+ const available = createMemo(() =>
+ providers.connected().flatMap((p) =>
+ Object.values(p.models).map((m) => ({
+ ...m,
+ provider: p,
+ })),
+ ),
+ )
+
+ const latest = createMemo(() =>
+ pipe(
+ available(),
+ filter((x) => Math.abs(DateTime.fromISO(x.release_date).diffNow().as("months")) < 6),
+ groupBy((x) => x.provider.id),
+ mapValues((models) =>
+ pipe(
+ models,
+ groupBy((x) => x.family),
+ values(),
+ (groups) =>
+ groups.flatMap((g) => {
+ const first = firstBy(g, [(x) => x.release_date, "desc"])
+ return first ? [{ modelID: first.id, providerID: first.provider.id }] : []
+ }),
+ ),
+ ),
+ values(),
+ flat(),
+ ),
+ )
const list = createMemo(() =>
- sync.data.provider.flatMap((p) => Object.values(p.models).map((m) => ({ ...m, provider: p }) as LocalModel)),
+ available().map((m) => ({
+ ...m,
+ name: m.name.replace("(latest)", "").trim(),
+ latest: m.name.includes("(latest)"),
+ })),
)
+
const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID)
const fallbackModel = createMemo(() => {
@@ -134,18 +184,23 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return item
}
}
- const provider = sync.data.provider[0]
- const model = Object.values(provider.models)[0]
- return {
- providerID: provider.id,
- modelID: model.id,
+
+ for (const p of providers.connected()) {
+ if (p.id in providers.default()) {
+ return {
+ providerID: p.id,
+ modelID: providers.default()[p.id],
+ }
+ }
}
+
+ throw new Error("No default model found")
})
- const currentModel = createMemo(() => {
+ const current = createMemo(() => {
const a = agent.current()
const key = getFirstValidModel(
- () => store.model[a.name],
+ () => ephemeral.model[a.name],
() => a.model,
fallbackModel,
)!
@@ -156,10 +211,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const cycle = (direction: 1 | -1) => {
const recentList = recent()
- const current = currentModel()
- if (!current) return
+ const currentModel = current()
+ if (!currentModel) return
- const index = recentList.findIndex((x) => x?.provider.id === current.provider.id && x?.id === current.id)
+ const index = recentList.findIndex(
+ (x) => x?.provider.id === currentModel.provider.id && x?.id === currentModel.id,
+ )
if (index === -1) return
let next = index + direction
@@ -175,14 +232,24 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
}
+ function updateVisibility(model: ModelKey, visibility: "show" | "hide") {
+ const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID)
+ if (index >= 0) {
+ setStore("user", index, { visibility })
+ } else {
+ setStore("user", store.user.length, { ...model, visibility })
+ }
+ }
+
return {
- current: currentModel,
+ current,
recent,
list,
cycle,
set(model: ModelKey | undefined, options?: { recent?: boolean }) {
batch(() => {
- setStore("model", agent.current().name, model ?? fallbackModel())
+ setEphemeral("model", agent.current().name, model ?? fallbackModel())
+ if (model) updateVisibility(model, "show")
if (options?.recent && model) {
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
if (uniq.length > 5) uniq.pop()
@@ -190,6 +257,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
})
},
+ visible(model: ModelKey) {
+ const user = store.user.find((x) => x.modelID === model.modelID && x.providerID === model.providerID)
+ return (
+ user?.visibility !== "hide" &&
+ (latest().find((x) => x.modelID === model.modelID && x.providerID === model.providerID) ||
+ user?.visibility === "show")
+ )
+ },
+ setVisibility(model: ModelKey, visible: boolean) {
+ updateVisibility(model, visible ? "show" : "hide")
+ },
}
})()
@@ -257,7 +335,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const load = async (path: string) => {
const relativePath = relative(path)
- sdk.client.file.read({ query: { path: relativePath } }).then((x) => {
+ await sdk.client.file.read({ path: relativePath }).then((x) => {
setStore(
"node",
relativePath,
@@ -305,7 +383,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
const list = async (path: string) => {
- return sdk.client.file.list({ query: { path: path + "/" } }).then((x) => {
+ return sdk.client.file.list({ path: path + "/" }).then((x) => {
setStore(
"node",
produce((draft) => {
@@ -318,10 +396,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
}
- const searchFiles = (query: string) =>
- sdk.client.find.files({ query: { query, dirs: "false" } }).then((x) => x.data!)
+ const searchFiles = (query: string) => sdk.client.find.files({ query, dirs: "false" }).then((x) => x.data!)
const searchFilesAndDirectories = (query: string) =>
- sdk.client.find.files({ query: { query, dirs: "true" } }).then((x) => x.data!)
+ sdk.client.find.files({ query, dirs: "true" }).then((x) => x.data!)
sdk.event.listen((e) => {
const event = e.details
@@ -329,14 +406,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
case "file.watcher.updated":
const relativePath = relative(event.properties.file)
if (relativePath.startsWith(".git/")) return
- load(relativePath)
+ if (store.node[relativePath]) load(relativePath)
break
}
})
return {
node: async (path: string) => {
- if (!store.node[path]) {
+ if (!store.node[path] || !store.node[path].loaded) {
await init(path)
}
return store.node[path]
diff --git a/packages/desktop/src/context/notification.tsx b/packages/desktop/src/context/notification.tsx
new file mode 100644
index 000000000..ee15bc34a
--- /dev/null
+++ b/packages/desktop/src/context/notification.tsx
@@ -0,0 +1,122 @@
+import { createStore } from "solid-js/store"
+import { createSimpleContext } from "@opencode-ai/ui/context"
+import { makePersisted } from "@solid-primitives/storage"
+import { useGlobalSDK } from "./global-sdk"
+import { useGlobalSync } from "./global-sync"
+import { Binary } from "@opencode-ai/util/binary"
+import { EventSessionError } from "@opencode-ai/sdk/v2"
+import { makeAudioPlayer } from "@solid-primitives/audio"
+import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac"
+import errorSound from "@opencode-ai/ui/audio/nope-03.aac"
+
+type NotificationBase = {
+ directory?: string
+ session?: string
+ metadata?: any
+ time: number
+ viewed: boolean
+}
+
+type TurnCompleteNotification = NotificationBase & {
+ type: "turn-complete"
+}
+
+type ErrorNotification = NotificationBase & {
+ type: "error"
+ error: EventSessionError["properties"]["error"]
+}
+
+export type Notification = TurnCompleteNotification | ErrorNotification
+
+export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
+ name: "Notification",
+ init: () => {
+ const idlePlayer = makeAudioPlayer(idleSound)
+ const errorPlayer = makeAudioPlayer(errorSound)
+ const globalSDK = useGlobalSDK()
+ const globalSync = useGlobalSync()
+
+ const [store, setStore] = makePersisted(
+ createStore({
+ list: [] as Notification[],
+ }),
+ {
+ name: "notification.v1",
+ },
+ )
+
+ // onMount(() => {
+ // const daysToKeep = 7
+ // // setStore("list", (n) => n.filter((n) => !n.viewed && n.time + 1000 * 60 * 60 * 24 * daysToKeep < Date.now()))
+ // })
+
+ globalSDK.event.listen((e) => {
+ console.log(e)
+ const directory = e.name
+ const event = e.details
+ const base = {
+ directory,
+ time: Date.now(),
+ viewed: false,
+ }
+ switch (event.type) {
+ case "session.idle": {
+ const sessionID = event.properties.sessionID
+ const [syncStore] = globalSync.child(directory)
+ const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
+ const isChild = match.found && syncStore.session[match.index].parentID
+ if (isChild) break
+ idlePlayer.play()
+ setStore("list", store.list.length, {
+ ...base,
+ type: "turn-complete",
+ session: sessionID,
+ })
+ break
+ }
+ case "session.error": {
+ const sessionID = event.properties.sessionID
+ if (sessionID) {
+ const [syncStore] = globalSync.child(directory)
+ const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
+ const isChild = match.found && syncStore.session[match.index].parentID
+ if (isChild) break
+ }
+ errorPlayer.play()
+ setStore("list", store.list.length, {
+ ...base,
+ type: "error",
+ session: sessionID ?? "global",
+ error: "error" in event.properties ? event.properties.error : undefined,
+ })
+ break
+ }
+ }
+ })
+
+ return {
+ session: {
+ all(session: string) {
+ return store.list.filter((n) => n.session === session)
+ },
+ unseen(session: string) {
+ return store.list.filter((n) => n.session === session && !n.viewed)
+ },
+ markViewed(session: string) {
+ setStore("list", (n) => n.session === session, "viewed", true)
+ },
+ },
+ project: {
+ all(directory: string) {
+ return store.list.filter((n) => n.directory === directory)
+ },
+ unseen(directory: string) {
+ return store.list.filter((n) => n.directory === directory && !n.viewed)
+ },
+ markViewed(directory: string) {
+ setStore("list", (n) => n.directory === directory, "viewed", true)
+ },
+ },
+ }
+ },
+})
diff --git a/packages/desktop/src/context/platform.tsx b/packages/desktop/src/context/platform.tsx
new file mode 100644
index 000000000..21be49cbd
--- /dev/null
+++ b/packages/desktop/src/context/platform.tsx
@@ -0,0 +1,25 @@
+import { createSimpleContext } from "@opencode-ai/ui/context"
+
+export type Platform = {
+ /** Platform discriminator */
+ platform: "web" | "tauri"
+
+ /** Open native directory picker dialog (Tauri only) */
+ openDirectoryPickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise
+
+ /** Open native file picker dialog (Tauri only) */
+ openFilePickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise
+
+ /** Save file picker dialog (Tauri only) */
+ saveFilePickerDialog?(opts?: { title?: string; defaultPath?: string }): Promise
+
+ /** Open a URL in the default browser */
+ openLink(url: string): void
+}
+
+export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
+ name: "Platform",
+ init: (props: { value: Platform }) => {
+ return props.value
+ },
+})
diff --git a/packages/desktop/src/context/prompt.tsx b/packages/desktop/src/context/prompt.tsx
new file mode 100644
index 000000000..2da0a08d5
--- /dev/null
+++ b/packages/desktop/src/context/prompt.tsx
@@ -0,0 +1,112 @@
+import { createStore } from "solid-js/store"
+import { createSimpleContext } from "@opencode-ai/ui/context"
+import { batch, createMemo } from "solid-js"
+import { makePersisted } from "@solid-primitives/storage"
+import { useParams } from "@solidjs/router"
+import { TextSelection } from "./local"
+
+interface PartBase {
+ content: string
+ start: number
+ end: number
+}
+
+export interface TextPart extends PartBase {
+ type: "text"
+}
+
+export interface FileAttachmentPart extends PartBase {
+ type: "file"
+ path: string
+ selection?: TextSelection
+}
+
+export interface ImageAttachmentPart {
+ type: "image"
+ id: string
+ filename: string
+ mime: string
+ dataUrl: string
+}
+
+export type ContentPart = TextPart | FileAttachmentPart | ImageAttachmentPart
+export type Prompt = ContentPart[]
+
+export const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
+
+export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean {
+ if (promptA.length !== promptB.length) return false
+ for (let i = 0; i < promptA.length; i++) {
+ const partA = promptA[i]
+ const partB = promptB[i]
+ if (partA.type !== partB.type) return false
+ if (partA.type === "text" && partA.content !== (partB as TextPart).content) {
+ return false
+ }
+ if (partA.type === "file" && partA.path !== (partB as FileAttachmentPart).path) {
+ return false
+ }
+ if (partA.type === "image" && partA.id !== (partB as ImageAttachmentPart).id) {
+ return false
+ }
+ }
+ return true
+}
+
+function cloneSelection(selection?: TextSelection) {
+ if (!selection) return undefined
+ return { ...selection }
+}
+
+function clonePart(part: ContentPart): ContentPart {
+ if (part.type === "text") return { ...part }
+ if (part.type === "image") return { ...part }
+ return {
+ ...part,
+ selection: cloneSelection(part.selection),
+ }
+}
+
+function clonePrompt(prompt: Prompt): Prompt {
+ return prompt.map(clonePart)
+}
+
+export const { use: usePrompt, provider: PromptProvider } = createSimpleContext({
+ name: "Prompt",
+ init: () => {
+ const params = useParams()
+ const name = createMemo(() => `${params.dir}/prompt${params.id ? "/" + params.id : ""}.v1`)
+
+ const [store, setStore] = makePersisted(
+ createStore<{
+ prompt: Prompt
+ cursor?: number
+ }>({
+ prompt: clonePrompt(DEFAULT_PROMPT),
+ cursor: undefined,
+ }),
+ {
+ name: name(),
+ },
+ )
+
+ return {
+ current: createMemo(() => store.prompt),
+ cursor: createMemo(() => store.cursor),
+ dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
+ set(prompt: Prompt, cursorPosition?: number) {
+ const next = clonePrompt(prompt)
+ batch(() => {
+ setStore("prompt", next)
+ if (cursorPosition !== undefined) setStore("cursor", cursorPosition)
+ })
+ },
+ reset() {
+ batch(() => {
+ setStore("prompt", clonePrompt(DEFAULT_PROMPT))
+ setStore("cursor", 0)
+ })
+ },
+ }
+ },
+})
diff --git a/packages/desktop/src/context/sdk.tsx b/packages/desktop/src/context/sdk.tsx
index 81b32035a..764b01f8a 100644
--- a/packages/desktop/src/context/sdk.tsx
+++ b/packages/desktop/src/context/sdk.tsx
@@ -1,4 +1,4 @@
-import { createOpencodeClient, type Event } from "@opencode-ai/sdk/client"
+import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { onCleanup } from "solid-js"
@@ -27,6 +27,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
abort.abort()
})
- return { directory: props.directory, client: sdk, event: emitter }
+ return { directory: props.directory, client: sdk, event: emitter, url: globalSDK.url }
},
})
diff --git a/packages/desktop/src/context/session.tsx b/packages/desktop/src/context/session.tsx
deleted file mode 100644
index 72098a939..000000000
--- a/packages/desktop/src/context/session.tsx
+++ /dev/null
@@ -1,237 +0,0 @@
-import { createStore, produce } from "solid-js/store"
-import { createSimpleContext } from "@opencode-ai/ui/context"
-import { batch, createEffect, createMemo } from "solid-js"
-import { useSync } from "./sync"
-import { makePersisted } from "@solid-primitives/storage"
-import { TextSelection } from "./local"
-import { pipe, sumBy } from "remeda"
-import { AssistantMessage, UserMessage } from "@opencode-ai/sdk"
-import { useParams } from "@solidjs/router"
-import { base64Encode } from "@/utils"
-
-export const { use: useSession, provider: SessionProvider } = createSimpleContext({
- name: "Session",
- init: () => {
- const params = useParams()
- const sync = useSync()
- const name = createMemo(
- () => `___${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}`,
- )
-
- const [store, setStore] = makePersisted(
- createStore<{
- messageId?: string
- tabs: {
- active?: string
- opened: string[]
- }
- prompt: Prompt
- cursor?: number
- }>({
- tabs: {
- opened: [],
- },
- prompt: clonePrompt(DEFAULT_PROMPT),
- cursor: undefined,
- }),
- {
- name: name(),
- },
- )
-
- createEffect(() => {
- if (!params.id) return
- sync.session.sync(params.id)
- })
-
- const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
- const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
- const userMessages = createMemo(() =>
- messages()
- .filter((m) => m.role === "user")
- .sort((a, b) => b.id.localeCompare(a.id)),
- )
- const lastUserMessage = createMemo(() => {
- return userMessages()?.at(0)
- })
- const activeMessage = createMemo(() => {
- if (!store.messageId) return lastUserMessage()
- return userMessages()?.find((m) => m.id === store.messageId)
- })
- const status = createMemo(
- () =>
- sync.data.session_status[params.id ?? ""] ?? {
- type: "idle",
- },
- )
- const working = createMemo(() => status()?.type !== "idle")
-
- const cost = createMemo(() => {
- const total = pipe(
- messages(),
- sumBy((x) => (x.role === "assistant" ? x.cost : 0)),
- )
- return new Intl.NumberFormat("en-US", {
- style: "currency",
- currency: "USD",
- }).format(total)
- })
-
- const last = createMemo(
- () => messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage,
- )
- const model = createMemo(() =>
- last() ? sync.data.provider.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined,
- )
- const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
-
- const tokens = createMemo(() => {
- if (!last()) return
- const tokens = last().tokens
- return tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write
- })
-
- const context = createMemo(() => {
- const total = tokens()
- const limit = model()?.limit.context
- if (!total || !limit) return 0
- return Math.round((total / limit) * 100)
- })
-
- return {
- get id() {
- return params.id
- },
- info,
- status,
- working,
- diffs,
- prompt: {
- current: createMemo(() => store.prompt),
- cursor: createMemo(() => store.cursor),
- dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
- set(prompt: Prompt, cursorPosition?: number) {
- const next = clonePrompt(prompt)
- batch(() => {
- setStore("prompt", next)
- if (cursorPosition !== undefined) setStore("cursor", cursorPosition)
- })
- },
- },
- messages: {
- all: messages,
- user: userMessages,
- last: lastUserMessage,
- active: activeMessage,
- setActive(message: UserMessage | undefined) {
- setStore("messageId", message?.id)
- },
- },
- usage: {
- tokens,
- cost,
- context,
- },
- layout: {
- tabs: store.tabs,
- setActiveTab(tab: string | undefined) {
- setStore("tabs", "active", tab)
- },
- setOpenedTabs(tabs: string[]) {
- setStore("tabs", "opened", tabs)
- },
- async openTab(tab: string) {
- if (tab === "chat") {
- setStore("tabs", "active", undefined)
- return
- }
- if (tab !== "review") {
- if (!store.tabs.opened.includes(tab)) {
- setStore("tabs", "opened", [...store.tabs.opened, tab])
- }
- }
- setStore("tabs", "active", tab)
- },
- closeTab(tab: string) {
- batch(() => {
- setStore(
- "tabs",
- "opened",
- store.tabs.opened.filter((x) => x !== tab),
- )
- if (store.tabs.active === tab) {
- const index = store.tabs.opened.findIndex((f) => f === tab)
- const previous = store.tabs.opened[Math.max(0, index - 1)]
- setStore("tabs", "active", previous)
- }
- })
- },
- moveTab(tab: string, to: number) {
- const index = store.tabs.opened.findIndex((f) => f === tab)
- if (index === -1) return
- setStore(
- "tabs",
- "opened",
- produce((opened) => {
- opened.splice(to, 0, opened.splice(index, 1)[0])
- }),
- )
- },
- },
- }
- },
-})
-
-interface PartBase {
- content: string
- start: number
- end: number
-}
-
-export interface TextPart extends PartBase {
- type: "text"
-}
-
-export interface FileAttachmentPart extends PartBase {
- type: "file"
- path: string
- selection?: TextSelection
-}
-
-export type ContentPart = TextPart | FileAttachmentPart
-export type Prompt = ContentPart[]
-
-export const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
-
-export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean {
- if (promptA.length !== promptB.length) return false
- for (let i = 0; i < promptA.length; i++) {
- const partA = promptA[i]
- const partB = promptB[i]
- if (partA.type !== partB.type) return false
- if (partA.type === "text" && partA.content !== (partB as TextPart).content) {
- return false
- }
- if (partA.type === "file" && partA.path !== (partB as FileAttachmentPart).path) {
- return false
- }
- }
- return true
-}
-
-function cloneSelection(selection?: TextSelection) {
- if (!selection) return undefined
- return { ...selection }
-}
-
-function clonePart(part: ContentPart): ContentPart {
- if (part.type === "text") return { ...part }
- return {
- ...part,
- selection: cloneSelection(part.selection),
- }
-}
-
-function clonePrompt(prompt: Prompt): Prompt {
- return prompt.map(clonePart)
-}
diff --git a/packages/desktop/src/context/sync.tsx b/packages/desktop/src/context/sync.tsx
index 3eb921a31..2ab54b3ae 100644
--- a/packages/desktop/src/context/sync.tsx
+++ b/packages/desktop/src/context/sync.tsx
@@ -11,28 +11,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const globalSync = useGlobalSync()
const sdk = useSDK()
const [store, setStore] = globalSync.child(sdk.directory)
-
- const load = {
- project: () => sdk.client.project.current().then((x) => setStore("project", x.data!)),
- provider: () => sdk.client.config.providers().then((x) => setStore("provider", x.data!.providers)),
- path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)),
- agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
- session: () =>
- sdk.client.session.list().then((x) => {
- const sessions = (x.data ?? [])
- .slice()
- .sort((a, b) => a.id.localeCompare(b.id))
- .slice(0, store.limit)
- setStore("session", sessions)
- }),
- status: () => sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
- config: () => sdk.client.config.get().then((x) => setStore("config", x.data!)),
- changes: () => sdk.client.file.status().then((x) => setStore("changes", x.data!)),
- node: () => sdk.client.file.list({ query: { path: "/" } }).then((x) => setStore("node", x.data!)),
- }
-
- Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
-
const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
return {
@@ -41,6 +19,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
get ready() {
return store.ready
},
+ get project() {
+ const match = Binary.search(globalSync.data.project, store.project, (p) => p.id)
+ if (match.found) return globalSync.data.project[match.index]
+ return undefined
+ },
session: {
get(sessionID: string) {
const match = Binary.search(store.session, sessionID, (s) => s.id)
@@ -49,10 +32,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
},
async sync(sessionID: string, _isRetry = false) {
const [session, messages, todo, diff] = await Promise.all([
- sdk.client.session.get({ path: { id: sessionID }, throwOnError: true }),
- sdk.client.session.messages({ path: { id: sessionID }, query: { limit: 100 } }),
- sdk.client.session.todo({ path: { id: sessionID } }),
- sdk.client.session.diff({ path: { id: sessionID } }),
+ sdk.client.session.get({ sessionID }, { throwOnError: true }),
+ sdk.client.session.messages({ sessionID, limit: 100 }),
+ sdk.client.session.todo({ sessionID }),
+ sdk.client.session.diff({ sessionID }),
])
setStore(
produce((draft) => {
@@ -73,11 +56,25 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
},
fetch: async (count = 10) => {
setStore("limit", (x) => x + count)
- await load.session()
+ await sdk.client.session.list().then((x) => {
+ const sessions = (x.data ?? [])
+ .slice()
+ .sort((a, b) => a.id.localeCompare(b.id))
+ .slice(0, store.limit)
+ setStore("session", sessions)
+ })
},
more: createMemo(() => store.session.length >= store.limit),
+ archive: async (sessionID: string) => {
+ await sdk.client.session.update({ sessionID, time: { archived: Date.now() } })
+ setStore(
+ produce((draft) => {
+ const match = Binary.search(draft.session, sessionID, (s) => s.id)
+ if (match.found) draft.session.splice(match.index, 1)
+ }),
+ )
+ },
},
- load,
absolute,
get directory() {
return store.path.directory
diff --git a/packages/desktop/src/context/terminal.tsx b/packages/desktop/src/context/terminal.tsx
new file mode 100644
index 000000000..cf9b5a5b9
--- /dev/null
+++ b/packages/desktop/src/context/terminal.tsx
@@ -0,0 +1,106 @@
+import { createStore, produce } from "solid-js/store"
+import { createSimpleContext } from "@opencode-ai/ui/context"
+import { batch, createMemo } from "solid-js"
+import { makePersisted } from "@solid-primitives/storage"
+import { useParams } from "@solidjs/router"
+import { useSDK } from "./sdk"
+
+export type LocalPTY = {
+ id: string
+ title: string
+ rows?: number
+ cols?: number
+ buffer?: string
+ scrollY?: number
+}
+
+export const { use: useTerminal, provider: TerminalProvider } = createSimpleContext({
+ name: "Terminal",
+ init: () => {
+ const sdk = useSDK()
+ const params = useParams()
+ const name = createMemo(() => `${params.dir}/terminal${params.id ? "/" + params.id : ""}.v1`)
+
+ const [store, setStore] = makePersisted(
+ createStore<{
+ active?: string
+ all: LocalPTY[]
+ }>({
+ all: [],
+ }),
+ {
+ name: name(),
+ },
+ )
+
+ return {
+ all: createMemo(() => Object.values(store.all)),
+ active: createMemo(() => store.active),
+ new() {
+ sdk.client.pty.create({ title: `Terminal ${store.all.length + 1}` }).then((pty) => {
+ const id = pty.data?.id
+ if (!id) return
+ setStore("all", [
+ ...store.all,
+ {
+ id,
+ title: pty.data?.title ?? "Terminal",
+ },
+ ])
+ setStore("active", id)
+ })
+ },
+ update(pty: Partial & { id: string }) {
+ setStore("all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x)))
+ sdk.client.pty.update({
+ ptyID: pty.id,
+ title: pty.title,
+ size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
+ })
+ },
+ async clone(id: string) {
+ const index = store.all.findIndex((x) => x.id === id)
+ const pty = store.all[index]
+ if (!pty) return
+ const clone = await sdk.client.pty.create({
+ title: pty.title,
+ })
+ if (!clone.data) return
+ setStore("all", index, {
+ ...pty,
+ ...clone.data,
+ })
+ if (store.active === pty.id) {
+ setStore("active", clone.data.id)
+ }
+ },
+ open(id: string) {
+ setStore("active", id)
+ },
+ async close(id: string) {
+ batch(() => {
+ setStore(
+ "all",
+ store.all.filter((x) => x.id !== id),
+ )
+ if (store.active === id) {
+ const index = store.all.findIndex((f) => f.id === id)
+ const previous = store.all[Math.max(0, index - 1)]
+ setStore("active", previous?.id)
+ }
+ })
+ await sdk.client.pty.remove({ ptyID: id })
+ },
+ move(id: string, to: number) {
+ const index = store.all.findIndex((f) => f.id === id)
+ if (index === -1) return
+ setStore(
+ "all",
+ produce((all) => {
+ all.splice(to, 0, all.splice(index, 1)[0])
+ }),
+ )
+ },
+ }
+ },
+})
diff --git a/packages/ui/src/index.tsx b/packages/desktop/src/entry.tsx
similarity index 51%
rename from packages/ui/src/index.tsx
rename to packages/desktop/src/entry.tsx
index fa76ba9af..eec6396e9 100644
--- a/packages/ui/src/index.tsx
+++ b/packages/desktop/src/entry.tsx
@@ -1,22 +1,27 @@
-/* @refresh reload */
+// @refresh reload
import { render } from "solid-js/web"
-import { MetaProvider } from "@solidjs/meta"
-
-import Demo from "./demo"
+import { App } from "@/app"
+import { Platform, PlatformProvider } from "@/context/platform"
const root = document.getElementById("root")
-
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
throw new Error(
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
)
}
+const platform: Platform = {
+ platform: "web",
+ openLink(url: string) {
+ window.open(url, "_blank")
+ },
+}
+
render(
() => (
-
-
-
+
+
+
),
root!,
)
diff --git a/packages/desktop/src/hooks/use-providers.ts b/packages/desktop/src/hooks/use-providers.ts
new file mode 100644
index 000000000..4a73fa055
--- /dev/null
+++ b/packages/desktop/src/hooks/use-providers.ts
@@ -0,0 +1,31 @@
+import { useGlobalSync } from "@/context/global-sync"
+import { base64Decode } from "@opencode-ai/util/encode"
+import { useParams } from "@solidjs/router"
+import { createMemo } from "solid-js"
+
+export const popularProviders = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"]
+
+export function useProviders() {
+ const globalSync = useGlobalSync()
+ const params = useParams()
+ const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
+ const providers = createMemo(() => {
+ if (currentDirectory()) {
+ const [projectStore] = globalSync.child(currentDirectory())
+ return projectStore.provider
+ }
+ return globalSync.data.provider
+ })
+ const connected = createMemo(() => providers().all.filter((p) => providers().connected.includes(p.id)))
+ const paid = createMemo(() =>
+ connected().filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input)),
+ )
+ const popular = createMemo(() => providers().all.filter((p) => popularProviders.includes(p.id)))
+ return {
+ all: createMemo(() => providers().all),
+ default: createMemo(() => providers().default),
+ popular,
+ connected,
+ paid,
+ }
+}
diff --git a/packages/desktop/src/index.ts b/packages/desktop/src/index.ts
new file mode 100644
index 000000000..cf5be9f51
--- /dev/null
+++ b/packages/desktop/src/index.ts
@@ -0,0 +1,2 @@
+export { PlatformProvider, type Platform } from "./context/platform"
+export { App } from "./app"
diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx
deleted file mode 100644
index 149b907bc..000000000
--- a/packages/desktop/src/index.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-/* @refresh reload */
-import "@/index.css"
-import { render } from "solid-js/web"
-import { Router, Route, Navigate } from "@solidjs/router"
-import { MetaProvider } from "@solidjs/meta"
-import { Font } from "@opencode-ai/ui/font"
-import { Favicon } from "@opencode-ai/ui/favicon"
-import { MarkedProvider } from "@opencode-ai/ui/context/marked"
-import { GlobalSyncProvider, useGlobalSync } from "./context/global-sync"
-import Layout from "@/pages/layout"
-import DirectoryLayout from "@/pages/directory-layout"
-import Session from "@/pages/session"
-import { LayoutProvider } from "./context/layout"
-import { GlobalSDKProvider } from "./context/global-sdk"
-import { SessionProvider } from "./context/session"
-import { base64Encode } from "./utils"
-import { createMemo, Show } from "solid-js"
-
-const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1"
-const port = import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"
-
-const url =
- new URLSearchParams(document.location.search).get("url") ||
- (location.hostname.includes("opencode.ai") || location.hostname.includes("localhost")
- ? `http://${host}:${port}`
- : "/")
-
-const root = document.getElementById("root")
-if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
- throw new Error(
- "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
- )
-}
-
-render(
- () => (
-
-
-
-
-
-
-
- {
- const globalSync = useGlobalSync()
- const slug = createMemo(() => base64Encode(globalSync.data.defaultProject!.worktree))
- return
- }}
- />
-
- } />
- (
-
-
-
-
-
- )}
- />
-
-
-
-
-
-
-
- ),
- root!,
-)
diff --git a/packages/desktop/src/pages/directory-layout.tsx b/packages/desktop/src/pages/directory-layout.tsx
index de16eff30..c909a373d 100644
--- a/packages/desktop/src/pages/directory-layout.tsx
+++ b/packages/desktop/src/pages/directory-layout.tsx
@@ -1,32 +1,31 @@
-import { createMemo, type ParentProps } from "solid-js"
+import { createMemo, Show, type ParentProps } from "solid-js"
import { useParams } from "@solidjs/router"
import { SDKProvider } from "@/context/sdk"
import { SyncProvider, useSync } from "@/context/sync"
import { LocalProvider } from "@/context/local"
-import { useGlobalSync } from "@/context/global-sync"
-import { base64Decode } from "@/utils"
+import { base64Decode } from "@opencode-ai/util/encode"
import { DataProvider } from "@opencode-ai/ui/context"
import { iife } from "@opencode-ai/util/iife"
export default function Layout(props: ParentProps) {
const params = useParams()
- const sync = useGlobalSync()
const directory = createMemo(() => {
- const decoded = base64Decode(params.dir!)
- return sync.data.projects.find((x) => x.worktree === decoded)?.worktree ?? "/"
+ return base64Decode(params.dir!)
})
return (
-
-
- {iife(() => {
- const sync = useSync()
- return (
-
- {props.children}
-
- )
- })}
-
-
+
+
+
+ {iife(() => {
+ const sync = useSync()
+ return (
+
+ {props.children}
+
+ )
+ })}
+
+
+
)
}
diff --git a/packages/desktop/src/pages/home.tsx b/packages/desktop/src/pages/home.tsx
index 58fcb20ce..7cd2916e8 100644
--- a/packages/desktop/src/pages/home.tsx
+++ b/packages/desktop/src/pages/home.tsx
@@ -1,21 +1,93 @@
import { useGlobalSync } from "@/context/global-sync"
-import { base64Encode } from "@/utils"
-import { For } from "solid-js"
-import { A } from "@solidjs/router"
+import { createMemo, For, Match, Show, Switch } from "solid-js"
import { Button } from "@opencode-ai/ui/button"
-import { getFilename } from "@opencode-ai/util/path"
+import { Logo } from "@opencode-ai/ui/logo"
+import { useLayout } from "@/context/layout"
+import { useNavigate } from "@solidjs/router"
+import { base64Encode } from "@opencode-ai/util/encode"
+import { Icon } from "@opencode-ai/ui/icon"
+import { usePlatform } from "@/context/platform"
+import { DateTime } from "luxon"
export default function Home() {
const sync = useGlobalSync()
+ const layout = useLayout()
+ const platform = usePlatform()
+ const navigate = useNavigate()
+ const homedir = createMemo(() => sync.data.path.home)
+
+ function openProject(directory: string) {
+ layout.projects.open(directory)
+ navigate(`/${base64Encode(directory)}`)
+ }
+
+ async function chooseProject() {
+ const result = await platform.openDirectoryPickerDialog?.({
+ title: "Open project",
+ multiple: true,
+ })
+ if (Array.isArray(result)) {
+ for (const directory of result) {
+ openProject(directory)
+ }
+ } else if (result) {
+ openProject(result)
+ }
+ }
+
return (
-
-
- {(project) => (
-
- )}
-
+
+
+
+ 0}>
+
+
+
Recent projects
+
+
+
+
+
+ (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
+ .slice(0, 5)}
+ >
+ {(project) => (
+
+ )}
+
+
+
+
+
+
+
+
+
No recent projects
+
Get started by opening a local project
+
+
+
+
+
+
+
+
)
}
diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx
index 15180c885..79470cf14 100644
--- a/packages/desktop/src/pages/layout.tsx
+++ b/packages/desktop/src/pages/layout.tsx
@@ -1,57 +1,644 @@
-import { createMemo, For, ParentProps, Show } from "solid-js"
+import { createEffect, createMemo, createSignal, For, Match, ParentProps, Show, Switch, type JSX } from "solid-js"
import { DateTime } from "luxon"
-import { A, useParams } from "@solidjs/router"
-import { useLayout } from "@/context/layout"
+import { A, useNavigate, useParams } from "@solidjs/router"
+import { useLayout, getAvatarColors } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
-import { base64Encode } from "@/utils"
-import { Mark } from "@opencode-ai/ui/logo"
+import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
+import { Avatar } from "@opencode-ai/ui/avatar"
+import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
+import { Spinner } from "@opencode-ai/ui/spinner"
import { getFilename } from "@opencode-ai/util/path"
+import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
+import { Session, Project } from "@opencode-ai/sdk/v2/client"
+import { usePlatform } from "@/context/platform"
+import { createStore, produce } from "solid-js/store"
+import {
+ DragDropProvider,
+ DragDropSensors,
+ DragOverlay,
+ SortableProvider,
+ closestCenter,
+ createSortable,
+ useDragDropContext,
+} from "@thisbeyond/solid-dnd"
+import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
+import { useProviders } from "@/hooks/use-providers"
+import { Toast } from "@opencode-ai/ui/toast"
+import { useGlobalSDK } from "@/context/global-sdk"
+import { useNotification } from "@/context/notification"
+import { Binary } from "@opencode-ai/util/binary"
+import { Header } from "@/components/header"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { DialogSelectProvider } from "@/components/dialog-select-provider"
+import { useCommand } from "@/context/command"
export default function Layout(props: ParentProps) {
+ const [store, setStore] = createStore({
+ lastSession: {} as { [directory: string]: string },
+ activeDraggable: undefined as string | undefined,
+ })
+
+ let scrollContainerRef: HTMLDivElement | undefined
+
+ function scrollToSession(sessionId: string) {
+ if (!scrollContainerRef) return
+ const element = scrollContainerRef.querySelector(`[data-session-id="${sessionId}"]`)
+ if (element) {
+ element.scrollIntoView({ block: "center", behavior: "smooth" })
+ }
+ }
+
const params = useParams()
+ const globalSDK = useGlobalSDK()
const globalSync = useGlobalSync()
const layout = useLayout()
+ const platform = usePlatform()
+ const notification = useNotification()
+ const navigate = useNavigate()
+ const providers = useProviders()
+ const dialog = useDialog()
+ const command = useCommand()
- const handleOpenProject = async () => {
- // layout.projects.open(dir.)
+ function flattenSessions(sessions: Session[]): Session[] {
+ const childrenMap = new Map
()
+ for (const session of sessions) {
+ if (session.parentID) {
+ const children = childrenMap.get(session.parentID) ?? []
+ children.push(session)
+ childrenMap.set(session.parentID, children)
+ }
+ }
+ const result: Session[] = []
+ function visit(session: Session) {
+ result.push(session)
+ for (const child of childrenMap.get(session.id) ?? []) {
+ visit(child)
+ }
+ }
+ for (const session of sessions) {
+ if (!session.parentID) visit(session)
+ }
+ return result
+ }
+
+ const currentSessions = createMemo(() => {
+ if (!params.dir) return []
+ const directory = base64Decode(params.dir)
+ return flattenSessions(globalSync.child(directory)[0].session ?? [])
+ })
+
+ function navigateSessionByOffset(offset: number) {
+ const projects = layout.projects.list()
+ if (projects.length === 0) return
+
+ const currentDirectory = params.dir ? base64Decode(params.dir) : undefined
+ const projectIndex = currentDirectory ? projects.findIndex((p) => p.worktree === currentDirectory) : -1
+
+ if (projectIndex === -1) {
+ const targetProject = offset > 0 ? projects[0] : projects[projects.length - 1]
+ if (targetProject) navigateToProject(targetProject.worktree)
+ return
+ }
+
+ const sessions = currentSessions()
+ const sessionIndex = params.id ? sessions.findIndex((s) => s.id === params.id) : -1
+
+ let targetIndex: number
+ if (sessionIndex === -1) {
+ targetIndex = offset > 0 ? 0 : sessions.length - 1
+ } else {
+ targetIndex = sessionIndex + offset
+ }
+
+ if (targetIndex >= 0 && targetIndex < sessions.length) {
+ const session = sessions[targetIndex]
+ navigateToSession(session)
+ queueMicrotask(() => scrollToSession(session.id))
+ return
+ }
+
+ const nextProjectIndex = projectIndex + (offset > 0 ? 1 : -1)
+ const nextProject = projects[nextProjectIndex]
+ if (!nextProject) return
+
+ const nextProjectSessions = flattenSessions(globalSync.child(nextProject.worktree)[0].session ?? [])
+ if (nextProjectSessions.length === 0) {
+ navigateToProject(nextProject.worktree)
+ return
+ }
+
+ const targetSession = offset > 0 ? nextProjectSessions[0] : nextProjectSessions[nextProjectSessions.length - 1]
+ navigate(`/${base64Encode(nextProject.worktree)}/session/${targetSession.id}`)
+ queueMicrotask(() => scrollToSession(targetSession.id))
+ }
+
+ async function archiveSession(session: Session) {
+ const [store, setStore] = globalSync.child(session.directory)
+ const sessions = store.session ?? []
+ const index = sessions.findIndex((s) => s.id === session.id)
+ const nextSession = sessions[index + 1] ?? sessions[index - 1]
+
+ await globalSDK.client.session.update({
+ directory: session.directory,
+ sessionID: session.id,
+ time: { archived: Date.now() },
+ })
+ setStore(
+ produce((draft) => {
+ const match = Binary.search(draft.session, session.id, (s) => s.id)
+ if (match.found) draft.session.splice(match.index, 1)
+ }),
+ )
+ if (session.id === params.id) {
+ if (nextSession) {
+ navigate(`/${params.dir}/session/${nextSession.id}`)
+ } else {
+ navigate(`/${params.dir}/session`)
+ }
+ }
+ }
+
+ command.register(() => [
+ {
+ id: "sidebar.toggle",
+ title: "Toggle sidebar",
+ category: "View",
+ keybind: "mod+b",
+ onSelect: () => layout.sidebar.toggle(),
+ },
+ ...(platform.openDirectoryPickerDialog
+ ? [
+ {
+ id: "project.open",
+ title: "Open project",
+ category: "Project",
+ keybind: "mod+o",
+ onSelect: () => chooseProject(),
+ },
+ ]
+ : []),
+ {
+ id: "provider.connect",
+ title: "Connect provider",
+ category: "Provider",
+ onSelect: () => connectProvider(),
+ },
+ {
+ id: "session.previous",
+ title: "Previous session",
+ category: "Session",
+ keybind: "alt+arrowup",
+ onSelect: () => navigateSessionByOffset(-1),
+ },
+ {
+ id: "session.next",
+ title: "Next session",
+ category: "Session",
+ keybind: "alt+arrowdown",
+ onSelect: () => navigateSessionByOffset(1),
+ },
+ {
+ id: "session.archive",
+ title: "Archive session",
+ category: "Session",
+ keybind: "mod+shift+backspace",
+ disabled: !params.dir || !params.id,
+ onSelect: () => {
+ const session = currentSessions().find((s) => s.id === params.id)
+ if (session) archiveSession(session)
+ },
+ },
+ ])
+
+ function connectProvider() {
+ dialog.show(() => )
+ }
+
+ function navigateToProject(directory: string | undefined) {
+ if (!directory) return
+ const lastSession = store.lastSession[directory]
+ navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`)
+ }
+
+ function navigateToSession(session: Session | undefined) {
+ if (!session) return
+ navigate(`/${params.dir}/session/${session?.id}`)
+ }
+
+ function openProject(directory: string, navigate = true) {
+ layout.projects.open(directory)
+ if (navigate) navigateToProject(directory)
+ }
+
+ function closeProject(directory: string) {
+ const index = layout.projects.list().findIndex((x) => x.worktree === directory)
+ const next = layout.projects.list()[index + 1]
+ layout.projects.close(directory)
+ if (next) navigateToProject(next.worktree)
+ else navigate("/")
+ }
+
+ async function chooseProject() {
+ const result = await platform.openDirectoryPickerDialog?.({
+ title: "Open project",
+ multiple: true,
+ })
+ if (Array.isArray(result)) {
+ for (const directory of result) {
+ openProject(directory, false)
+ }
+ navigateToProject(result[0])
+ } else if (result) {
+ openProject(result)
+ }
+ }
+
+ createEffect(() => {
+ if (!params.dir || !params.id) return
+ const directory = base64Decode(params.dir)
+ setStore("lastSession", directory, params.id)
+ notification.session.markViewed(params.id)
+ })
+
+ createEffect(() => {
+ const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48
+ document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`)
+ })
+
+ function getDraggableId(event: unknown): string | undefined {
+ if (typeof event !== "object" || event === null) return undefined
+ if (!("draggable" in event)) return undefined
+ const draggable = (event as { draggable?: { id?: unknown } }).draggable
+ if (!draggable) return undefined
+ return typeof draggable.id === "string" ? draggable.id : undefined
+ }
+
+ function handleDragStart(event: unknown) {
+ const id = getDraggableId(event)
+ if (!id) return
+ setStore("activeDraggable", id)
+ }
+
+ function handleDragOver(event: DragEvent) {
+ const { draggable, droppable } = event
+ if (draggable && droppable) {
+ const projects = layout.projects.list()
+ const fromIndex = projects.findIndex((p) => p.worktree === draggable.id.toString())
+ const toIndex = projects.findIndex((p) => p.worktree === droppable.id.toString())
+ if (fromIndex !== toIndex && toIndex !== -1) {
+ layout.projects.move(draggable.id.toString(), toIndex)
+ }
+ }
+ }
+
+ function handleDragEnd() {
+ setStore("activeDraggable", undefined)
+ }
+
+ const ConstrainDragXAxis = (): JSX.Element => {
+ const context = useDragDropContext()
+ if (!context) return <>>
+ const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context
+ const transformer: Transformer = {
+ id: "constrain-x-axis",
+ order: 100,
+ callback: (transform) => ({ ...transform, x: 0 }),
+ }
+ onDragStart((event) => {
+ const id = getDraggableId(event)
+ if (!id) return
+ addTransformer("draggables", id, transformer)
+ })
+ onDragEnd((event) => {
+ const id = getDraggableId(event)
+ if (!id) return
+ removeTransformer("draggables", id, transformer.id)
+ })
+ return <>>
+ }
+
+ const ProjectAvatar = (props: {
+ project: Project
+ class?: string
+ expandable?: boolean
+ notify?: boolean
+ }): JSX.Element => {
+ const notification = useNotification()
+ const notifications = createMemo(() => notification.project.unseen(props.project.worktree))
+ const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
+ const name = createMemo(() => getFilename(props.project.worktree))
+ const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)"
+ const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
+
+ return (
+
+
0 && props.notify ? { "-webkit-mask-image": mask, "mask-image": mask } : undefined
+ }
+ />
+
+
+
+ 0 && props.notify}>
+
+
+
+ )
+ }
+
+ const ProjectVisual = (props: { project: Project & { expanded: boolean }; class?: string }): JSX.Element => {
+ const name = createMemo(() => getFilename(props.project.worktree))
+ const current = createMemo(() => base64Decode(params.dir ?? ""))
+ return (
+
+
+
+
+
+
+
+
+ )
+ }
+
+ const SessionItem = (props: {
+ session: Session
+ slug: string
+ project: Project
+ depth?: number
+ childrenMap: Map
+ }): JSX.Element => {
+ const notification = useNotification()
+ const depth = props.depth ?? 0
+ const children = createMemo(() => props.childrenMap.get(props.session.id) ?? [])
+ const updated = createMemo(() => DateTime.fromMillis(props.session.time.updated))
+ const notifications = createMemo(() => notification.session.unseen(props.session.id))
+ const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
+ const isWorking = createMemo(
+ () =>
+ props.session.id !== params.id &&
+ globalSync.child(props.project.worktree)[0].session_status[props.session.id]?.type === "busy",
+ )
+ return (
+ <>
+
+
+ {(child) => (
+
+ )}
+
+ >
+ )
+ }
+
+ const SortableProject = (props: { project: Project & { expanded: boolean } }): JSX.Element => {
+ const sortable = createSortable(props.project.worktree)
+ const slug = createMemo(() => base64Encode(props.project.worktree))
+ const name = createMemo(() => getFilename(props.project.worktree))
+ const [store] = globalSync.child(props.project.worktree)
+ const sessions = createMemo(() => store.session ?? [])
+ const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
+ const childSessionsByParent = createMemo(() => {
+ const map = new Map()
+ for (const session of sessions()) {
+ if (session.parentID) {
+ const children = map.get(session.parentID) ?? []
+ children.push(session)
+ map.set(session.parentID, children)
+ }
+ }
+ return map
+ })
+ const [expanded, setExpanded] = createSignal(true)
+ return (
+ // @ts-ignore
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ const ProjectDragOverlay = (): JSX.Element => {
+ const project = createMemo(() => layout.projects.list().find((p) => p.worktree === store.activeDraggable))
+ return (
+
+ {(p) => (
+
+ )}
+
+ )
}
return (
-
-
-
+
+
+
-
)
}
diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx
index 281b6765a..390872d36 100644
--- a/packages/desktop/src/pages/session.tsx
+++ b/packages/desktop/src/pages/session.tsx
@@ -1,4 +1,4 @@
-import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo } from "solid-js"
+import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo, createEffect, on } from "solid-js"
import { useLocal, type LocalFile } from "@/context/local"
import { createStore } from "solid-js/store"
import { PromptInput } from "@/components/prompt-input"
@@ -9,12 +9,12 @@ import { Icon } from "@opencode-ai/ui/icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
+import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Tabs } from "@opencode-ai/ui/tabs"
import { Code } from "@opencode-ai/ui/code"
import { SessionTurn } from "@opencode-ai/ui/session-turn"
import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail"
import { SessionReview } from "@opencode-ai/ui/session-review"
-import { SelectDialog } from "@opencode-ai/ui/select-dialog"
import {
DragDropProvider,
DragDropSensors,
@@ -27,23 +27,324 @@ import {
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
import type { JSX } from "solid-js"
import { useSync } from "@/context/sync"
-import { useSession } from "@/context/session"
+import { useTerminal, type LocalPTY } from "@/context/terminal"
import { useLayout } from "@/context/layout"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
+import { Terminal } from "@/components/terminal"
+import { checksum } from "@opencode-ai/util/encode"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { DialogSelectFile } from "@/components/dialog-select-file"
+import { DialogSelectModel } from "@/components/dialog-select-model"
+import { useCommand } from "@/context/command"
+import { useNavigate, useParams } from "@solidjs/router"
+import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
+import { useSDK } from "@/context/sdk"
+import { usePrompt } from "@/context/prompt"
+import { extractPromptFromParts } from "@/utils/prompt"
export default function Page() {
const layout = useLayout()
const local = useLocal()
const sync = useSync()
- const session = useSession()
+ const terminal = useTerminal()
+ const dialog = useDialog()
+ const command = useCommand()
+ const params = useParams()
+ const navigate = useNavigate()
+ const sdk = useSDK()
+ const prompt = usePrompt()
+
+ const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
+ const tabs = createMemo(() => layout.tabs(sessionKey()))
+
+ const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
+ const revertMessageID = createMemo(() => info()?.revert?.messageID)
+ const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
+ const userMessages = createMemo(() =>
+ messages()
+ .filter((m) => m.role === "user")
+ .sort((a, b) => a.id.localeCompare(b.id)),
+ )
+ // Visible user messages excludes reverted messages (those >= revertMessageID)
+ const visibleUserMessages = createMemo(() => {
+ const revert = revertMessageID()
+ if (!revert) return userMessages()
+ return userMessages().filter((m) => m.id < revert)
+ })
+ const lastUserMessage = createMemo(() => visibleUserMessages()?.at(-1))
+
+ const [messageStore, setMessageStore] = createStore<{ messageId?: string }>({})
+ const activeMessage = createMemo(() => {
+ if (!messageStore.messageId) return lastUserMessage()
+ // If the stored message is no longer visible (e.g., was reverted), fall back to last visible
+ const found = visibleUserMessages()?.find((m) => m.id === messageStore.messageId)
+ return found ?? lastUserMessage()
+ })
+ const setActiveMessage = (message: UserMessage | undefined) => {
+ setMessageStore("messageId", message?.id)
+ }
+
+ function navigateMessageByOffset(offset: number) {
+ const msgs = visibleUserMessages()
+ if (msgs.length === 0) return
+
+ const current = activeMessage()
+ const currentIndex = current ? msgs.findIndex((m) => m.id === current.id) : -1
+
+ let targetIndex: number
+ if (currentIndex === -1) {
+ targetIndex = offset > 0 ? 0 : msgs.length - 1
+ } else {
+ targetIndex = currentIndex + offset
+ }
+
+ if (targetIndex < 0 || targetIndex >= msgs.length) return
+
+ setActiveMessage(msgs[targetIndex])
+ }
+
+ const last = createMemo(
+ () => messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage,
+ )
+ const model = createMemo(() =>
+ last() ? sync.data.provider.all.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined,
+ )
+ const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
+
+ const tokens = createMemo(() => {
+ if (!last()) return
+ const t = last().tokens
+ return t.input + t.output + t.reasoning + t.cache.read + t.cache.write
+ })
+
+ const context = createMemo(() => {
+ const total = tokens()
+ const limit = model()?.limit.context
+ if (!total || !limit) return 0
+ return Math.round((total / limit) * 100)
+ })
+
const [store, setStore] = createStore({
clickTimer: undefined as number | undefined,
- fileSelectOpen: false,
activeDraggable: undefined as string | undefined,
+ activeTerminalDraggable: undefined as string | undefined,
+ stepsExpanded: false,
})
let inputRef!: HTMLDivElement
- const MOD = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control"
+ createEffect(() => {
+ if (!params.id) return
+ sync.session.sync(params.id)
+ })
+
+ createEffect(() => {
+ if (layout.terminal.opened()) {
+ if (terminal.all().length === 0) {
+ terminal.new()
+ }
+ }
+ })
+
+ createEffect(
+ on(
+ () => visibleUserMessages().at(-1)?.id,
+ (lastId, prevLastId) => {
+ if (lastId && prevLastId && lastId > prevLastId) {
+ setMessageStore("messageId", undefined)
+ }
+ },
+ { defer: true },
+ ),
+ )
+
+ const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? { type: "idle" })
+
+ command.register(() => [
+ {
+ id: "session.new",
+ title: "New session",
+ description: "Create a new session",
+ category: "Session",
+ keybind: "mod+shift+s",
+ slash: "new",
+ onSelect: () => navigate(`/${params.dir}/session`),
+ },
+ {
+ id: "file.open",
+ title: "Open file",
+ description: "Search and open a file",
+ category: "File",
+ keybind: "mod+p",
+ slash: "open",
+ onSelect: () => dialog.show(() =>
),
+ },
+ // {
+ // id: "theme.toggle",
+ // title: "Toggle theme",
+ // description: "Switch between themes",
+ // category: "View",
+ // keybind: "ctrl+t",
+ // slash: "theme",
+ // onSelect: () => {
+ // const currentTheme = localStorage.getItem("theme") ?? "oc-1"
+ // const themes = ["oc-1", "oc-2-paper"]
+ // const nextTheme = themes[(themes.indexOf(currentTheme) + 1) % themes.length]
+ // localStorage.setItem("theme", nextTheme)
+ // document.documentElement.setAttribute("data-theme", nextTheme)
+ // },
+ // },
+ {
+ id: "terminal.toggle",
+ title: "Toggle terminal",
+ description: "Show or hide the terminal",
+ category: "View",
+ keybind: "ctrl+`",
+ slash: "terminal",
+ onSelect: () => layout.terminal.toggle(),
+ },
+ {
+ id: "terminal.new",
+ title: "New terminal",
+ description: "Create a new terminal tab",
+ category: "Terminal",
+ keybind: "ctrl+shift+`",
+ onSelect: () => terminal.new(),
+ },
+ {
+ id: "steps.toggle",
+ title: "Toggle steps",
+ description: "Show or hide the steps",
+ category: "View",
+ keybind: "mod+e",
+ slash: "steps",
+ disabled: !params.id,
+ onSelect: () => setStore("stepsExpanded", (x) => !x),
+ },
+ {
+ id: "message.previous",
+ title: "Previous message",
+ description: "Go to the previous user message",
+ category: "Session",
+ keybind: "mod+arrowup",
+ disabled: !params.id,
+ onSelect: () => navigateMessageByOffset(-1),
+ },
+ {
+ id: "message.next",
+ title: "Next message",
+ description: "Go to the next user message",
+ category: "Session",
+ keybind: "mod+arrowdown",
+ disabled: !params.id,
+ onSelect: () => navigateMessageByOffset(1),
+ },
+ {
+ id: "model.choose",
+ title: "Choose model",
+ description: "Select a different model",
+ category: "Model",
+ keybind: "mod+'",
+ slash: "model",
+ onSelect: () => dialog.show(() =>
),
+ },
+ {
+ id: "agent.cycle",
+ title: "Cycle agent",
+ description: "Switch to the next agent",
+ category: "Agent",
+ keybind: "mod+.",
+ slash: "agent",
+ onSelect: () => local.agent.move(1),
+ },
+ {
+ id: "session.undo",
+ title: "Undo",
+ description: "Undo the last message",
+ category: "Session",
+ keybind: "mod+z",
+ slash: "undo",
+ disabled: !params.id || visibleUserMessages().length === 0,
+ onSelect: async () => {
+ const sessionID = params.id
+ if (!sessionID) return
+ if (status()?.type !== "idle") {
+ await sdk.client.session.abort({ sessionID }).catch(() => {})
+ }
+ const revert = info()?.revert?.messageID
+ // Find the last user message that's not already reverted
+ const message = userMessages().findLast((x) => !revert || x.id < revert)
+ if (!message) return
+ await sdk.client.session.revert({ sessionID, messageID: message.id })
+ // Restore the prompt from the reverted message
+ const parts = sync.data.part[message.id]
+ if (parts) {
+ const restored = extractPromptFromParts(parts)
+ prompt.set(restored)
+ }
+ // Navigate to the message before the reverted one (which will be the new last visible message)
+ const priorMessage = userMessages().findLast((x) => x.id < message.id)
+ setActiveMessage(priorMessage)
+ },
+ },
+ {
+ id: "session.redo",
+ title: "Redo",
+ description: "Redo the last undone message",
+ category: "Session",
+ keybind: "mod+shift+z",
+ slash: "redo",
+ disabled: !params.id || !info()?.revert?.messageID,
+ onSelect: async () => {
+ const sessionID = params.id
+ if (!sessionID) return
+ const revertMessageID = info()?.revert?.messageID
+ if (!revertMessageID) return
+ const nextMessage = userMessages().find((x) => x.id > revertMessageID)
+ if (!nextMessage) {
+ // Full unrevert - restore all messages and navigate to last
+ await sdk.client.session.unrevert({ sessionID })
+ prompt.reset()
+ // Navigate to the last message (the one that was at the revert point)
+ const lastMsg = userMessages().findLast((x) => x.id >= revertMessageID)
+ setActiveMessage(lastMsg)
+ return
+ }
+ // Partial redo - move forward to next message
+ await sdk.client.session.revert({ sessionID, messageID: nextMessage.id })
+ // Navigate to the message before the new revert point
+ const priorMsg = userMessages().findLast((x) => x.id < nextMessage.id)
+ setActiveMessage(priorMsg)
+ },
+ },
+ ])
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if ((document.activeElement as HTMLElement)?.dataset?.component === "terminal") return
+ if (dialog.active) return
+
+ if (event.key === "PageUp" || event.key === "PageDown") {
+ const scrollContainer = document.querySelector('[data-slot="session-turn-content"]') as HTMLElement
+ if (scrollContainer) {
+ event.preventDefault()
+ const scrollAmount = scrollContainer.clientHeight * 0.8
+ scrollContainer.scrollBy({
+ top: event.key === "PageUp" ? -scrollAmount : scrollAmount,
+ behavior: "instant",
+ })
+ }
+ return
+ }
+
+ const focused = document.activeElement === inputRef
+ if (focused) {
+ if (event.key === "Escape") inputRef?.blur()
+ return
+ }
+
+ if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) {
+ inputRef?.focus()
+ }
+ }
onMount(() => {
document.addEventListener("keydown", handleKeyDown)
@@ -53,60 +354,6 @@ export default function Page() {
document.removeEventListener("keydown", handleKeyDown)
})
- const handleKeyDown = (event: KeyboardEvent) => {
- if (event.getModifierState(MOD) && event.shiftKey && event.key.toLowerCase() === "p") {
- event.preventDefault()
- return
- }
- if (event.getModifierState(MOD) && event.key.toLowerCase() === "p") {
- event.preventDefault()
- setStore("fileSelectOpen", true)
- return
- }
- if (event.ctrlKey && event.key.toLowerCase() === "t") {
- event.preventDefault()
- const currentTheme = localStorage.getItem("theme") ?? "oc-1"
- const themes = ["oc-1", "oc-2-paper"]
- const nextTheme = themes[(themes.indexOf(currentTheme) + 1) % themes.length]
- localStorage.setItem("theme", nextTheme)
- document.documentElement.setAttribute("data-theme", nextTheme)
- return
- }
-
- const focused = document.activeElement === inputRef
- if (focused) {
- if (event.key === "Escape") {
- inputRef?.blur()
- }
- return
- }
-
- // if (local.file.active()) {
- // const active = local.file.active()!
- // if (event.key === "Enter" && active.selection) {
- // local.context.add({
- // type: "file",
- // path: active.path,
- // selection: { ...active.selection },
- // })
- // return
- // }
- //
- // if (event.getModifierState(MOD)) {
- // if (event.key.toLowerCase() === "a") {
- // return
- // }
- // if (event.key.toLowerCase() === "c") {
- // return
- // }
- // }
- // }
-
- if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) {
- inputRef?.focus()
- }
- }
-
const resetClickTimer = () => {
if (!store.clickTimer) return
clearTimeout(store.clickTimer)
@@ -123,7 +370,6 @@ export default function Page() {
const handleTabClick = async (tab: string) => {
if (store.clickTimer) {
resetClickTimer()
- // local.file.update(file.path, { ...file, pinned: true })
} else {
if (tab.startsWith("file://")) {
local.file.open(tab.replace("file://", ""))
@@ -141,11 +387,11 @@ export default function Page() {
const handleDragOver = (event: DragEvent) => {
const { draggable, droppable } = event
if (draggable && droppable) {
- const currentTabs = session.layout.tabs.opened
+ const currentTabs = tabs().all()
const fromIndex = currentTabs?.indexOf(draggable.id.toString())
const toIndex = currentTabs?.indexOf(droppable.id.toString())
if (fromIndex !== toIndex && toIndex !== undefined) {
- session.layout.moveTab(draggable.id.toString(), toIndex)
+ tabs().move(draggable.id.toString(), toIndex)
}
}
}
@@ -154,6 +400,49 @@ export default function Page() {
setStore("activeDraggable", undefined)
}
+ const handleTerminalDragStart = (event: unknown) => {
+ const id = getDraggableId(event)
+ if (!id) return
+ setStore("activeTerminalDraggable", id)
+ }
+
+ const handleTerminalDragOver = (event: DragEvent) => {
+ const { draggable, droppable } = event
+ if (draggable && droppable) {
+ const terminals = terminal.all()
+ const fromIndex = terminals.findIndex((t: LocalPTY) => t.id === draggable.id.toString())
+ const toIndex = terminals.findIndex((t: LocalPTY) => t.id === droppable.id.toString())
+ if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
+ terminal.move(draggable.id.toString(), toIndex)
+ }
+ }
+ }
+
+ const handleTerminalDragEnd = () => {
+ setStore("activeTerminalDraggable", undefined)
+ }
+
+ const SortableTerminalTab = (props: { terminal: LocalPTY }): JSX.Element => {
+ const sortable = createSortable(props.terminal.id)
+ return (
+ // @ts-ignore
+
+
+ 1 && (
+ terminal.close(props.terminal.id)} />
+ )
+ }
+ >
+ {props.terminal.title}
+
+
+
+ )
+ }
+
const FileVisual = (props: { file: LocalFile; active?: boolean }): JSX.Element => {
return (
@@ -196,7 +485,6 @@ export default function Page() {
onTabClose: (tab: string) => void
}): JSX.Element => {
const sortable = createSortable(props.tab)
-
const [file] = createResource(
() => props.tab,
async (tab) => {
@@ -206,14 +494,17 @@ export default function Page() {
return undefined
},
)
-
return (
// @ts-ignore
props.onTabClose(props.tab)} />}
+ closeButton={
+
+ props.onTabClose(props.tab)} />
+
+ }
hideCloseButton
onClick={() => props.onTabClick(props.tab)}
>
@@ -256,311 +547,330 @@ export default function Page() {
return typeof draggable.id === "string" ? draggable.id : undefined
}
- const wide = createMemo(() => layout.review.state() === "tab" || !session.diffs().length)
+ const wide = createMemo(() => layout.review.state() === "tab" || !diffs().length)
return (
-
-
-
-
-
-
-
-
-
-
Session
-
-
- {session.usage.context() ?? 0}%
-
-
-
-
-
- }
- >
-
-
-
-
-
-
Review
-
-
- {session.info()?.summary?.files ?? 0}
-
-
-
+
+
+
+
+
+
+
+
+
+
+
Session
+
+
+ {context() ?? 0}%
+
-
-
-
- {(tab) => }
-
-
-
-
- setStore("fileSelectOpen", true)}
- />
-
-
-
-
-
-
+
+
+
+
+ }
+ >
+
+
+
+
+
+
Review
+
+
+ {info()?.summary?.files ?? 0}
+
+
+
+
+
+
+
+
+ {(tab) => }
+
+
+
+
+ dialog.show(() => )}
+ />
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+ setStore("stepsExpanded", expanded)}
+ classes={{
+ root: "pb-20 flex-1 min-w-0",
+ content: "pb-20",
+ container:
+ "w-full " +
+ (wide()
+ ? "max-w-146 mx-auto px-6"
+ : visibleUserMessages().length > 1
+ ? "pr-6 pl-18"
+ : "px-6"),
+ }}
+ />
+
+
+
+
+
+
New session
+
+
+
+ {getDirectory(sync.data.path.directory)}
+ {getFilename(sync.data.path.directory)}
+
+
+
+ {(project) => (
+
+
+
+ Last modified
+
+ {DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
+
+
+
+ )}
+
+
+
+
+
+
+
{
+ inputRef = el
}}
/>
-
-
-
-
New session
-
-
-
- {getDirectory(sync.data.path.directory)}
- {getFilename(sync.data.path.directory)}
-
-
-
-
-
- Last modified
-
- {DateTime.fromMillis(sync.data.project.time.created).toRelative()}
-
-
-
-
-
-
-
-
-
{
- inputRef = el
- }}
- />
+
+
+
+ {
+ layout.review.tab()
+ tabs().setActive("review")
+ }}
+ />
+
+ }
+ />
+
+
-
+
+
+
- {
- layout.review.tab()
- session.layout.setActiveTab("review")
- }}
- />
-
- }
+ diffs={diffs()}
+ split
/>
-
-
-
-
-
-
-
-
-
-
-
- {(tab) => {
- const [file] = createResource(
- () => tab,
- async (tab) => {
- if (tab.startsWith("file://")) {
- return local.file.node(tab.replace("file://", ""))
- }
- return undefined
- },
- )
- return (
-
-
-
- {(f) => (
-
- )}
-
-
-
- )
- }}
-
-
-
-
- {(draggedFile) => {
- const [file] = createResource(
- () => draggedFile(),
- async (tab) => {
- if (tab.startsWith("file://")) {
- return local.file.node(tab.replace("file://", ""))
- }
- return undefined
- },
- )
- return (
-
- {(f) => }
-
- )
- }}
-
-
-
-
-
-
{
- inputRef = el
- }}
- />
-
-
-
- {/* */}
-
-
- No changes
}>
-
-
- {(path) => (
- -
-
-
- )}
+
+
+
+ {(tab) => {
+ const [file] = createResource(
+ () => tab,
+ async (tab) => {
+ if (tab.startsWith("file://")) {
+ return local.file.node(tab.replace("file://", ""))
+ }
+ return undefined
+ },
+ )
+ return (
+
+
+
+ {(f) => (
+
+ )}
+
+
+
+ )
+ }}
-
+
+
+
+ {(draggedFile) => {
+ const [file] = createResource(
+ () => draggedFile(),
+ async (tab) => {
+ if (tab.startsWith("file://")) {
+ return local.file.node(tab.replace("file://", ""))
+ }
+ return undefined
+ },
+ )
+ return (
+
+ {(f) => }
+
+ )
+ }}
+
+
+
+
+
+
{
+ inputRef = el
+ }}
+ />
+
-
- x}
- onOpenChange={(open) => setStore("fileSelectOpen", open)}
- onSelect={(x) => {
- if (x) {
- local.file.open(x)
- return session.layout.openTab("file://" + x)
- }
- return undefined
- }}
+
+
- {(i) => (
-
-
-
-
-
- {getDirectory(i)}
-
-
{getFilename(i)}
+
+
+
+
+
+
+ t.id)}>
+ {(pty) => }
+
+
+
+
+
-
-
-
- )}
-
+
+
+ {(pty) => (
+
+ terminal.clone(pty.id)} />
+
+ )}
+
+
+
+
+ {(draggedId) => {
+ const pty = createMemo(() => terminal.all().find((t: LocalPTY) => t.id === draggedId()))
+ return (
+
+ {(t) => (
+
+ {t().title}
+
+ )}
+
+ )
+ }}
+
+
+
+
)
diff --git a/packages/desktop/src/sst-env.d.ts b/packages/desktop/src/sst-env.d.ts
index 1b1683a1e..47a8fbec7 100644
--- a/packages/desktop/src/sst-env.d.ts
+++ b/packages/desktop/src/sst-env.d.ts
@@ -2,7 +2,9 @@
/* tslint:disable */
/* eslint-disable */
///
-interface ImportMetaEnv {}
+interface ImportMetaEnv {
+
+}
interface ImportMeta {
readonly env: ImportMetaEnv
-}
+}
\ No newline at end of file
diff --git a/packages/desktop/src/utils/encode.ts b/packages/desktop/src/utils/encode.ts
deleted file mode 100644
index 265bba5c4..000000000
--- a/packages/desktop/src/utils/encode.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export function base64Encode(value: string) {
- return btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
-}
-
-export function base64Decode(value: string) {
- return atob(value.replace(/-/g, "+").replace(/_/g, "/"))
-}
diff --git a/packages/desktop/src/utils/index.ts b/packages/desktop/src/utils/index.ts
index e50efe837..d87053269 100644
--- a/packages/desktop/src/utils/index.ts
+++ b/packages/desktop/src/utils/index.ts
@@ -1,2 +1 @@
export * from "./dom"
-export * from "./encode"
diff --git a/packages/desktop/src/utils/prompt.ts b/packages/desktop/src/utils/prompt.ts
new file mode 100644
index 000000000..45c5ce1f3
--- /dev/null
+++ b/packages/desktop/src/utils/prompt.ts
@@ -0,0 +1,47 @@
+import type { Part, TextPart, FilePart } from "@opencode-ai/sdk/v2"
+import type { Prompt, FileAttachmentPart } from "@/context/prompt"
+
+/**
+ * Extract prompt content from message parts for restoring into the prompt input.
+ * This is used by undo to restore the original user prompt.
+ */
+export function extractPromptFromParts(parts: Part[]): Prompt {
+ const result: Prompt = []
+ let position = 0
+
+ for (const part of parts) {
+ if (part.type === "text") {
+ const textPart = part as TextPart
+ if (!textPart.synthetic && textPart.text) {
+ result.push({
+ type: "text",
+ content: textPart.text,
+ start: position,
+ end: position + textPart.text.length,
+ })
+ position += textPart.text.length
+ }
+ } else if (part.type === "file") {
+ const filePart = part as FilePart
+ if (filePart.source?.type === "file") {
+ const path = filePart.source.path
+ const content = "@" + path
+ const attachment: FileAttachmentPart = {
+ type: "file",
+ path,
+ content,
+ start: position,
+ end: position + content.length,
+ }
+ result.push(attachment)
+ position += content.length
+ }
+ }
+ }
+
+ if (result.length === 0) {
+ result.push({ type: "text", content: "", start: 0, end: 0 })
+ }
+
+ return result
+}
diff --git a/packages/desktop/sst-env.d.ts b/packages/desktop/sst-env.d.ts
index 0397645b5..b6a7e9066 100644
--- a/packages/desktop/sst-env.d.ts
+++ b/packages/desktop/sst-env.d.ts
@@ -6,4 +6,4 @@
///
import "sst"
-export {}
+export {}
\ No newline at end of file
diff --git a/packages/desktop/tsconfig.json b/packages/desktop/tsconfig.json
index 82541a6d3..db04f79ca 100644
--- a/packages/desktop/tsconfig.json
+++ b/packages/desktop/tsconfig.json
@@ -1,6 +1,7 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
+ "composite": true,
"target": "ESNext",
"module": "ESNext",
"skipLibCheck": true,
@@ -11,10 +12,13 @@
"jsxImportSource": "solid-js",
"allowJs": true,
"strict": true,
- "noEmit": true,
+ "noEmit": false,
+ "emitDeclarationOnly": true,
+ "outDir": "node_modules/.ts-dist",
"isolatedModules": true,
"paths": {
"@/*": ["./src/*"]
}
- }
+ },
+ "exclude": ["dist", "ts-dist"]
}
diff --git a/packages/desktop/vite.config.ts b/packages/desktop/vite.config.ts
index ff18b1de3..a388884cd 100644
--- a/packages/desktop/vite.config.ts
+++ b/packages/desktop/vite.config.ts
@@ -1,15 +1,8 @@
import { defineConfig } from "vite"
-import solidPlugin from "vite-plugin-solid"
-import tailwindcss from "@tailwindcss/vite"
-import path from "path"
+import desktopPlugin from "./vite"
export default defineConfig({
- resolve: {
- alias: {
- "@": path.resolve(__dirname, "./src"),
- },
- },
- plugins: [tailwindcss(), solidPlugin()] as any,
+ plugins: [desktopPlugin] as any,
server: {
host: "0.0.0.0",
allowedHosts: true,
diff --git a/packages/desktop/vite.js b/packages/desktop/vite.js
new file mode 100644
index 000000000..6b8fd6137
--- /dev/null
+++ b/packages/desktop/vite.js
@@ -0,0 +1,26 @@
+import solidPlugin from "vite-plugin-solid"
+import tailwindcss from "@tailwindcss/vite"
+import { fileURLToPath } from "url"
+
+/**
+ * @type {import("vite").PluginOption}
+ */
+export default [
+ {
+ name: "opencode-desktop:config",
+ config() {
+ return {
+ resolve: {
+ alias: {
+ "@": fileURLToPath(new URL("./src", import.meta.url)),
+ },
+ },
+ worker: {
+ format: "es",
+ },
+ }
+ },
+ },
+ tailwindcss(),
+ solidPlugin(),
+]
diff --git a/packages/docs/LICENSE b/packages/docs/LICENSE
new file mode 100644
index 000000000..541137427
--- /dev/null
+++ b/packages/docs/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Mintlify
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/packages/docs/README.md b/packages/docs/README.md
new file mode 100644
index 000000000..17956df32
--- /dev/null
+++ b/packages/docs/README.md
@@ -0,0 +1,44 @@
+# Mintlify Starter Kit
+
+Use the starter kit to get your docs deployed and ready to customize.
+
+Click the green **Use this template** button at the top of this repo to copy the Mintlify starter kit. The starter kit contains examples with
+
+- Guide pages
+- Navigation
+- Customizations
+- API reference pages
+- Use of popular components
+
+**[Follow the full quickstart guide](https://starter.mintlify.com/quickstart)**
+
+## Development
+
+Install the [Mintlify CLI](https://www.npmjs.com/package/mint) to preview your documentation changes locally. To install, use the following command:
+
+```
+npm i -g mint
+```
+
+Run the following command at the root of your documentation, where your `docs.json` is located:
+
+```
+mint dev
+```
+
+View your local preview at `http://localhost:3000`.
+
+## Publishing changes
+
+Install our GitHub app from your [dashboard](https://dashboard.mintlify.com/settings/organization/github-app) to propagate changes from your repo to your deployment. Changes are deployed to production automatically after pushing to the default branch.
+
+## Need help?
+
+### Troubleshooting
+
+- If your dev environment isn't running: Run `mint update` to ensure you have the most recent version of the CLI.
+- If a page loads as a 404: Make sure you are running in a folder with a valid `docs.json`.
+
+### Resources
+
+- [Mintlify documentation](https://mintlify.com/docs)
diff --git a/packages/docs/ai-tools/claude-code.mdx b/packages/docs/ai-tools/claude-code.mdx
new file mode 100644
index 000000000..4039c6e0e
--- /dev/null
+++ b/packages/docs/ai-tools/claude-code.mdx
@@ -0,0 +1,83 @@
+---
+title: "Claude Code setup"
+description: "Configure Claude Code for your documentation workflow"
+icon: "asterisk"
+---
+
+Claude Code is Anthropic's official CLI tool. This guide will help you set up Claude Code to help you write and maintain your documentation.
+
+## Prerequisites
+
+- Active Claude subscription (Pro, Max, or API access)
+
+## Setup
+
+1. Install Claude Code globally:
+
+```bash
+npm install -g @anthropic-ai/claude-code
+```
+
+2. Navigate to your docs directory.
+3. (Optional) Add the `CLAUDE.md` file below to your project.
+4. Run `claude` to start.
+
+## Create `CLAUDE.md`
+
+Create a `CLAUDE.md` file at the root of your documentation repository to train Claude Code on your specific documentation standards:
+
+```markdown
+# Mintlify documentation
+
+## Working relationship
+
+- You can push back on ideas-this can lead to better documentation. Cite sources and explain your reasoning when you do so
+- ALWAYS ask for clarification rather than making assumptions
+- NEVER lie, guess, or make up information
+
+## Project context
+
+- Format: MDX files with YAML frontmatter
+- Config: docs.json for navigation, theme, settings
+- Components: Mintlify components
+
+## Content strategy
+
+- Document just enough for user success - not too much, not too little
+- Prioritize accuracy and usability of information
+- Make content evergreen when possible
+- Search for existing information before adding new content. Avoid duplication unless it is done for a strategic reason
+- Check existing patterns for consistency
+- Start by making the smallest reasonable changes
+
+## Frontmatter requirements for pages
+
+- title: Clear, descriptive page title
+- description: Concise summary for SEO/navigation
+
+## Writing standards
+
+- Second-person voice ("you")
+- Prerequisites at start of procedural content
+- Test all code examples before publishing
+- Match style and formatting of existing pages
+- Include both basic and advanced use cases
+- Language tags on all code blocks
+- Alt text on all images
+- Relative paths for internal links
+
+## Git workflow
+
+- NEVER use --no-verify when committing
+- Ask how to handle uncommitted changes before starting
+- Create a new branch when no clear branch exists for changes
+- Commit frequently throughout development
+- NEVER skip or disable pre-commit hooks
+
+## Do not
+
+- Skip frontmatter on any MDX file
+- Use absolute URLs for internal links
+- Include untested code examples
+- Make assumptions - always ask for clarification
+```
diff --git a/packages/docs/ai-tools/cursor.mdx b/packages/docs/ai-tools/cursor.mdx
new file mode 100644
index 000000000..d05882919
--- /dev/null
+++ b/packages/docs/ai-tools/cursor.mdx
@@ -0,0 +1,423 @@
+---
+title: "Cursor setup"
+description: "Configure Cursor for your documentation workflow"
+icon: "arrow-pointer"
+---
+
+Use Cursor to help write and maintain your documentation. This guide shows how to configure Cursor for better results on technical writing tasks and using Mintlify components.
+
+## Prerequisites
+
+- Cursor editor installed
+- Access to your documentation repository
+
+## Project rules
+
+Create project rules that all team members can use. In your documentation repository root:
+
+```bash
+mkdir -p .cursor
+```
+
+Create `.cursor/rules.md`:
+
+````markdown
+# Mintlify technical writing rule
+
+You are an AI writing assistant specialized in creating exceptional technical documentation using Mintlify components and following industry-leading technical writing practices.
+
+## Core writing principles
+
+### Language and style requirements
+
+- Use clear, direct language appropriate for technical audiences
+- Write in second person ("you") for instructions and procedures
+- Use active voice over passive voice
+- Employ present tense for current states, future tense for outcomes
+- Avoid jargon unless necessary and define terms when first used
+- Maintain consistent terminology throughout all documentation
+- Keep sentences concise while providing necessary context
+- Use parallel structure in lists, headings, and procedures
+
+### Content organization standards
+
+- Lead with the most important information (inverted pyramid structure)
+- Use progressive disclosure: basic concepts before advanced ones
+- Break complex procedures into numbered steps
+- Include prerequisites and context before instructions
+- Provide expected outcomes for each major step
+- Use descriptive, keyword-rich headings for navigation and SEO
+- Group related information logically with clear section breaks
+
+### User-centered approach
+
+- Focus on user goals and outcomes rather than system features
+- Anticipate common questions and address them proactively
+- Include troubleshooting for likely failure points
+- Write for scannability with clear headings, lists, and white space
+- Include verification steps to confirm success
+
+## Mintlify component reference
+
+### Callout components
+
+#### Note - Additional helpful information
+
+
+Supplementary information that supports the main content without interrupting flow
+
+
+#### Tip - Best practices and pro tips
+
+
+Expert advice, shortcuts, or best practices that enhance user success
+
+
+#### Warning - Important cautions
+
+
+Critical information about potential issues, breaking changes, or destructive actions
+
+
+#### Info - Neutral contextual information
+
+
+Background information, context, or neutral announcements
+
+
+#### Check - Success confirmations
+
+
+Positive confirmations, successful completions, or achievement indicators
+
+
+### Code components
+
+#### Single code block
+
+Example of a single code block:
+
+```javascript config.js
+const apiConfig = {
+ baseURL: "https://api.example.com",
+ timeout: 5000,
+ headers: {
+ Authorization: `Bearer ${process.env.API_TOKEN}`,
+ },
+}
+```
+
+#### Code group with multiple languages
+
+Example of a code group:
+
+
+```javascript Node.js
+const response = await fetch('/api/endpoint', {
+ headers: { Authorization: `Bearer ${apiKey}` }
+});
+```
+
+```python Python
+import requests
+response = requests.get('/api/endpoint',
+ headers={'Authorization': f'Bearer {api_key}'})
+```
+
+```curl cURL
+curl -X GET '/api/endpoint' \
+ -H 'Authorization: Bearer YOUR_API_KEY'
+```
+
+
+
+#### Request/response examples
+
+Example of request/response documentation:
+
+
+```bash cURL
+curl -X POST 'https://api.example.com/users' \
+ -H 'Content-Type: application/json' \
+ -d '{"name": "John Doe", "email": "john@example.com"}'
+```
+
+
+
+```json Success
+{
+ "id": "user_123",
+ "name": "John Doe",
+ "email": "john@example.com",
+ "created_at": "2024-01-15T10:30:00Z"
+}
+```
+
+
+### Structural components
+
+#### Steps for procedures
+
+Example of step-by-step instructions:
+
+
+
+ Run `npm install` to install required packages.
+
+
+ Verify installation by running `npm list`.
+
+
+
+
+ Create a `.env` file with your API credentials.
+
+ ```bash
+ API_KEY=your_api_key_here
+ ```
+
+
+ Never commit API keys to version control.
+
+
+
+
+#### Tabs for alternative content
+
+Example of tabbed content:
+
+
+
+ ```bash
+ brew install node
+ npm install -g package-name
+ ```
+
+
+
+ ```powershell
+ choco install nodejs
+ npm install -g package-name
+ ```
+
+
+
+ ```bash
+ sudo apt install nodejs npm
+ npm install -g package-name
+ ```
+
+
+
+#### Accordions for collapsible content
+
+Example of accordion groups:
+
+
+
+ - **Firewall blocking**: Ensure ports 80 and 443 are open
+ - **Proxy configuration**: Set HTTP_PROXY environment variable
+ - **DNS resolution**: Try using 8.8.8.8 as DNS server
+
+
+
+ ```javascript
+ const config = {
+ performance: { cache: true, timeout: 30000 },
+ security: { encryption: 'AES-256' }
+ };
+ ```
+
+
+
+### Cards and columns for emphasizing information
+
+Example of cards and card groups:
+
+
+Complete walkthrough from installation to your first API call in under 10 minutes.
+
+
+
+
+ Learn how to authenticate requests using API keys or JWT tokens.
+
+
+
+ Understand rate limits and best practices for high-volume usage.
+
+
+
+### API documentation components
+
+#### Parameter fields
+
+Example of parameter documentation:
+
+
+Unique identifier for the user. Must be a valid UUID v4 format.
+
+
+
+User's email address. Must be valid and unique within the system.
+
+
+
+Maximum number of results to return. Range: 1-100.
+
+
+
+Bearer token for API authentication. Format: `Bearer YOUR_API_KEY`
+
+
+#### Response fields
+
+Example of response field documentation:
+
+
+Unique identifier assigned to the newly created user.
+
+
+
+ISO 8601 formatted timestamp of when the user was created.
+
+
+
+List of permission strings assigned to this user.
+
+
+#### Expandable nested fields
+
+Example of nested field documentation:
+
+
+Complete user object with all associated data.
+
+
+
+ User profile information including personal details.
+
+
+
+ User's first name as entered during registration.
+
+
+
+ URL to user's profile picture. Returns null if no avatar is set.
+
+
+
+
+
+
+### Media and advanced components
+
+#### Frames for images
+
+Wrap all images in frames:
+
+
+
+
+
+
+
+
+
+#### Videos
+
+Use the HTML video element for self-hosted video content:
+
+
+
+Embed YouTube videos using iframe elements:
+
+
+
+#### Tooltips
+
+Example of tooltip usage:
+
+
+API
+
+
+#### Updates
+
+Use updates for changelogs:
+
+
+## New features
+- Added bulk user import functionality
+- Improved error messages with actionable suggestions
+
+## Bug fixes
+
+- Fixed pagination issue with large datasets
+- Resolved authentication timeout problems
+
+
+## Required page structure
+
+Every documentation page must begin with YAML frontmatter:
+
+```yaml
+---
+title: "Clear, specific, keyword-rich title"
+description: "Concise description explaining page purpose and value"
+---
+```
+
+## Content quality standards
+
+### Code examples requirements
+
+- Always include complete, runnable examples that users can copy and execute
+- Show proper error handling and edge case management
+- Use realistic data instead of placeholder values
+- Include expected outputs and results for verification
+- Test all code examples thoroughly before publishing
+- Specify language and include filename when relevant
+- Add explanatory comments for complex logic
+- Never include real API keys or secrets in code examples
+
+### API documentation requirements
+
+- Document all parameters including optional ones with clear descriptions
+- Show both success and error response examples with realistic data
+- Include rate limiting information with specific limits
+- Provide authentication examples showing proper format
+- Explain all HTTP status codes and error handling
+- Cover complete request/response cycles
+
+### Accessibility requirements
+
+- Include descriptive alt text for all images and diagrams
+- Use specific, actionable link text instead of "click here"
+- Ensure proper heading hierarchy starting with H2
+- Provide keyboard navigation considerations
+- Use sufficient color contrast in examples and visuals
+- Structure content for easy scanning with headers and lists
+
+## Component selection logic
+
+- Use **Steps** for procedures and sequential instructions
+- Use **Tabs** for platform-specific content or alternative approaches
+- Use **CodeGroup** when showing the same concept in multiple programming languages
+- Use **Accordions** for progressive disclosure of information
+- Use **RequestExample/ResponseExample** specifically for API endpoint documentation
+- Use **ParamField** for API parameters, **ResponseField** for API responses
+- Use **Expandable** for nested object properties or hierarchical information
+````
diff --git a/packages/docs/ai-tools/windsurf.mdx b/packages/docs/ai-tools/windsurf.mdx
new file mode 100644
index 000000000..310c81d5f
--- /dev/null
+++ b/packages/docs/ai-tools/windsurf.mdx
@@ -0,0 +1,96 @@
+---
+title: "Windsurf setup"
+description: "Configure Windsurf for your documentation workflow"
+icon: "water"
+---
+
+Configure Windsurf's Cascade AI assistant to help you write and maintain documentation. This guide shows how to set up Windsurf specifically for your Mintlify documentation workflow.
+
+## Prerequisites
+
+- Windsurf editor installed
+- Access to your documentation repository
+
+## Workspace rules
+
+Create workspace rules that provide Windsurf with context about your documentation project and standards.
+
+Create `.windsurf/rules.md` in your project root:
+
+````markdown
+# Mintlify technical writing rule
+
+## Project context
+
+- This is a documentation project on the Mintlify platform
+- We use MDX files with YAML frontmatter
+- Navigation is configured in `docs.json`
+- We follow technical writing best practices
+
+## Writing standards
+
+- Use second person ("you") for instructions
+- Write in active voice and present tense
+- Start procedures with prerequisites
+- Include expected outcomes for major steps
+- Use descriptive, keyword-rich headings
+- Keep sentences concise but informative
+
+## Required page structure
+
+Every page must start with frontmatter:
+
+```yaml
+---
+title: "Clear, specific title"
+description: "Concise description for SEO and navigation"
+---
+```
+
+## Mintlify components
+
+### Callouts
+
+- `` for helpful supplementary information
+- `` for important cautions and breaking changes
+- `` for best practices and expert advice
+- `` for neutral contextual information
+- `` for success confirmations
+
+### Code examples
+
+- When appropriate, include complete, runnable examples
+- Use `` for multiple language examples
+- Specify language tags on all code blocks
+- Include realistic data, not placeholders
+- Use `` and `` for API docs
+
+### Procedures
+
+- Use `` component for sequential instructions
+- Include verification steps with `` components when relevant
+- Break complex procedures into smaller steps
+
+### Content organization
+
+- Use `` for platform-specific content
+- Use `` for progressive disclosure
+- Use `` and `` for highlighting content
+- Wrap images in `` components with descriptive alt text
+
+## API documentation requirements
+
+- Document all parameters with ``
+- Show response structure with ``
+- Include both success and error examples
+- Use `` for nested object properties
+- Always include authentication examples
+
+## Quality standards
+
+- Test all code examples before publishing
+- Use relative paths for internal links
+- Include alt text for all images
+- Ensure proper heading hierarchy (start with h2)
+- Check existing patterns for consistency
+````
diff --git a/packages/docs/development.mdx b/packages/docs/development.mdx
new file mode 100644
index 000000000..432ef80e4
--- /dev/null
+++ b/packages/docs/development.mdx
@@ -0,0 +1,96 @@
+---
+title: "Development"
+description: "Preview changes locally to update your docs"
+---
+
+**Prerequisites**: - Node.js version 19 or higher - A docs repository with a `docs.json` file
+
+Follow these steps to install and run Mintlify on your operating system.
+
+
+
+
+```bash
+npm i -g mint
+```
+
+
+
+
+
+Navigate to your docs directory where your `docs.json` file is located, and run the following command:
+
+```bash
+mint dev
+```
+
+A local preview of your documentation will be available at `http://localhost:3000`.
+
+
+
+
+## Custom ports
+
+By default, Mintlify uses port 3000. You can customize the port Mintlify runs on by using the `--port` flag. For example, to run Mintlify on port 3333, use this command:
+
+```bash
+mint dev --port 3333
+```
+
+If you attempt to run Mintlify on a port that's already in use, it will use the next available port:
+
+```md
+Port 3000 is already in use. Trying 3001 instead.
+```
+
+## Mintlify versions
+
+Please note that each CLI release is associated with a specific version of Mintlify. If your local preview does not align with the production version, please update the CLI:
+
+```bash
+npm mint update
+```
+
+## Validating links
+
+The CLI can assist with validating links in your documentation. To identify any broken links, use the following command:
+
+```bash
+mint broken-links
+```
+
+## Deployment
+
+If the deployment is successful, you should see the following:
+
+
+
+
+
+## Code formatting
+
+We suggest using extensions on your IDE to recognize and format MDX. If you're a VSCode user, consider the [MDX VSCode extension](https://marketplace.visualstudio.com/items?itemName=unifiedjs.vscode-mdx) for syntax highlighting, and [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) for code formatting.
+
+## Troubleshooting
+
+
+
+
+ This may be due to an outdated version of node. Try the following:
+ 1. Remove the currently-installed version of the CLI: `npm remove -g mint`
+ 2. Upgrade to Node v19 or higher.
+ 3. Reinstall the CLI: `npm i -g mint`
+
+
+
+
+
+ Solution: Go to the root of your device and delete the `~/.mintlify` folder. Then run `mint dev` again.
+
+
+
+Curious about what changed in the latest CLI version? Check out the [CLI changelog](https://www.npmjs.com/package/mintlify?activeTab=versions).
diff --git a/packages/docs/docs.json b/packages/docs/docs.json
new file mode 100644
index 000000000..4461f8253
--- /dev/null
+++ b/packages/docs/docs.json
@@ -0,0 +1,53 @@
+{
+ "$schema": "https://mintlify.com/docs.json",
+ "theme": "mint",
+ "name": "@opencode-ai/docs",
+ "colors": {
+ "primary": "#16A34A",
+ "light": "#07C983",
+ "dark": "#15803D"
+ },
+ "favicon": "/favicon.svg",
+ "navigation": {
+ "tabs": [
+ {
+ "tab": "SDK",
+ "groups": [
+ {
+ "group": "Getting started",
+ "pages": ["index", "quickstart", "development"],
+ "openapi": "https://opencode.ai/openapi.json"
+ }
+ ]
+ }
+ ],
+ "global": {}
+ },
+ "logo": {
+ "light": "/logo/light.svg",
+ "dark": "/logo/dark.svg"
+ },
+ "navbar": {
+ "links": [
+ {
+ "label": "Support",
+ "href": "mailto:hi@mintlify.com"
+ }
+ ],
+ "primary": {
+ "type": "button",
+ "label": "Dashboard",
+ "href": "https://dashboard.mintlify.com"
+ }
+ },
+ "contextual": {
+ "options": ["copy", "view", "chatgpt", "claude", "perplexity", "mcp", "cursor", "vscode"]
+ },
+ "footer": {
+ "socials": {
+ "x": "https://x.com/mintlify",
+ "github": "https://github.com/mintlify",
+ "linkedin": "https://linkedin.com/company/mintlify"
+ }
+ }
+}
diff --git a/packages/docs/essentials/code.mdx b/packages/docs/essentials/code.mdx
new file mode 100644
index 000000000..7a0465447
--- /dev/null
+++ b/packages/docs/essentials/code.mdx
@@ -0,0 +1,35 @@
+---
+title: "Code blocks"
+description: "Display inline code and code blocks"
+icon: "code"
+---
+
+## Inline code
+
+To denote a `word` or `phrase` as code, enclose it in backticks (`).
+
+```
+To denote a `word` or `phrase` as code, enclose it in backticks (`).
+```
+
+## Code blocks
+
+Use [fenced code blocks](https://www.markdownguide.org/extended-syntax/#fenced-code-blocks) by enclosing code in three backticks and follow the leading ticks with the programming language of your snippet to get syntax highlighting. Optionally, you can also write the name of your code after the programming language.
+
+```java HelloWorld.java
+class HelloWorld {
+ public static void main(String[] args) {
+ System.out.println("Hello, World!");
+ }
+}
+```
+
+````md
+```java HelloWorld.java
+class HelloWorld {
+ public static void main(String[] args) {
+ System.out.println("Hello, World!");
+ }
+}
+```
+````
diff --git a/packages/docs/essentials/images.mdx b/packages/docs/essentials/images.mdx
new file mode 100644
index 000000000..f2a10d253
--- /dev/null
+++ b/packages/docs/essentials/images.mdx
@@ -0,0 +1,56 @@
+---
+title: "Images and embeds"
+description: "Add image, video, and other HTML elements"
+icon: "image"
+---
+
+
+
+## Image
+
+### Using Markdown
+
+The [markdown syntax](https://www.markdownguide.org/basic-syntax/#images) lets you add images using the following code
+
+```md
+
+```
+
+Note that the image file size must be less than 5MB. Otherwise, we recommend hosting on a service like [Cloudinary](https://cloudinary.com/) or [S3](https://aws.amazon.com/s3/). You can then use that URL and embed.
+
+### Using embeds
+
+To get more customizability with images, you can also use [embeds](/writing-content/embed) to add images
+
+```html
+
+```
+
+## Embeds and HTML elements
+
+
+
+
+
+
+
+Mintlify supports [HTML tags in Markdown](https://www.markdownguide.org/basic-syntax/#html). This is helpful if you prefer HTML tags to Markdown syntax, and lets you create documentation with infinite flexibility.
+
+
+
+### iFrames
+
+Loads another HTML page within the document. Most commonly used for embedding videos.
+
+```html
+
+```
diff --git a/packages/docs/essentials/markdown.mdx b/packages/docs/essentials/markdown.mdx
new file mode 100644
index 000000000..0ca5b8250
--- /dev/null
+++ b/packages/docs/essentials/markdown.mdx
@@ -0,0 +1,88 @@
+---
+title: "Markdown syntax"
+description: "Text, title, and styling in standard markdown"
+icon: "text-size"
+---
+
+## Titles
+
+Best used for section headers.
+
+```md
+## Titles
+```
+
+### Subtitles
+
+Best used for subsection headers.
+
+```md
+### Subtitles
+```
+
+
+
+Each **title** and **subtitle** creates an anchor and also shows up on the table of contents on the right.
+
+
+
+## Text formatting
+
+We support most markdown formatting. Simply add `**`, `_`, or `~` around text to format it.
+
+| Style | How to write it | Result |
+| ------------- | ----------------- | --------------- |
+| Bold | `**bold**` | **bold** |
+| Italic | `_italic_` | _italic_ |
+| Strikethrough | `~strikethrough~` | ~strikethrough~ |
+
+You can combine these. For example, write `**_bold and italic_**` to get **_bold and italic_** text.
+
+You need to use HTML to write superscript and subscript text. That is, add `` or `` around your text.
+
+| Text Size | How to write it | Result |
+| ----------- | ------------------------ | ---------------------- |
+| Superscript | `superscript` | superscript |
+| Subscript | `subscript` | subscript |
+
+## Linking to pages
+
+You can add a link by wrapping text in `[]()`. You would write `[link to google](https://google.com)` to [link to google](https://google.com).
+
+Links to pages in your docs need to be root-relative. Basically, you should include the entire folder path. For example, `[link to text](/writing-content/text)` links to the page "Text" in our components section.
+
+Relative links like `[link to text](../text)` will open slower because we cannot optimize them as easily.
+
+## Blockquotes
+
+### Singleline
+
+To create a blockquote, add a `>` in front of a paragraph.
+
+> Dorothy followed her through many of the beautiful rooms in her castle.
+
+```md
+> Dorothy followed her through many of the beautiful rooms in her castle.
+```
+
+### Multiline
+
+> Dorothy followed her through many of the beautiful rooms in her castle.
+>
+> The Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood.
+
+```md
+> Dorothy followed her through many of the beautiful rooms in her castle.
+>
+> The Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood.
+```
+
+### LaTeX
+
+Mintlify supports [LaTeX](https://www.latex-project.org) through the Latex component.
+
+8 x (vk x H1 - H2) = (0,1)
+
+```md
+8 x (vk x H1 - H2) = (0,1)
+```
diff --git a/packages/docs/essentials/navigation.mdx b/packages/docs/essentials/navigation.mdx
new file mode 100644
index 000000000..a6a309004
--- /dev/null
+++ b/packages/docs/essentials/navigation.mdx
@@ -0,0 +1,87 @@
+---
+title: "Navigation"
+description: "The navigation field in docs.json defines the pages that go in the navigation menu"
+icon: "map"
+---
+
+The navigation menu is the list of links on every website.
+
+You will likely update `docs.json` every time you add a new page. Pages do not show up automatically.
+
+## Navigation syntax
+
+Our navigation syntax is recursive which means you can make nested navigation groups. You don't need to include `.mdx` in page names.
+
+
+
+```json Regular Navigation
+"navigation": {
+ "tabs": [
+ {
+ "tab": "Docs",
+ "groups": [
+ {
+ "group": "Getting Started",
+ "pages": ["quickstart"]
+ }
+ ]
+ }
+ ]
+}
+```
+
+```json Nested Navigation
+"navigation": {
+ "tabs": [
+ {
+ "tab": "Docs",
+ "groups": [
+ {
+ "group": "Getting Started",
+ "pages": [
+ "quickstart",
+ {
+ "group": "Nested Reference Pages",
+ "pages": ["nested-reference-page"]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
+```
+
+
+
+## Folders
+
+Simply put your MDX files in folders and update the paths in `docs.json`.
+
+For example, to have a page at `https://yoursite.com/your-folder/your-page` you would make a folder called `your-folder` containing an MDX file called `your-page.mdx`.
+
+
+
+You cannot use `api` for the name of a folder unless you nest it inside another folder. Mintlify uses Next.js which reserves the top-level `api` folder for internal server calls. A folder name such as `api-reference` would be accepted.
+
+
+
+```json Navigation With Folder
+"navigation": {
+ "tabs": [
+ {
+ "tab": "Docs",
+ "groups": [
+ {
+ "group": "Group Name",
+ "pages": ["your-folder/your-page"]
+ }
+ ]
+ }
+ ]
+}
+```
+
+## Hidden pages
+
+MDX files not included in `docs.json` will not show up in the sidebar but are accessible through the search bar and by linking directly to them.
diff --git a/packages/docs/essentials/reusable-snippets.mdx b/packages/docs/essentials/reusable-snippets.mdx
new file mode 100644
index 000000000..a26ab89a3
--- /dev/null
+++ b/packages/docs/essentials/reusable-snippets.mdx
@@ -0,0 +1,112 @@
+---
+title: "Reusable snippets"
+description: "Reusable, custom snippets to keep content in sync"
+icon: "recycle"
+---
+
+import SnippetIntro from "/snippets/snippet-intro.mdx"
+
+
+
+## Creating a custom snippet
+
+**Pre-condition**: You must create your snippet file in the `snippets` directory.
+
+
+ Any page in the `snippets` directory will be treated as a snippet and will not be rendered into a standalone page. If
+ you want to create a standalone page from the snippet, import the snippet into another file and call it as a
+ component.
+
+
+### Default export
+
+1. Add content to your snippet file that you want to re-use across multiple
+ locations. Optionally, you can add variables that can be filled in via props
+ when you import the snippet.
+
+```mdx snippets/my-snippet.mdx
+Hello world! This is my content I want to reuse across pages. My keyword of the
+day is {word}.
+```
+
+
+ The content that you want to reuse must be inside the `snippets` directory in order for the import to work.
+
+
+2. Import the snippet into your destination file.
+
+```mdx destination-file.mdx
+---
+title: My title
+description: My Description
+---
+
+import MySnippet from "/snippets/path/to/my-snippet.mdx"
+
+## Header
+
+Lorem impsum dolor sit amet.
+
+
+```
+
+### Reusable variables
+
+1. Export a variable from your snippet file:
+
+```mdx snippets/path/to/custom-variables.mdx
+export const myName = "my name"
+
+export const myObject = { fruit: "strawberries" }
+
+;
+```
+
+2. Import the snippet from your destination file and use the variable:
+
+```mdx destination-file.mdx
+---
+title: My title
+description: My Description
+---
+
+import { myName, myObject } from "/snippets/path/to/custom-variables.mdx"
+
+Hello, my name is {myName} and I like {myObject.fruit}.
+```
+
+### Reusable components
+
+1. Inside your snippet file, create a component that takes in props by exporting
+ your component in the form of an arrow function.
+
+```mdx snippets/custom-component.mdx
+export const MyComponent = ({ title }) => (
+
+
{title}
+
... snippet content ...
+
+)
+
+;
+```
+
+
+ MDX does not compile inside the body of an arrow function. Stick to HTML syntax when you can or use a default export
+ if you need to use MDX.
+
+
+2. Import the snippet into your destination file and pass in the props
+
+```mdx destination-file.mdx
+---
+title: My title
+description: My Description
+---
+
+import { MyComponent } from "/snippets/custom-component.mdx"
+
+Lorem ipsum dolor sit amet.
+
+
+```
diff --git a/packages/docs/essentials/settings.mdx b/packages/docs/essentials/settings.mdx
new file mode 100644
index 000000000..7aa44ce1e
--- /dev/null
+++ b/packages/docs/essentials/settings.mdx
@@ -0,0 +1,317 @@
+---
+title: "Global Settings"
+description: "Mintlify gives you complete control over the look and feel of your documentation using the docs.json file"
+icon: "gear"
+---
+
+Every Mintlify site needs a `docs.json` file with the core configuration settings. Learn more about the [properties](#properties) below.
+
+## Properties
+
+
+Name of your project. Used for the global title.
+
+Example: `mintlify`
+
+
+
+
+ An array of groups with all the pages within that group
+
+
+ The name of the group.
+
+ Example: `Settings`
+
+
+
+ The relative paths to the markdown files that will serve as pages.
+
+ Example: `["customization", "page"]`
+
+
+
+
+
+
+
+ Path to logo image or object with path to "light" and "dark" mode logo images
+
+
+ Path to the logo in light mode
+
+
+ Path to the logo in dark mode
+
+
+ Where clicking on the logo links you to
+
+
+
+
+
+ Path to the favicon image
+
+
+
+ Hex color codes for your global theme
+
+
+ The primary color. Used for most often for highlighted content, section headers, accents, in light mode
+
+
+ The primary color for dark mode. Used for most often for highlighted content, section headers, accents, in dark
+ mode
+
+
+ The primary color for important buttons
+
+
+ The color of the background in both light and dark mode
+
+
+ The hex color code of the background in light mode
+
+
+ The hex color code of the background in dark mode
+
+
+
+
+
+
+
+ Array of `name`s and `url`s of links you want to include in the topbar
+
+
+ The name of the button.
+
+ Example: `Contact us`
+
+
+ The url once you click on the button. Example: `https://mintlify.com/docs`
+
+
+
+
+
+
+
+
+ Link shows a button. GitHub shows the repo information at the url provided including the number of GitHub stars.
+
+
+ If `link`: What the button links to.
+
+ If `github`: Link to the repository to load GitHub information from.
+
+
+ Text inside the button. Only required if `type` is a `link`.
+
+
+
+
+
+
+ Array of version names. Only use this if you want to show different versions of docs with a dropdown in the navigation
+ bar.
+
+
+
+ An array of the anchors, includes the `icon`, `color`, and `url`.
+
+
+ The [Font Awesome](https://fontawesome.com/search?q=heart) icon used to feature the anchor.
+
+ Example: `comments`
+
+
+ The name of the anchor label.
+
+ Example: `Community`
+
+
+ The start of the URL that marks what pages go in the anchor. Generally, this is the name of the folder you put your pages in.
+
+
+ The hex color of the anchor icon background. Can also be a gradient if you pass an object with the properties `from` and `to` that are each a hex color.
+
+
+ Used if you want to hide an anchor until the correct docs version is selected.
+
+
+ Pass `true` if you want to hide the anchor until you directly link someone to docs inside it.
+
+
+ One of: "brands", "duotone", "light", "sharp-solid", "solid", or "thin"
+
+
+
+
+
+
+ Override the default configurations for the top-most anchor.
+
+
+ The name of the top-most anchor
+
+
+ Font Awesome icon.
+
+
+ One of: "brands", "duotone", "light", "sharp-solid", "solid", or "thin"
+
+
+
+
+
+ An array of navigational tabs.
+
+
+ The name of the tab label.
+
+
+ The start of the URL that marks what pages go in the tab. Generally, this is the name of the folder you put your
+ pages in.
+
+
+
+
+
+ Configuration for API settings. Learn more about API pages at [API Components](/api-playground/demo).
+
+
+ The base url for all API endpoints. If `baseUrl` is an array, it will enable for multiple base url
+ options that the user can toggle.
+
+
+
+
+
+ The authentication strategy used for all API endpoints.
+
+
+ The name of the authentication parameter used in the API playground.
+
+ If method is `basic`, the format should be `[usernameName]:[passwordName]`
+
+
+ The default value that's designed to be a prefix for the authentication input field.
+
+ E.g. If an `inputPrefix` of `AuthKey` would inherit the default input result of the authentication field as `AuthKey`.
+
+
+
+
+
+ Configurations for the API playground
+
+
+
+ Whether the playground is showing, hidden, or only displaying the endpoint with no added user interactivity `simple`
+
+ Learn more at the [playground guides](/api-playground/demo)
+
+
+
+
+
+ Enabling this flag ensures that key ordering in OpenAPI pages matches the key ordering defined in the OpenAPI file.
+
+ This behavior will soon be enabled by default, at which point this field will be deprecated.
+
+
+
+
+
+
+ A string or an array of strings of URL(s) or relative path(s) pointing to your
+ OpenAPI file.
+
+ Examples:
+
+ ```json Absolute
+ "openapi": "https://example.com/openapi.json"
+ ```
+ ```json Relative
+ "openapi": "/openapi.json"
+ ```
+ ```json Multiple
+ "openapi": ["https://example.com/openapi1.json", "/openapi2.json", "/openapi3.json"]
+ ```
+
+
+
+
+
+ An object of social media accounts where the key:property pair represents the social media platform and the account url.
+
+ Example:
+ ```json
+ {
+ "x": "https://x.com/mintlify",
+ "website": "https://mintlify.com"
+ }
+ ```
+
+
+ One of the following values `website`, `facebook`, `x`, `discord`, `slack`, `github`, `linkedin`, `instagram`, `hacker-news`
+
+ Example: `x`
+
+
+ The URL to the social platform.
+
+ Example: `https://x.com/mintlify`
+
+
+
+
+
+ Configurations to enable feedback buttons
+
+
+
+ Enables a button to allow users to suggest edits via pull requests
+
+
+ Enables a button to allow users to raise an issue about the documentation
+
+
+
+
+
+ Customize the dark mode toggle.
+
+
+ Set if you always want to show light or dark mode for new users. When not
+ set, we default to the same mode as the user's operating system.
+
+
+ Set to true to hide the dark/light mode toggle. You can combine `isHidden` with `default` to force your docs to only use light or dark mode. For example:
+
+
+ ```json Only Dark Mode
+ "modeToggle": {
+ "default": "dark",
+ "isHidden": true
+ }
+ ```
+
+ ```json Only Light Mode
+ "modeToggle": {
+ "default": "light",
+ "isHidden": true
+ }
+ ```
+
+
+
+
+
+
+
+
+ A background image to be displayed behind every page. See example with [Infisical](https://infisical.com/docs) and
+ [FRPC](https://frpc.io).
+
diff --git a/packages/docs/favicon.svg b/packages/docs/favicon.svg
new file mode 100644
index 000000000..b785c738b
--- /dev/null
+++ b/packages/docs/favicon.svg
@@ -0,0 +1,19 @@
+
diff --git a/packages/docs/images/checks-passed.png b/packages/docs/images/checks-passed.png
new file mode 100644
index 000000000..3303c7736
Binary files /dev/null and b/packages/docs/images/checks-passed.png differ
diff --git a/packages/docs/images/hero-dark.png b/packages/docs/images/hero-dark.png
new file mode 100644
index 000000000..a61cbb125
Binary files /dev/null and b/packages/docs/images/hero-dark.png differ
diff --git a/packages/docs/images/hero-light.png b/packages/docs/images/hero-light.png
new file mode 100644
index 000000000..68c712d6d
Binary files /dev/null and b/packages/docs/images/hero-light.png differ
diff --git a/packages/docs/index.mdx b/packages/docs/index.mdx
new file mode 100644
index 000000000..19a09f890
--- /dev/null
+++ b/packages/docs/index.mdx
@@ -0,0 +1,56 @@
+---
+title: "Introduction"
+description: "Welcome to the new home for your documentation"
+---
+
+## Setting up
+
+Get your documentation site up and running in minutes.
+
+
+ Follow our three step quickstart guide.
+
+
+## Make it yours
+
+Design a docs site that looks great and empowers your users.
+
+
+
+ Edit your docs locally and preview them in real time.
+
+
+ Customize the design and colors of your site to match your brand.
+
+
+ Organize your docs to help users find what they need and succeed with your product.
+
+
+ Auto-generate API documentation from OpenAPI specifications.
+
+
+
+## Create beautiful pages
+
+Everything you need to create world-class documentation.
+
+
+
+ Use MDX to style your docs pages.
+
+
+ Add sample code to demonstrate how to use your product.
+
+
+ Display images and other media.
+
+
+ Write once and reuse across your docs.
+
+
+
+## Need inspiration?
+
+
+ Browse our showcase of exceptional documentation sites.
+
diff --git a/packages/docs/logo/dark.svg b/packages/docs/logo/dark.svg
new file mode 100644
index 000000000..8b343cd6f
--- /dev/null
+++ b/packages/docs/logo/dark.svg
@@ -0,0 +1,21 @@
+
diff --git a/packages/docs/logo/light.svg b/packages/docs/logo/light.svg
new file mode 100644
index 000000000..03e62bf1d
--- /dev/null
+++ b/packages/docs/logo/light.svg
@@ -0,0 +1,21 @@
+
diff --git a/packages/docs/openapi.json b/packages/docs/openapi.json
new file mode 120000
index 000000000..854dd8b2b
--- /dev/null
+++ b/packages/docs/openapi.json
@@ -0,0 +1 @@
+../sdk/openapi.json
\ No newline at end of file
diff --git a/packages/docs/quickstart.mdx b/packages/docs/quickstart.mdx
new file mode 100644
index 000000000..52243fba6
--- /dev/null
+++ b/packages/docs/quickstart.mdx
@@ -0,0 +1,81 @@
+---
+title: "Quickstart"
+description: "Start building awesome documentation in minutes"
+---
+
+## Get started in three steps
+
+Get your documentation site running locally and make your first customization.
+
+### Step 1: Set up your local environment
+
+
+
+ During the onboarding process, you created a GitHub repository with your docs content if you didn't already have
+ one. You can find a link to this repository in your [dashboard](https://dashboard.mintlify.com). To clone the
+ repository locally so that you can make and preview changes to your docs, follow the [Cloning a
+ repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) guide
+ in the GitHub docs.
+
+
+ 1. Install the Mintlify CLI: `npm i -g mint` 2. Navigate to your docs directory and run: `mint dev` 3. Open
+ `http://localhost:3000` to see your docs live!
+ Your preview updates automatically as you edit files.
+
+
+
+### Step 2: Deploy your changes
+
+
+
+ Install the Mintlify GitHub app from your [dashboard](https://dashboard.mintlify.com/settings/organization/github-app).
+
+ Our GitHub app automatically deploys your changes to your docs site, so you don't need to manage deployments yourself.
+
+
+ For a first change, let's update the name and colors of your docs site.
+
+ 1. Open `docs.json` in your editor.
+ 2. Change the `"name"` field to your project name.
+ 3. Update the `"colors"` to match your brand.
+ 4. Save and see your changes instantly at `http://localhost:3000`.
+
+ Try changing the primary color to see an immediate difference!
+
+
+
+
+### Step 3: Go live
+
+
+ 1. Commit and push your changes. 2. Your docs will update and be live in moments!
+
+
+## Next steps
+
+Now that you have your docs running, explore these key features:
+
+
+
+
+ Learn MDX syntax and start writing your documentation.
+
+
+
+ Make your docs match your brand perfectly.
+
+
+
+ Include syntax-highlighted code blocks.
+
+
+
+ Auto-generate API docs from OpenAPI specs.
+
+
+
+
+
+ **Need help?** See our [full documentation](https://mintlify.com/docs) or join our
+ [community](https://mintlify.com/community).
+
diff --git a/packages/docs/snippets/snippet-intro.mdx b/packages/docs/snippets/snippet-intro.mdx
new file mode 100644
index 000000000..e20fbb6fc
--- /dev/null
+++ b/packages/docs/snippets/snippet-intro.mdx
@@ -0,0 +1,4 @@
+One of the core principles of software development is DRY (Don't Repeat
+Yourself). This is a principle that applies to documentation as
+well. If you find yourself repeating the same content in multiple places, you
+should consider creating a custom snippet to keep your content in sync.
diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json
index 9c453027d..89243ffeb 100644
--- a/packages/enterprise/package.json
+++ b/packages/enterprise/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
- "version": "1.0.120",
+ "version": "1.0.162",
"private": true,
"type": "module",
"scripts": {
@@ -14,12 +14,13 @@
"@opencode-ai/util": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"aws4fetch": "^1.0.20",
- "@pierre/precision-diffs": "catalog:",
+ "@pierre/diffs": "catalog:",
"@solidjs/router": "catalog:",
"@solidjs/start": "catalog:",
"@solidjs/meta": "catalog:",
"hono": "catalog:",
"hono-openapi": "catalog:",
+ "js-base64": "3.7.7",
"luxon": "catalog:",
"nitro": "3.0.1-alpha.1",
"solid-js": "catalog:",
diff --git a/packages/enterprise/public/social-share-zen.png b/packages/enterprise/public/social-share-zen.png
new file mode 120000
index 000000000..02f205fc5
--- /dev/null
+++ b/packages/enterprise/public/social-share-zen.png
@@ -0,0 +1 @@
+../../ui/src/assets/images/social-share-zen.png
\ No newline at end of file
diff --git a/packages/enterprise/public/social-share.png b/packages/enterprise/public/social-share.png
deleted file mode 100644
index 92224f54c..000000000
Binary files a/packages/enterprise/public/social-share.png and /dev/null differ
diff --git a/packages/enterprise/public/social-share.png b/packages/enterprise/public/social-share.png
new file mode 120000
index 000000000..88bf2d4c6
--- /dev/null
+++ b/packages/enterprise/public/social-share.png
@@ -0,0 +1 @@
+../../ui/src/assets/images/social-share.png
\ No newline at end of file
diff --git a/packages/enterprise/src/core/share.ts b/packages/enterprise/src/core/share.ts
index dff7d0d74..d7f5c8b8d 100644
--- a/packages/enterprise/src/core/share.ts
+++ b/packages/enterprise/src/core/share.ts
@@ -1,8 +1,10 @@
-import { FileDiff, Message, Model, Part, Session, SessionStatus } from "@opencode-ai/sdk"
+import { FileDiff, Message, Model, Part, Session } from "@opencode-ai/sdk/v2"
import { fn } from "@opencode-ai/util/fn"
import { iife } from "@opencode-ai/util/iife"
+import { Identifier } from "@opencode-ai/util/identifier"
import z from "zod"
import { Storage } from "./storage"
+import { Binary } from "@opencode-ai/util/binary"
export namespace Share {
export const Info = z.object({
@@ -37,15 +39,15 @@ export namespace Share {
export type Data = z.infer
export const create = fn(z.object({ sessionID: z.string() }), async (body) => {
+ const isTest = process.env.NODE_ENV === "test" || body.sessionID.startsWith("test_")
const info: Info = {
- id: body.sessionID.slice(-8),
+ id: (isTest ? "test_" : "") + body.sessionID.slice(-8),
sessionID: body.sessionID,
secret: crypto.randomUUID(),
}
const exists = await get(info.id)
if (exists) throw new Errors.AlreadyExists(info.id)
await Storage.write(["share", info.id], info)
- console.log("created share", info.id)
return info
})
@@ -58,30 +60,77 @@ export namespace Share {
if (!share) throw new Errors.NotFound(body.id)
if (share.secret !== body.secret) throw new Errors.InvalidSecret(body.id)
await Storage.remove(["share", body.id])
- const list = await Storage.list(["share_data", body.id])
+ const list = await Storage.list({ prefix: ["share_data", body.id] })
for (const item of list) {
await Storage.remove(item)
}
})
- export async function data(id: string) {
- const list = await Storage.list(["share_data", id])
- const promises = []
- for (const item of list) {
- promises.push(
- iife(async () => {
- const [, , type] = item
- return {
- type: type as any,
- data: await Storage.read(item),
- } as Data
- }),
- )
- }
- return await Promise.all(promises)
+ export const sync = fn(
+ z.object({
+ share: Info.pick({ id: true, secret: true }),
+ data: Data.array(),
+ }),
+ async (input) => {
+ const share = await get(input.share.id)
+ if (!share) throw new Errors.NotFound(input.share.id)
+ if (share.secret !== input.share.secret) throw new Errors.InvalidSecret(input.share.id)
+ await Storage.write(["share_event", input.share.id, Identifier.descending()], input.data)
+ },
+ )
+
+ type Compaction = {
+ event?: string
+ data: Data[]
}
- export const sync = fn(
+ export async function data(shareID: string) {
+ console.log("reading compaction")
+ const compaction: Compaction = (await Storage.read(["share_compaction", shareID])) ?? {
+ data: [],
+ event: undefined,
+ }
+ console.log("reading pending events")
+ const list = await Storage.list({
+ prefix: ["share_event", shareID],
+ before: compaction.event,
+ }).then((x) => x.toReversed())
+
+ console.log("compacting", list.length)
+
+ if (list.length > 0) {
+ const data = await Promise.all(list.map(async (event) => await Storage.read(event))).then((x) => x.flat())
+ for (const item of data) {
+ if (!item) continue
+ const key = (item: Data) => {
+ switch (item.type) {
+ case "session":
+ return "session"
+ case "message":
+ return `message/${item.data.id}`
+ case "part":
+ return `${item.data.messageID}/${item.data.id}`
+ case "session_diff":
+ return "session_diff"
+ case "model":
+ return "model"
+ }
+ }
+ const id = key(item)
+ const result = Binary.search(compaction.data, id, key)
+ if (result.found) {
+ compaction.data[result.index] = item
+ } else {
+ compaction.data.splice(result.index, 0, item)
+ }
+ }
+ compaction.event = list.at(-1)?.at(-1)
+ await Storage.write(["share_compaction", shareID], compaction)
+ }
+ return compaction.data
+ }
+
+ export const syncOld = fn(
z.object({
share: Info.pick({ id: true, secret: true }),
data: Data.array(),
@@ -98,15 +147,16 @@ export namespace Share {
case "session":
await Storage.write(["share_data", input.share.id, "session"], item.data)
break
- case "message":
- await Storage.write(["share_data", input.share.id, "message", item.data.id], item.data)
+ case "message": {
+ const data = item.data as Message
+ await Storage.write(["share_data", input.share.id, "message", data.id], item.data)
break
- case "part":
- await Storage.write(
- ["share_data", input.share.id, "part", item.data.messageID, item.data.id],
- item.data,
- )
+ }
+ case "part": {
+ const data = item.data as Part
+ await Storage.write(["share_data", input.share.id, "part", data.messageID, data.id], item.data)
break
+ }
case "session_diff":
await Storage.write(["share_data", input.share.id, "session_diff"], item.data)
break
diff --git a/packages/enterprise/src/core/storage.ts b/packages/enterprise/src/core/storage.ts
index ee711458b..b8030b4f9 100644
--- a/packages/enterprise/src/core/storage.ts
+++ b/packages/enterprise/src/core/storage.ts
@@ -6,7 +6,7 @@ export namespace Storage {
read(path: string): Promise
write(path: string, value: string): Promise
remove(path: string): Promise
- list(prefix: string): Promise
+ list(options?: { prefix?: string; limit?: number; after?: string; before?: string }): Promise
}
function createAdapter(client: AwsClient, endpoint: string, bucket: string): Adapter {
@@ -37,8 +37,14 @@ export namespace Storage {
if (!response.ok) throw new Error(`Failed to remove ${path}: ${response.status}`)
},
- async list(prefix: string): Promise {
+ async list(options?: { prefix?: string; limit?: number; after?: string; before?: string }): Promise {
+ const prefix = options?.prefix || ""
const params = new URLSearchParams({ "list-type": "2", prefix })
+ if (options?.limit) params.set("max-keys", options.limit.toString())
+ if (options?.after) {
+ const afterPath = prefix + options.after + ".json"
+ params.set("start-after", afterPath)
+ }
const response = await client.fetch(`${base}?${params}`)
if (!response.ok) throw new Error(`Failed to list ${prefix}: ${response.status}`)
const xml = await response.text()
@@ -48,6 +54,10 @@ export namespace Storage {
while ((match = regex.exec(xml)) !== null) {
keys.push(match[1])
}
+ if (options?.before) {
+ const beforePath = prefix + options.before + ".json"
+ return keys.filter((key) => key < beforePath)
+ }
return keys
},
}
@@ -98,9 +108,14 @@ export namespace Storage {
return adapter().remove(resolve(key))
}
- export async function list(prefix: string[]) {
- const p = prefix.join("/") + (prefix.length ? "/" : "")
- const result = await adapter().list(p)
+ export async function list(options?: { prefix?: string[]; limit?: number; after?: string; before?: string }) {
+ const p = options?.prefix ? options.prefix.join("/") + (options.prefix.length ? "/" : "") : ""
+ const result = await adapter().list({
+ prefix: p,
+ limit: options?.limit,
+ after: options?.after,
+ before: options?.before,
+ })
return result.map((x) => x.replace(/\.json$/, "").split("/"))
}
diff --git a/packages/enterprise/src/entry-server.tsx b/packages/enterprise/src/entry-server.tsx
index 68f4325c8..fbe5e6e0b 100644
--- a/packages/enterprise/src/entry-server.tsx
+++ b/packages/enterprise/src/entry-server.tsx
@@ -11,8 +11,6 @@ export default createHandler(() => (
OpenCode
-
-
{assets}
diff --git a/packages/enterprise/src/routes/index.tsx b/packages/enterprise/src/routes/index.tsx
new file mode 100644
index 000000000..5a743b039
--- /dev/null
+++ b/packages/enterprise/src/routes/index.tsx
@@ -0,0 +1,3 @@
+export default function () {
+ return Hello World
+}
diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx
index ffe7f533a..83cc030f9 100644
--- a/packages/enterprise/src/routes/share/[shareID].tsx
+++ b/packages/enterprise/src/routes/share/[shareID].tsx
@@ -1,12 +1,14 @@
-import { FileDiff, Message, Model, Part, Session, SessionStatus, UserMessage } from "@opencode-ai/sdk"
+import { FileDiff, Message, Model, Part, Session, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2"
import { SessionTurn } from "@opencode-ai/ui/session-turn"
import { SessionReview } from "@opencode-ai/ui/session-review"
import { DataProvider } from "@opencode-ai/ui/context"
+import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
import { createAsync, query, useParams } from "@solidjs/router"
import { createEffect, createMemo, ErrorBoundary, For, Match, Show, Switch } from "solid-js"
import { Share } from "~/core/share"
import { Logo, Mark } from "@opencode-ai/ui/logo"
import { IconButton } from "@opencode-ai/ui/icon-button"
+import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { createDefaultOptions } from "@opencode-ai/ui/pierre"
import { iife } from "@opencode-ai/util/iife"
import { Binary } from "@opencode-ai/util/binary"
@@ -17,7 +19,14 @@ import { createStore } from "solid-js/store"
import z from "zod"
import NotFound from "../[...404]"
import { Tabs } from "@opencode-ai/ui/tabs"
-import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
+import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
+import { Diff as SSRDiff } from "@opencode-ai/ui/diff-ssr"
+import { clientOnly } from "@solidjs/start"
+import { type IconName } from "@opencode-ai/ui/icons/provider"
+import { Meta } from "@solidjs/meta"
+import { Base64 } from "js-base64"
+
+const ClientOnlyDiff = clientOnly(() => import("@opencode-ai/ui/diff").then((m) => ({ default: m.Diff })))
const SessionDataMissingError = NamedError.create(
"SessionDataMissingError",
@@ -34,6 +43,7 @@ const getData = query(async (shareID) => {
const data = await Share.data(shareID)
const result: {
sessionID: string
+ shareID: string
session: Session[]
session_diff: {
[sessionID: string]: FileDiff[]
@@ -41,6 +51,9 @@ const getData = query(async (shareID) => {
session_diff_preload: {
[sessionID: string]: PreloadMultiFileDiffResult[]
}
+ session_diff_preload_split: {
+ [sessionID: string]: PreloadMultiFileDiffResult[]
+ }
session_status: {
[sessionID: string]: SessionStatus
}
@@ -55,6 +68,7 @@ const getData = query(async (shareID) => {
}
} = {
sessionID: share.sessionID,
+ shareID,
session: [],
session_diff: {
[share.sessionID]: [],
@@ -62,6 +76,9 @@ const getData = query(async (shareID) => {
session_diff_preload: {
[share.sessionID]: [],
},
+ session_diff_preload_split: {
+ [share.sessionID]: [],
+ },
session_status: {
[share.sessionID]: {
type: "idle",
@@ -78,16 +95,28 @@ const getData = query(async (shareID) => {
break
case "session_diff":
result.session_diff[share.sessionID] = item.data
- result.session_diff_preload[share.sessionID] = await Promise.all(
- item.data.map(async (diff) =>
- preloadMultiFileDiff({
- oldFile: { name: diff.file, contents: diff.before },
- newFile: { name: diff.file, contents: diff.after },
- options: createDefaultOptions("unified"),
- // annotations,
- }),
- ),
- )
+ await Promise.all([
+ Promise.all(
+ item.data.map(async (diff) =>
+ preloadMultiFileDiff({
+ oldFile: { name: diff.file, contents: diff.before },
+ newFile: { name: diff.file, contents: diff.after },
+ options: createDefaultOptions("unified"),
+ // annotations,
+ }),
+ ),
+ ).then((r) => (result.session_diff_preload[share.sessionID] = r)),
+ Promise.all(
+ item.data.map(async (diff) =>
+ preloadMultiFileDiff({
+ oldFile: { name: diff.file, contents: diff.before },
+ newFile: { name: diff.file, contents: diff.after },
+ options: createDefaultOptions("split"),
+ // annotations,
+ }),
+ ),
+ ).then((r) => (result.session_diff_preload_split[share.sessionID] = r)),
+ ])
break
case "message":
result.message[item.data.sessionID] = result.message[item.data.sessionID] ?? []
@@ -111,7 +140,10 @@ export default function () {
const params = useParams()
const data = createAsync(async () => {
if (!params.shareID) throw new Error("Missing shareID")
- return getData(params.shareID)
+ const now = Date.now()
+ const data = getData(params.shareID)
+ console.log("getData", Date.now() - now)
+ return data
})
createEffect(() => {
@@ -128,214 +160,277 @@ export default function () {
)
}}
>
+
{(data) => {
const match = createMemo(() => Binary.search(data().session, data().sessionID, (s) => s.id))
if (!match().found) throw new Error(`Session ${data().sessionID} not found`)
const info = createMemo(() => data().session[match().index])
+ const ogImage = createMemo(() => {
+ const models = new Set()
+ const messages = data().message[data().sessionID] ?? []
+ for (const msg of messages) {
+ if (msg.role === "assistant" && msg.modelID) {
+ models.add(msg.modelID)
+ }
+ }
+ const modelIDs = Array.from(models)
+ const encodedTitle = encodeURIComponent(Base64.encode(encodeURIComponent(info().title.substring(0, 700))))
+ let modelParam: string
+ if (modelIDs.length === 1) {
+ modelParam = modelIDs[0]
+ } else if (modelIDs.length === 2) {
+ modelParam = encodeURIComponent(`${modelIDs[0]} & ${modelIDs[1]}`)
+ } else if (modelIDs.length > 2) {
+ modelParam = encodeURIComponent(`${modelIDs[0]} & ${modelIDs.length - 1} others`)
+ } else {
+ modelParam = "unknown"
+ }
+ const version = `v${info().version}`
+ return `https://social-cards.sst.dev/opencode-share/${encodedTitle}.png?model=${modelParam}&version=${version}&id=${data().shareID}`
+ })
return (
-
- {iife(() => {
- const [store, setStore] = createStore({
- messageId: undefined as string | undefined,
- })
- const messages = createMemo(() =>
- data().sessionID
- ? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
- (a, b) => b.time.created - a.time.created,
- )
- : [],
- )
- const firstUserMessage = createMemo(() => messages().at(0))
- const activeMessage = createMemo(
- () => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
- )
- function setActiveMessage(message: UserMessage | undefined) {
- if (message) {
- setStore("messageId", message.id)
- } else {
- setStore("messageId", undefined)
- }
- }
- const provider = createMemo(() => activeMessage()?.model?.providerID)
- const modelID = createMemo(() => activeMessage()?.model?.modelID)
- const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
- const diffs = createMemo(() => {
- const diffs = data().session_diff[data().sessionID] ?? []
- const preloaded = data().session_diff_preload[data().sessionID] ?? []
- return diffs.map((diff) => ({
- ...diff,
- preloaded: preloaded.find((d) => d.newFile.name === diff.file),
- }))
- })
+ <>
+
+
+
+
+
+ {iife(() => {
+ const [store, setStore] = createStore({
+ messageId: undefined as string | undefined,
+ })
+ const messages = createMemo(() =>
+ data().sessionID
+ ? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
+ (a, b) => a.time.created - b.time.created,
+ )
+ : [],
+ )
+ const firstUserMessage = createMemo(() => messages().at(0))
+ const activeMessage = createMemo(
+ () => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
+ )
+ function setActiveMessage(message: UserMessage | undefined) {
+ if (message) {
+ setStore("messageId", message.id)
+ } else {
+ setStore("messageId", undefined)
+ }
+ }
+ const provider = createMemo(() => activeMessage()?.model?.providerID)
+ const modelID = createMemo(() => activeMessage()?.model?.modelID)
+ const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
+ const diffs = createMemo(() => {
+ const diffs = data().session_diff[data().sessionID] ?? []
+ const preloaded = data().session_diff_preload[data().sessionID] ?? []
+ return diffs.map((diff) => ({
+ ...diff,
+ preloaded: preloaded.find((d) => d.newFile.name === diff.file),
+ }))
+ })
+ const splitDiffs = createMemo(() => {
+ const diffs = data().session_diff[data().sessionID] ?? []
+ const preloaded = data().session_diff_preload_split[data().sessionID] ?? []
+ return diffs.map((diff) => ({
+ ...diff,
+ preloaded: preloaded.find((d) => d.newFile.name === diff.file),
+ }))
+ })
- const title = () => (
-
-
-
-
-
}.svg`})
-
{model()?.name ?? modelID()}
-
-
- {DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
-
-
-
{info().title}
-
- )
-
- const turns = () => (
-
- )
-
- const wide = createMemo(() => diffs().length === 0)
-
- return (
-
-
-
-
-
-
- {title()}
+ const title = () => (
+
+
+
-
-
-
-
-
-
-
+
+
+
{model()?.name ?? modelID()}
+
+
+ {DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
-
0}>
-
- {info().title}
+
+ )
+
+ const turns = () => (
+
+ )
+
+ const wide = createMemo(() => diffs().length === 0)
+
+ return (
+
-
- 0}>
-
-
-
- Session
-
-
- 5 Files Changed
-
-
-
- {turns()}
-
-
+
+
- )
- })}
-
+
+ 0}>
+
+
+
+ Session
+
+
+ {diffs().length} Files Changed
+
+
+
+ {turns()}
+
+
+
+
+
+
+
+
+
+
+
+
+ {turns()}
+
+
+
+
+
+ )
+ })}
+
+
+ >
)
}}
diff --git a/packages/enterprise/sst-env.d.ts b/packages/enterprise/sst-env.d.ts
index 08710b63e..632ea3fbe 100644
--- a/packages/enterprise/sst-env.d.ts
+++ b/packages/enterprise/sst-env.d.ts
@@ -6,126 +6,134 @@
import "sst"
declare module "sst" {
export interface Resource {
- ADMIN_SECRET: {
- type: "sst.sst.Secret"
- value: string
+ "ADMIN_SECRET": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- AUTH_API_URL: {
- type: "sst.sst.Linkable"
- value: string
+ "AUTH_API_URL": {
+ "type": "sst.sst.Linkable"
+ "value": string
}
- AWS_SES_ACCESS_KEY_ID: {
- type: "sst.sst.Secret"
- value: string
+ "AWS_SES_ACCESS_KEY_ID": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- AWS_SES_SECRET_ACCESS_KEY: {
- type: "sst.sst.Secret"
- value: string
+ "AWS_SES_SECRET_ACCESS_KEY": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- CLOUDFLARE_API_TOKEN: {
- type: "sst.sst.Secret"
- value: string
+ "CLOUDFLARE_API_TOKEN": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
- type: "sst.sst.Secret"
- value: string
+ "CLOUDFLARE_DEFAULT_ACCOUNT_ID": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- Console: {
- type: "sst.cloudflare.SolidStart"
- url: string
+ "Console": {
+ "type": "sst.cloudflare.SolidStart"
+ "url": string
}
- Database: {
- database: string
- host: string
- password: string
- port: number
- type: "sst.sst.Linkable"
- username: string
+ "Database": {
+ "database": string
+ "host": string
+ "password": string
+ "port": number
+ "type": "sst.sst.Linkable"
+ "username": string
}
- Desktop: {
- type: "sst.cloudflare.StaticSite"
- url: string
+ "Desktop": {
+ "type": "sst.cloudflare.StaticSite"
+ "url": string
}
- EMAILOCTOPUS_API_KEY: {
- type: "sst.sst.Secret"
- value: string
+ "EMAILOCTOPUS_API_KEY": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- GITHUB_APP_ID: {
- type: "sst.sst.Secret"
- value: string
+ "GITHUB_APP_ID": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- GITHUB_APP_PRIVATE_KEY: {
- type: "sst.sst.Secret"
- value: string
+ "GITHUB_APP_PRIVATE_KEY": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- GITHUB_CLIENT_ID_CONSOLE: {
- type: "sst.sst.Secret"
- value: string
+ "GITHUB_CLIENT_ID_CONSOLE": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- GITHUB_CLIENT_SECRET_CONSOLE: {
- type: "sst.sst.Secret"
- value: string
+ "GITHUB_CLIENT_SECRET_CONSOLE": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- GOOGLE_CLIENT_ID: {
- type: "sst.sst.Secret"
- value: string
+ "GOOGLE_CLIENT_ID": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- HONEYCOMB_API_KEY: {
- type: "sst.sst.Secret"
- value: string
+ "HONEYCOMB_API_KEY": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- R2AccessKey: {
- type: "sst.sst.Secret"
- value: string
+ "R2AccessKey": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- R2SecretKey: {
- type: "sst.sst.Secret"
- value: string
+ "R2SecretKey": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- STRIPE_SECRET_KEY: {
- type: "sst.sst.Secret"
- value: string
+ "STRIPE_SECRET_KEY": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- STRIPE_WEBHOOK_SECRET: {
- type: "sst.sst.Linkable"
- value: string
+ "STRIPE_WEBHOOK_SECRET": {
+ "type": "sst.sst.Linkable"
+ "value": string
}
- Web: {
- type: "sst.cloudflare.Astro"
- url: string
+ "Teams": {
+ "type": "sst.cloudflare.SolidStart"
+ "url": string
}
- ZEN_MODELS1: {
- type: "sst.sst.Secret"
- value: string
+ "Web": {
+ "type": "sst.cloudflare.Astro"
+ "url": string
}
- ZEN_MODELS2: {
- type: "sst.sst.Secret"
- value: string
+ "ZEN_MODELS1": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- ZEN_MODELS3: {
- type: "sst.sst.Secret"
- value: string
+ "ZEN_MODELS2": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- ZEN_MODELS4: {
- type: "sst.sst.Secret"
- value: string
+ "ZEN_MODELS3": {
+ "type": "sst.sst.Secret"
+ "value": string
+ }
+ "ZEN_MODELS4": {
+ "type": "sst.sst.Secret"
+ "value": string
+ }
+ "ZEN_MODELS5": {
+ "type": "sst.sst.Secret"
+ "value": string
}
}
}
-// cloudflare
-import * as cloudflare from "@cloudflare/workers-types"
+// cloudflare
+import * as cloudflare from "@cloudflare/workers-types";
declare module "sst" {
export interface Resource {
- Api: cloudflare.Service
- AuthApi: cloudflare.Service
- AuthStorage: cloudflare.KVNamespace
- Bucket: cloudflare.R2Bucket
- ConsoleData: cloudflare.R2Bucket
- EnterpriseStorage: cloudflare.R2Bucket
- GatewayKv: cloudflare.KVNamespace
- LogProcessor: cloudflare.Service
+ "Api": cloudflare.Service
+ "AuthApi": cloudflare.Service
+ "AuthStorage": cloudflare.KVNamespace
+ "Bucket": cloudflare.R2Bucket
+ "EnterpriseStorage": cloudflare.R2Bucket
+ "GatewayKv": cloudflare.KVNamespace
+ "LogProcessor": cloudflare.Service
+ "ZenData": cloudflare.R2Bucket
}
}
import "sst"
-export {}
+export {}
\ No newline at end of file
diff --git a/packages/enterprise/test-debug.ts b/packages/enterprise/test-debug.ts
new file mode 100644
index 000000000..a2ec4d8cd
--- /dev/null
+++ b/packages/enterprise/test-debug.ts
@@ -0,0 +1,40 @@
+import { Share } from "./src/core/share"
+import { Storage } from "./src/core/storage"
+
+async function test() {
+ const shareInfo = await Share.create({ sessionID: "test-debug-" + Date.now() })
+
+ const batch1: Share.Data[] = [
+ { type: "part", data: { id: "part1", sessionID: "session1", messageID: "msg1", type: "text", text: "Hello" } },
+ ]
+
+ const batch2: Share.Data[] = [
+ {
+ type: "part",
+ data: { id: "part1", sessionID: "session1", messageID: "msg1", type: "text", text: "Hello Updated" },
+ },
+ ]
+
+ await Share.sync({
+ share: { id: shareInfo.id, secret: shareInfo.secret },
+ data: batch1,
+ })
+
+ await Share.sync({
+ share: { id: shareInfo.id, secret: shareInfo.secret },
+ data: batch2,
+ })
+
+ const events = await Storage.list({ prefix: ["share_event", shareInfo.id] })
+ console.log("Events (raw):", events)
+ console.log("Events (reversed):", events.toReversed())
+
+ for (const event of events.toReversed()) {
+ const data = await Storage.read(event)
+ console.log("Event data (reversed order):", event, data)
+ }
+
+ await Share.remove({ id: shareInfo.id, secret: shareInfo.secret })
+}
+
+test()
diff --git a/packages/enterprise/test/core/share.test.ts b/packages/enterprise/test/core/share.test.ts
new file mode 100644
index 000000000..9e9c06db3
--- /dev/null
+++ b/packages/enterprise/test/core/share.test.ts
@@ -0,0 +1,262 @@
+import { describe, expect, test, afterAll } from "bun:test"
+import { Share } from "../../src/core/share"
+import { Storage } from "../../src/core/storage"
+import { Identifier } from "@opencode-ai/util/identifier"
+
+describe.concurrent("core.share", () => {
+ test("should create a share", async () => {
+ const sessionID = Identifier.descending()
+ const share = await Share.create({ sessionID })
+
+ expect(share.sessionID).toBe(sessionID)
+ expect(share.secret).toBeDefined()
+
+ await Share.remove({ id: share.id, secret: share.secret })
+ })
+
+ test("should sync data to a share", async () => {
+ const sessionID = Identifier.descending()
+ const share = await Share.create({ sessionID })
+
+ const data: Share.Data[] = [
+ {
+ type: "part",
+ data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Hello" },
+ },
+ ]
+
+ await Share.sync({
+ share: { id: share.id, secret: share.secret },
+ data,
+ })
+
+ const events = await Storage.list({ prefix: ["share_event", share.id] })
+ expect(events.length).toBe(1)
+
+ await Share.remove({ id: share.id, secret: share.secret })
+ })
+
+ test("should sync multiple batches of data", async () => {
+ const sessionID = Identifier.descending()
+ const share = await Share.create({ sessionID })
+
+ const data1: Share.Data[] = [
+ {
+ type: "part",
+ data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Hello" },
+ },
+ ]
+
+ const data2: Share.Data[] = [
+ {
+ type: "part",
+ data: { id: "part2", sessionID, messageID: "msg1", type: "text", text: "World" },
+ },
+ ]
+
+ await Share.sync({
+ share: { id: share.id, secret: share.secret },
+ data: data1,
+ })
+
+ await Share.sync({
+ share: { id: share.id, secret: share.secret },
+ data: data2,
+ })
+
+ const events = await Storage.list({ prefix: ["share_event", share.id] })
+ expect(events.length).toBe(2)
+
+ await Share.remove({ id: share.id, secret: share.secret })
+ })
+
+ test("should retrieve synced data", async () => {
+ const sessionID = Identifier.descending()
+ const share = await Share.create({ sessionID })
+
+ const data: Share.Data[] = [
+ {
+ type: "part",
+ data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Hello" },
+ },
+ {
+ type: "part",
+ data: { id: "part2", sessionID, messageID: "msg1", type: "text", text: "World" },
+ },
+ ]
+
+ await Share.sync({
+ share: { id: share.id, secret: share.secret },
+ data,
+ })
+
+ const result = await Share.data(share.id)
+
+ expect(result.length).toBe(2)
+ expect(result[0].type).toBe("part")
+ expect(result[1].type).toBe("part")
+
+ await Share.remove({ id: share.id, secret: share.secret })
+ })
+
+ test("should retrieve data from multiple syncs", async () => {
+ const sessionID = Identifier.descending()
+ const share = await Share.create({ sessionID })
+
+ const data1: Share.Data[] = [
+ {
+ type: "part",
+ data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Hello" },
+ },
+ ]
+
+ const data2: Share.Data[] = [
+ {
+ type: "part",
+ data: { id: "part2", sessionID, messageID: "msg2", type: "text", text: "World" },
+ },
+ ]
+
+ const data3: Share.Data[] = [
+ { type: "part", data: { id: "part3", sessionID, messageID: "msg3", type: "text", text: "!" } },
+ ]
+
+ await Share.sync({
+ share: { id: share.id, secret: share.secret },
+ data: data1,
+ })
+
+ await Share.sync({
+ share: { id: share.id, secret: share.secret },
+ data: data2,
+ })
+
+ await Share.sync({
+ share: { id: share.id, secret: share.secret },
+ data: data3,
+ })
+
+ const result = await Share.data(share.id)
+
+ expect(result.length).toBe(3)
+ const parts = result.filter((d) => d.type === "part")
+ expect(parts.length).toBe(3)
+
+ await Share.remove({ id: share.id, secret: share.secret })
+ })
+
+ test("should return latest data when syncing duplicate parts", async () => {
+ const sessionID = Identifier.descending()
+ const share = await Share.create({ sessionID })
+
+ const data1: Share.Data[] = [
+ {
+ type: "part",
+ data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Hello" },
+ },
+ ]
+
+ const data2: Share.Data[] = [
+ {
+ type: "part",
+ data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Hello Updated" },
+ },
+ ]
+
+ await Share.sync({
+ share: { id: share.id, secret: share.secret },
+ data: data1,
+ })
+
+ await Share.sync({
+ share: { id: share.id, secret: share.secret },
+ data: data2,
+ })
+
+ const result = await Share.data(share.id)
+
+ expect(result.length).toBe(1)
+ const [first] = result
+ expect(first.type).toBe("part")
+ expect(first.type === "part" && first.data.type === "text" && first.data.text).toBe("Hello Updated")
+
+ await Share.remove({ id: share.id, secret: share.secret })
+ })
+
+ test("should return empty array for share with no data", async () => {
+ const sessionID = Identifier.descending()
+ const share = await Share.create({ sessionID })
+
+ const result = await Share.data(share.id)
+
+ expect(result).toEqual([])
+
+ await Share.remove({ id: share.id, secret: share.secret })
+ })
+
+ test("should throw error for invalid secret", async () => {
+ const sessionID = Identifier.descending()
+ const share = await Share.create({ sessionID })
+
+ const data: Share.Data[] = [
+ {
+ type: "part",
+ data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Test" },
+ },
+ ]
+
+ expect(async () => {
+ await Share.sync({
+ share: { id: share.id, secret: "invalid-secret" },
+ data,
+ })
+ }).toThrow()
+
+ await Share.remove({ id: share.id, secret: share.secret })
+ })
+
+ test("should throw error for non-existent share", async () => {
+ const sessionID = Identifier.descending()
+ const data: Share.Data[] = [
+ {
+ type: "part",
+ data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Test" },
+ },
+ ]
+
+ expect(async () => {
+ await Share.sync({
+ share: { id: "non-existent-id", secret: "some-secret" },
+ data,
+ })
+ }).toThrow()
+ })
+
+ test("should handle different data types", async () => {
+ const sessionID = Identifier.descending()
+ const share = await Share.create({ sessionID })
+
+ const data: Share.Data[] = [
+ { type: "session", data: { id: sessionID, status: "running" } as any },
+ { type: "message", data: { id: "msg1", sessionID } as any },
+ {
+ type: "part",
+ data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Hello" },
+ },
+ ]
+
+ await Share.sync({
+ share: { id: share.id, secret: share.secret },
+ data,
+ })
+
+ const result = await Share.data(share.id)
+
+ expect(result.length).toBe(3)
+ expect(result.some((d) => d.type === "session")).toBe(true)
+ expect(result.some((d) => d.type === "message")).toBe(true)
+ expect(result.some((d) => d.type === "part")).toBe(true)
+
+ await Share.remove({ id: share.id, secret: share.secret })
+ })
+})
diff --git a/packages/enterprise/test/core/storage.test.ts b/packages/enterprise/test/core/storage.test.ts
new file mode 100644
index 000000000..5b5281791
--- /dev/null
+++ b/packages/enterprise/test/core/storage.test.ts
@@ -0,0 +1,64 @@
+import { describe, expect, test, afterAll } from "bun:test"
+import { Storage } from "../../src/core/storage"
+
+describe("core.storage", () => {
+ test("should list files with after and before range", async () => {
+ await Storage.write(["test", "users", "user1"], { name: "user1" })
+ await Storage.write(["test", "users", "user2"], { name: "user2" })
+ await Storage.write(["test", "users", "user3"], { name: "user3" })
+ await Storage.write(["test", "users", "user4"], { name: "user4" })
+ await Storage.write(["test", "users", "user5"], { name: "user5" })
+
+ const result = await Storage.list({ prefix: ["test", "users"], after: "user2", before: "user4" })
+
+ expect(result).toEqual([["test", "users", "user3"]])
+ })
+
+ test("should list files with after only", async () => {
+ const result = await Storage.list({ prefix: ["test", "users"], after: "user3" })
+
+ expect(result).toEqual([
+ ["test", "users", "user4"],
+ ["test", "users", "user5"],
+ ])
+ })
+
+ test("should list files with limit", async () => {
+ const result = await Storage.list({ prefix: ["test", "users"], limit: 3 })
+
+ expect(result).toEqual([
+ ["test", "users", "user1"],
+ ["test", "users", "user2"],
+ ["test", "users", "user3"],
+ ])
+ })
+
+ test("should list all files without prefix", async () => {
+ const result = await Storage.list()
+
+ expect(result.length).toBeGreaterThan(0)
+ })
+
+ test("should list all files with prefix", async () => {
+ const result = await Storage.list({ prefix: ["test", "users"] })
+
+ expect(result).toEqual([
+ ["test", "users", "user1"],
+ ["test", "users", "user2"],
+ ["test", "users", "user3"],
+ ["test", "users", "user4"],
+ ["test", "users", "user5"],
+ ])
+ })
+
+ afterAll(async () => {
+ const testFiles = await Storage.list({ prefix: ["test"] })
+
+ for (const file of testFiles) {
+ await Storage.remove(file)
+ }
+
+ const remainingFiles = await Storage.list({ prefix: ["test"] })
+ expect(remainingFiles).toEqual([])
+ })
+})
diff --git a/packages/enterprise/vite.config.ts b/packages/enterprise/vite.config.ts
index 9ad200884..11ca1729d 100644
--- a/packages/enterprise/vite.config.ts
+++ b/packages/enterprise/vite.config.ts
@@ -18,9 +18,19 @@ const nitroConfig: any = (() => {
})()
export default defineConfig({
- plugins: [tailwindcss(), solidStart() as PluginOption, nitro(nitroConfig)],
+ plugins: [
+ tailwindcss(),
+ solidStart() as PluginOption,
+ nitro({
+ ...nitroConfig,
+ baseURL: process.env.OPENCODE_BASE_URL,
+ }),
+ ],
server: {
host: "0.0.0.0",
allowedHosts: true,
},
+ worker: {
+ format: "es",
+ },
})
diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml
index fe9c80dc6..782f989ba 100644
--- a/packages/extensions/zed/extension.toml
+++ b/packages/extensions/zed/extension.toml
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
-description = "The AI coding agent built for the terminal"
-version = "1.0.120"
+description = "The open source coding agent."
+version = "1.0.162"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/sst/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.120/opencode-darwin-arm64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.162/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.120/opencode-darwin-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.162/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.120/opencode-linux-arm64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.162/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.120/opencode-linux-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.162/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.120/opencode-windows-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.162/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
diff --git a/packages/function/package.json b/packages/function/package.json
index 4b70b2801..f1c3cf78a 100644
--- a/packages/function/package.json
+++ b/packages/function/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
- "version": "1.0.120",
+ "version": "1.0.162",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
@@ -12,7 +12,7 @@
},
"dependencies": {
"@octokit/auth-app": "8.0.1",
- "@octokit/rest": "22.0.0",
+ "@octokit/rest": "catalog:",
"hono": "catalog:",
"jose": "6.0.11"
}
diff --git a/packages/function/sst-env.d.ts b/packages/function/sst-env.d.ts
index 08710b63e..632ea3fbe 100644
--- a/packages/function/sst-env.d.ts
+++ b/packages/function/sst-env.d.ts
@@ -6,126 +6,134 @@
import "sst"
declare module "sst" {
export interface Resource {
- ADMIN_SECRET: {
- type: "sst.sst.Secret"
- value: string
+ "ADMIN_SECRET": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- AUTH_API_URL: {
- type: "sst.sst.Linkable"
- value: string
+ "AUTH_API_URL": {
+ "type": "sst.sst.Linkable"
+ "value": string
}
- AWS_SES_ACCESS_KEY_ID: {
- type: "sst.sst.Secret"
- value: string
+ "AWS_SES_ACCESS_KEY_ID": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- AWS_SES_SECRET_ACCESS_KEY: {
- type: "sst.sst.Secret"
- value: string
+ "AWS_SES_SECRET_ACCESS_KEY": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- CLOUDFLARE_API_TOKEN: {
- type: "sst.sst.Secret"
- value: string
+ "CLOUDFLARE_API_TOKEN": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
- type: "sst.sst.Secret"
- value: string
+ "CLOUDFLARE_DEFAULT_ACCOUNT_ID": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- Console: {
- type: "sst.cloudflare.SolidStart"
- url: string
+ "Console": {
+ "type": "sst.cloudflare.SolidStart"
+ "url": string
}
- Database: {
- database: string
- host: string
- password: string
- port: number
- type: "sst.sst.Linkable"
- username: string
+ "Database": {
+ "database": string
+ "host": string
+ "password": string
+ "port": number
+ "type": "sst.sst.Linkable"
+ "username": string
}
- Desktop: {
- type: "sst.cloudflare.StaticSite"
- url: string
+ "Desktop": {
+ "type": "sst.cloudflare.StaticSite"
+ "url": string
}
- EMAILOCTOPUS_API_KEY: {
- type: "sst.sst.Secret"
- value: string
+ "EMAILOCTOPUS_API_KEY": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- GITHUB_APP_ID: {
- type: "sst.sst.Secret"
- value: string
+ "GITHUB_APP_ID": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- GITHUB_APP_PRIVATE_KEY: {
- type: "sst.sst.Secret"
- value: string
+ "GITHUB_APP_PRIVATE_KEY": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- GITHUB_CLIENT_ID_CONSOLE: {
- type: "sst.sst.Secret"
- value: string
+ "GITHUB_CLIENT_ID_CONSOLE": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- GITHUB_CLIENT_SECRET_CONSOLE: {
- type: "sst.sst.Secret"
- value: string
+ "GITHUB_CLIENT_SECRET_CONSOLE": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- GOOGLE_CLIENT_ID: {
- type: "sst.sst.Secret"
- value: string
+ "GOOGLE_CLIENT_ID": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- HONEYCOMB_API_KEY: {
- type: "sst.sst.Secret"
- value: string
+ "HONEYCOMB_API_KEY": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- R2AccessKey: {
- type: "sst.sst.Secret"
- value: string
+ "R2AccessKey": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- R2SecretKey: {
- type: "sst.sst.Secret"
- value: string
+ "R2SecretKey": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- STRIPE_SECRET_KEY: {
- type: "sst.sst.Secret"
- value: string
+ "STRIPE_SECRET_KEY": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- STRIPE_WEBHOOK_SECRET: {
- type: "sst.sst.Linkable"
- value: string
+ "STRIPE_WEBHOOK_SECRET": {
+ "type": "sst.sst.Linkable"
+ "value": string
}
- Web: {
- type: "sst.cloudflare.Astro"
- url: string
+ "Teams": {
+ "type": "sst.cloudflare.SolidStart"
+ "url": string
}
- ZEN_MODELS1: {
- type: "sst.sst.Secret"
- value: string
+ "Web": {
+ "type": "sst.cloudflare.Astro"
+ "url": string
}
- ZEN_MODELS2: {
- type: "sst.sst.Secret"
- value: string
+ "ZEN_MODELS1": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- ZEN_MODELS3: {
- type: "sst.sst.Secret"
- value: string
+ "ZEN_MODELS2": {
+ "type": "sst.sst.Secret"
+ "value": string
}
- ZEN_MODELS4: {
- type: "sst.sst.Secret"
- value: string
+ "ZEN_MODELS3": {
+ "type": "sst.sst.Secret"
+ "value": string
+ }
+ "ZEN_MODELS4": {
+ "type": "sst.sst.Secret"
+ "value": string
+ }
+ "ZEN_MODELS5": {
+ "type": "sst.sst.Secret"
+ "value": string
}
}
}
-// cloudflare
-import * as cloudflare from "@cloudflare/workers-types"
+// cloudflare
+import * as cloudflare from "@cloudflare/workers-types";
declare module "sst" {
export interface Resource {
- Api: cloudflare.Service
- AuthApi: cloudflare.Service
- AuthStorage: cloudflare.KVNamespace
- Bucket: cloudflare.R2Bucket
- ConsoleData: cloudflare.R2Bucket
- EnterpriseStorage: cloudflare.R2Bucket
- GatewayKv: cloudflare.KVNamespace
- LogProcessor: cloudflare.Service
+ "Api": cloudflare.Service
+ "AuthApi": cloudflare.Service
+ "AuthStorage": cloudflare.KVNamespace
+ "Bucket": cloudflare.R2Bucket
+ "EnterpriseStorage": cloudflare.R2Bucket
+ "GatewayKv": cloudflare.KVNamespace
+ "LogProcessor": cloudflare.Service
+ "ZenData": cloudflare.R2Bucket
}
}
import "sst"
-export {}
+export {}
\ No newline at end of file
diff --git a/packages/identity/avatar-dark.png b/packages/identity/avatar-dark.png
deleted file mode 100644
index d3dd04eac..000000000
Binary files a/packages/identity/avatar-dark.png and /dev/null differ
diff --git a/packages/identity/avatar-light.png b/packages/identity/avatar-light.png
deleted file mode 100644
index 678a7928e..000000000
Binary files a/packages/identity/avatar-light.png and /dev/null differ
diff --git a/packages/identity/logo-dark.svg b/packages/identity/logo-dark.svg
deleted file mode 100644
index a4e433958..000000000
--- a/packages/identity/logo-dark.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
diff --git a/packages/identity/logo-light.svg b/packages/identity/logo-light.svg
deleted file mode 100644
index cbfcccf51..000000000
--- a/packages/identity/logo-light.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
diff --git a/packages/identity/logo-ornate-dark.svg b/packages/identity/logo-ornate-dark.svg
deleted file mode 100644
index b937be0af..000000000
--- a/packages/identity/logo-ornate-dark.svg
+++ /dev/null
@@ -1,18 +0,0 @@
-
diff --git a/packages/identity/logo-ornate-light.svg b/packages/identity/logo-ornate-light.svg
deleted file mode 100644
index 789223bc4..000000000
--- a/packages/identity/logo-ornate-light.svg
+++ /dev/null
@@ -1,18 +0,0 @@
-
diff --git a/packages/identity/logo-square-dark.svg b/packages/identity/logo-square-dark.svg
deleted file mode 100644
index a309fcaed..000000000
--- a/packages/identity/logo-square-dark.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/packages/identity/logo-square-light.svg b/packages/identity/logo-square-light.svg
deleted file mode 100644
index 404e214d5..000000000
--- a/packages/identity/logo-square-light.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/packages/identity/logomark-dark.svg b/packages/identity/logomark-dark.svg
deleted file mode 100644
index 5c7e2ac70..000000000
--- a/packages/identity/logomark-dark.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/packages/identity/logomark-light.svg b/packages/identity/logomark-light.svg
deleted file mode 100644
index ad08d40b3..000000000
--- a/packages/identity/logomark-light.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/packages/identity/mark-192x192.png b/packages/identity/mark-192x192.png
new file mode 100644
index 000000000..071d18fe0
Binary files /dev/null and b/packages/identity/mark-192x192.png differ
diff --git a/packages/identity/mark-512x512-light.png b/packages/identity/mark-512x512-light.png
new file mode 100644
index 000000000..9f602d5ec
Binary files /dev/null and b/packages/identity/mark-512x512-light.png differ
diff --git a/packages/identity/mark-512x512.png b/packages/identity/mark-512x512.png
new file mode 100644
index 000000000..48f38fc8c
Binary files /dev/null and b/packages/identity/mark-512x512.png differ
diff --git a/packages/identity/mark-96x96.png b/packages/identity/mark-96x96.png
new file mode 100644
index 000000000..b635c0759
Binary files /dev/null and b/packages/identity/mark-96x96.png differ
diff --git a/packages/identity/mark-light.svg b/packages/identity/mark-light.svg
new file mode 100644
index 000000000..ac619f1b2
--- /dev/null
+++ b/packages/identity/mark-light.svg
@@ -0,0 +1,5 @@
+
diff --git a/packages/identity/mark.svg b/packages/identity/mark.svg
new file mode 100644
index 000000000..157edc4d7
--- /dev/null
+++ b/packages/identity/mark.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/packages/opencode/Dockerfile b/packages/opencode/Dockerfile
index fbbeacf04..f92b48a6d 100644
--- a/packages/opencode/Dockerfile
+++ b/packages/opencode/Dockerfile
@@ -1,10 +1,18 @@
-FROM alpine
+FROM alpine AS base
# Disable the runtime transpiler cache by default inside Docker containers.
# On ephemeral containers, the cache is not useful
ARG BUN_RUNTIME_TRANSPILER_CACHE_PATH=0
ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=${BUN_RUNTIME_TRANSPILER_CACHE_PATH}
-RUN apk add libgcc libstdc++
-ADD ./dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
+RUN apk add libgcc libstdc++ ripgrep
+
+FROM base AS build-amd64
+COPY dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
+
+FROM base AS build-arm64
+COPY dist/opencode-linux-arm64-musl/bin/opencode /usr/local/bin/opencode
+
+ARG TARGETARCH
+FROM build-${TARGETARCH}
RUN opencode --version
ENTRYPOINT ["opencode"]
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index d7ff9b2e5..a3798c4ba 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "version": "1.0.120",
+ "version": "1.0.162",
"name": "opencode",
"type": "module",
"private": true,
@@ -9,7 +9,12 @@
"test": "bun test",
"build": "./script/build.ts",
"dev": "bun run --conditions=browser ./src/index.ts",
- "random": "echo 'Random script updated at $(date)'"
+ "random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'",
+ "clean": "echo 'Cleaning up...' && rm -rf node_modules dist",
+ "lint": "echo 'Running lint checks...' && bun test --coverage",
+ "format": "echo 'Formatting code...' && bun run --prettier --write src/**/*.ts",
+ "docs": "echo 'Generating documentation...' && find src -name '*.ts' -exec echo 'Processing: {}' \\;",
+ "deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'"
},
"bin": {
"opencode": "./bin/opencode"
@@ -23,7 +28,9 @@
"@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-x64": "2.5.1",
"@standard-schema/spec": "1.0.0",
"@tsconfig/bun": "catalog:",
@@ -50,26 +57,29 @@
"@ai-sdk/mcp": "0.0.8",
"@ai-sdk/openai": "2.0.71",
"@ai-sdk/openai-compatible": "1.0.27",
+ "@ai-sdk/provider": "2.0.0",
+ "@ai-sdk/provider-utils": "3.0.18",
"@clack/prompts": "1.0.0-alpha.1",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
"@modelcontextprotocol/sdk": "1.15.1",
"@octokit/graphql": "9.0.2",
- "@octokit/rest": "22.0.0",
+ "@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
- "@openrouter/ai-sdk-provider": "1.2.8",
- "@opentui/core": "0.1.51",
- "@opentui/solid": "0.1.51",
+ "@openrouter/ai-sdk-provider": "1.5.2",
+ "@opentui/core": "0.0.0-20251211-4403a69a",
+ "@opentui/solid": "0.0.0-20251211-4403a69a",
"@parcel/watcher": "2.5.1",
- "@pierre/precision-diffs": "catalog:",
+ "@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
"@standard-schema/spec": "1.0.0",
"@zip.js/zip.js": "2.7.62",
"ai": "catalog:",
+ "bun-pty": "0.4.2",
"chokidar": "4.0.3",
"clipboardy": "4.0.0",
"decimal.js": "10.5.0",
diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts
index 98c332e32..a85fde9e2 100755
--- a/packages/opencode/script/build.ts
+++ b/packages/opencode/script/build.ts
@@ -16,6 +16,7 @@ import pkg from "../package.json"
import { Script } from "@opencode-ai/script"
const singleFlag = process.argv.includes("--single")
+const skipInstall = process.argv.includes("--skip-install")
const allTargets: {
os: string
@@ -83,8 +84,10 @@ const targets = singleFlag
await $`rm -rf dist`
const binaries: Record
= {}
-await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
-await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}`
+if (!skipInstall) {
+ await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
+ await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}`
+}
for (const item of targets) {
const name = [
pkg.name,
@@ -102,6 +105,10 @@ for (const item of targets) {
const parserWorker = fs.realpathSync(path.resolve(dir, "./node_modules/@opentui/core/parser.worker.js"))
const workerPath = "./src/cli/cmd/tui/worker.ts"
+ // Use platform-specific bunfs root path based on target OS
+ const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/"
+ const workerRelativePath = path.relative(dir, parserWorker).replaceAll("\\", "/")
+
await Bun.build({
conditions: ["browser"],
tsconfig: "./tsconfig.json",
@@ -110,6 +117,9 @@ for (const item of targets) {
compile: {
autoloadBunfig: false,
autoloadDotenv: false,
+ //@ts-ignore (bun types aren't up to date)
+ autoloadTsconfig: true,
+ autoloadPackageJson: true,
target: name.replace(pkg.name, "bun") as any,
outfile: `dist/${name}/bin/opencode`,
execArgv: [`--user-agent=opencode/${Script.version}`, "--"],
@@ -118,9 +128,10 @@ for (const item of targets) {
entrypoints: ["./src/index.ts", parserWorker, workerPath],
define: {
OPENCODE_VERSION: `'${Script.version}'`,
- OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(dir, parserWorker).replaceAll("\\", "/"),
+ OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
OPENCODE_WORKER_PATH: workerPath,
OPENCODE_CHANNEL: `'${Script.channel}'`,
+ OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
},
})
diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts
index 70d18905e..72632992f 100755
--- a/packages/opencode/script/publish.ts
+++ b/packages/opencode/script/publish.ts
@@ -35,26 +35,21 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
2,
),
)
-for (const [name] of Object.entries(binaries)) {
- try {
- process.chdir(`./dist/${name}`)
- if (process.platform !== "win32") {
- await $`chmod 755 -R .`
- }
- await $`bun publish --access public --tag ${Script.channel}`
- } finally {
- process.chdir(dir)
- }
-}
-await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${Script.channel}`
-if (!Script.preview) {
- const major = Script.version.split(".")[0]
- const majorTag = `latest-${major}`
- for (const [name] of Object.entries(binaries)) {
- await $`cd dist/${name} && npm dist-tag add ${name}@${Script.version} ${majorTag}`
+const tags = [Script.channel]
+
+const tasks = Object.entries(binaries).map(async ([name]) => {
+ if (process.platform !== "win32") {
+ await $`chmod 755 -R .`.cwd(`./dist/${name}`)
}
- await $`cd ./dist/${pkg.name} && npm dist-tag add ${pkg.name}-ai@${Script.version} ${majorTag}`
+ await $`bun pm pack`.cwd(`./dist/${name}`)
+ for (const tag of tags) {
+ await $`npm publish *.tgz --access public --tag ${tag}`.cwd(`./dist/${name}`)
+ }
+})
+await Promise.all(tasks)
+for (const tag of tags) {
+ await $`cd ./dist/${pkg.name} && bun pm pack && npm publish *.tgz --access public --tag ${tag}`
}
if (!Script.preview) {
@@ -90,7 +85,7 @@ if (!Script.preview) {
"license=('MIT')",
"provides=('opencode')",
"conflicts=('opencode')",
- "depends=('fzf' 'ripgrep')",
+ "depends=('ripgrep')",
"",
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/sst/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-arm64.tar.gz")`,
`sha256sums_aarch64=('${arm64Sha}')`,
@@ -120,7 +115,7 @@ if (!Script.preview) {
"license=('MIT')",
"provides=('opencode')",
"conflicts=('opencode-bin')",
- "depends=('fzf' 'ripgrep')",
+ "depends=('ripgrep')",
"makedepends=('git' 'bun-bin' 'go')",
"",
`source=("opencode-\${pkgver}.tar.gz::https://github.com/sst/opencode/archive/v\${pkgver}\${_subver}.tar.gz")`,
@@ -199,6 +194,8 @@ if (!Script.preview) {
` homepage "https://github.com/sst/opencode"`,
` version "${Script.version.split("-")[0]}"`,
"",
+ ` depends_on "ripgrep"`,
+ "",
" on_macos do",
" if Hardware::CPU.intel?",
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-darwin-x64.zip"`,
@@ -247,8 +244,8 @@ if (!Script.preview) {
await $`cd ./dist/homebrew-tap && git push`
const image = "ghcr.io/sst/opencode"
- await $`docker build -t ${image}:${Script.version} .`
- await $`docker push ${image}:${Script.version}`
- await $`docker tag ${image}:${Script.version} ${image}:latest`
- await $`docker push ${image}:latest`
+ const platforms = "linux/amd64,linux/arm64"
+ const tags = [`${image}:${Script.version}`, `${image}:latest`]
+ const tagFlags = tags.flatMap((t) => ["-t", t])
+ await $`docker buildx build --platform ${platforms} ${tagFlags} --push .`
}
diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts
index ff71b0453..d20c971eb 100644
--- a/packages/opencode/src/acp/agent.ts
+++ b/packages/opencode/src/acp/agent.ts
@@ -25,11 +25,10 @@ import { Provider } from "../provider/provider"
import { Installation } from "@/installation"
import { MessageV2 } from "@/session/message-v2"
import { Config } from "@/config/config"
-import { MCP } from "@/mcp"
import { Todo } from "@/session/todo"
import { z } from "zod"
import { LoadAPIKeyError } from "ai"
-import type { OpencodeClient } from "@opencode-ai/sdk"
+import type { OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
export namespace ACP {
const log = Log.create({ service: "acp-agent" })
@@ -68,7 +67,7 @@ export namespace ACP {
{ optionId: "always", kind: "allow_always", name: "Always allow" },
{ optionId: "reject", kind: "reject_once", name: "Reject" },
]
- this.config.sdk.event.subscribe({ query: { directory } }).then(async (events) => {
+ this.config.sdk.event.subscribe({ directory }).then(async (events) => {
for await (const event of events.stream) {
switch (event.type) {
case "permission.updated":
@@ -93,32 +92,29 @@ export namespace ACP {
permissionID: permission.id,
sessionID: permission.sessionID,
})
- await this.config.sdk.postSessionIdPermissionsPermissionId({
- path: { id: permission.sessionID, permissionID: permission.id },
- body: {
- response: "reject",
- },
- query: { directory },
+ await this.config.sdk.permission.respond({
+ sessionID: permission.sessionID,
+ permissionID: permission.id,
+ response: "reject",
+ directory,
})
return
})
if (!res) return
if (res.outcome.outcome !== "selected") {
- await this.config.sdk.postSessionIdPermissionsPermissionId({
- path: { id: permission.sessionID, permissionID: permission.id },
- body: {
- response: "reject",
- },
- query: { directory },
+ await this.config.sdk.permission.respond({
+ sessionID: permission.sessionID,
+ permissionID: permission.id,
+ response: "reject",
+ directory,
})
return
}
- await this.config.sdk.postSessionIdPermissionsPermissionId({
- path: { id: permission.sessionID, permissionID: permission.id },
- body: {
- response: res.outcome.optionId as "once" | "always" | "reject",
- },
- query: { directory },
+ await this.config.sdk.permission.respond({
+ sessionID: permission.sessionID,
+ permissionID: permission.id,
+ response: res.outcome.optionId as "once" | "always" | "reject",
+ directory,
})
} catch (err) {
log.error("unexpected error when handling permission", { error: err })
@@ -133,14 +129,14 @@ export namespace ACP {
const { part } = props
const message = await this.config.sdk.session
- .message({
- throwOnError: true,
- path: {
- id: part.sessionID,
+ .message(
+ {
+ sessionID: part.sessionID,
messageID: part.messageID,
+ directory,
},
- query: { directory },
- })
+ { throwOnError: true },
+ )
.then((x) => x.data)
.catch((err) => {
log.error("unexpected error when fetching message", { error: err })
@@ -390,7 +386,7 @@ export namespace ACP {
log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length })
- const load = await this.loadSession({
+ const load = await this.loadSessionMode({
cwd: directory,
mcpServers: params.mcpServers,
sessionId,
@@ -416,13 +412,247 @@ export namespace ACP {
}
async loadSession(params: LoadSessionRequest) {
+ const directory = params.cwd
+ const sessionId = params.sessionId
+
+ try {
+ const model = await defaultModel(this.config, directory)
+
+ // Store ACP session state
+ const state = await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model)
+
+ log.info("load_session", { sessionId, mcpServers: params.mcpServers.length })
+
+ const mode = await this.loadSessionMode({
+ cwd: directory,
+ mcpServers: params.mcpServers,
+ sessionId,
+ })
+
+ this.setupEventSubscriptions(state)
+
+ // Replay session history
+ const messages = await this.sdk.session
+ .messages(
+ {
+ sessionID: sessionId,
+ directory,
+ },
+ { throwOnError: true },
+ )
+ .then((x) => x.data)
+ .catch((err) => {
+ log.error("unexpected error when fetching message", { error: err })
+ return undefined
+ })
+
+ for (const msg of messages ?? []) {
+ log.debug("replay message", msg)
+ await this.processMessage(msg)
+ }
+
+ return mode
+ } catch (e) {
+ const error = MessageV2.fromError(e, {
+ providerID: this.config.defaultModel?.providerID ?? "unknown",
+ })
+ if (LoadAPIKeyError.isInstance(error)) {
+ throw RequestError.authRequired()
+ }
+ throw e
+ }
+ }
+
+ private async processMessage(message: SessionMessageResponse) {
+ log.debug("process message", message)
+ if (message.info.role !== "assistant" && message.info.role !== "user") return
+ const sessionId = message.info.sessionID
+
+ for (const part of message.parts) {
+ if (part.type === "tool") {
+ switch (part.state.status) {
+ case "pending":
+ await this.connection
+ .sessionUpdate({
+ sessionId,
+ update: {
+ sessionUpdate: "tool_call",
+ toolCallId: part.callID,
+ title: part.tool,
+ kind: toToolKind(part.tool),
+ status: "pending",
+ locations: [],
+ rawInput: {},
+ },
+ })
+ .catch((err) => {
+ log.error("failed to send tool pending to ACP", { error: err })
+ })
+ break
+ case "running":
+ await this.connection
+ .sessionUpdate({
+ sessionId,
+ update: {
+ sessionUpdate: "tool_call_update",
+ toolCallId: part.callID,
+ status: "in_progress",
+ locations: toLocations(part.tool, part.state.input),
+ rawInput: part.state.input,
+ },
+ })
+ .catch((err) => {
+ log.error("failed to send tool in_progress to ACP", { error: err })
+ })
+ break
+ case "completed":
+ const kind = toToolKind(part.tool)
+ const content: ToolCallContent[] = [
+ {
+ type: "content",
+ content: {
+ type: "text",
+ text: part.state.output,
+ },
+ },
+ ]
+
+ if (kind === "edit") {
+ const input = part.state.input
+ const filePath = typeof input["filePath"] === "string" ? input["filePath"] : ""
+ const oldText = typeof input["oldString"] === "string" ? input["oldString"] : ""
+ const newText =
+ typeof input["newString"] === "string"
+ ? input["newString"]
+ : typeof input["content"] === "string"
+ ? input["content"]
+ : ""
+ content.push({
+ type: "diff",
+ path: filePath,
+ oldText,
+ newText,
+ })
+ }
+
+ if (part.tool === "todowrite") {
+ const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
+ if (parsedTodos.success) {
+ await this.connection
+ .sessionUpdate({
+ sessionId,
+ update: {
+ sessionUpdate: "plan",
+ entries: parsedTodos.data.map((todo) => {
+ const status: PlanEntry["status"] =
+ todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
+ return {
+ priority: "medium",
+ status,
+ content: todo.content,
+ }
+ }),
+ },
+ })
+ .catch((err) => {
+ log.error("failed to send session update for todo", { error: err })
+ })
+ } else {
+ log.error("failed to parse todo output", { error: parsedTodos.error })
+ }
+ }
+
+ await this.connection
+ .sessionUpdate({
+ sessionId,
+ update: {
+ sessionUpdate: "tool_call_update",
+ toolCallId: part.callID,
+ status: "completed",
+ kind,
+ content,
+ title: part.state.title,
+ rawOutput: {
+ output: part.state.output,
+ metadata: part.state.metadata,
+ },
+ },
+ })
+ .catch((err) => {
+ log.error("failed to send tool completed to ACP", { error: err })
+ })
+ break
+ case "error":
+ await this.connection
+ .sessionUpdate({
+ sessionId,
+ update: {
+ sessionUpdate: "tool_call_update",
+ toolCallId: part.callID,
+ status: "failed",
+ content: [
+ {
+ type: "content",
+ content: {
+ type: "text",
+ text: part.state.error,
+ },
+ },
+ ],
+ rawOutput: {
+ error: part.state.error,
+ },
+ },
+ })
+ .catch((err) => {
+ log.error("failed to send tool error to ACP", { error: err })
+ })
+ break
+ }
+ } else if (part.type === "text") {
+ if (part.text) {
+ await this.connection
+ .sessionUpdate({
+ sessionId,
+ update: {
+ sessionUpdate: message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk",
+ content: {
+ type: "text",
+ text: part.text,
+ },
+ },
+ })
+ .catch((err) => {
+ log.error("failed to send text to ACP", { error: err })
+ })
+ }
+ } else if (part.type === "reasoning") {
+ if (part.text) {
+ await this.connection
+ .sessionUpdate({
+ sessionId,
+ update: {
+ sessionUpdate: "agent_thought_chunk",
+ content: {
+ type: "text",
+ text: part.text,
+ },
+ },
+ })
+ .catch((err) => {
+ log.error("failed to send reasoning to ACP", { error: err })
+ })
+ }
+ }
+ }
+ }
+
+ private async loadSessionMode(params: LoadSessionRequest) {
const directory = params.cwd
const model = await defaultModel(this.config, directory)
const sessionId = params.sessionId
- const providers = await this.sdk.config
- .providers({ throwOnError: true, query: { directory } })
- .then((x) => x.data.providers)
+ const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers)
const entries = providers.sort((a, b) => {
const nameA = a.name.toLowerCase()
const nameB = b.name.toLowerCase()
@@ -439,22 +669,22 @@ export namespace ACP {
})
const agents = await this.config.sdk.app
- .agents({
- throwOnError: true,
- query: {
+ .agents(
+ {
directory,
},
- })
- .then((resp) => resp.data)
+ { throwOnError: true },
+ )
+ .then((resp) => resp.data!)
const commands = await this.config.sdk.command
- .list({
- throwOnError: true,
- query: {
+ .list(
+ {
directory,
},
- })
- .then((resp) => resp.data)
+ { throwOnError: true },
+ )
+ .then((resp) => resp.data!)
const availableCommands = commands.map((command) => ({
name: command.name,
@@ -503,14 +733,14 @@ export namespace ACP {
await Promise.all(
Object.entries(mcpServers).map(async ([key, mcp]) => {
await this.sdk.mcp
- .add({
- throwOnError: true,
- query: { directory },
- body: {
+ .add(
+ {
+ directory,
name: key,
config: mcp,
},
- })
+ { throwOnError: true },
+ )
.catch((error) => {
log.error("failed to add mcp server", { name: key, error })
})
@@ -559,7 +789,7 @@ export namespace ACP {
async setSessionMode(params: SetSessionModeRequest): Promise {
this.sessionManager.get(params.sessionId)
await this.config.sdk.app
- .agents({ throwOnError: true })
+ .agents({}, { throwOnError: true })
.then((x) => x.data)
.then((agent) => {
if (!agent) throw new Error(`Agent not found: ${params.modeId}`)
@@ -651,50 +881,44 @@ export namespace ACP {
if (!cmd) {
await this.sdk.session.prompt({
- path: { id: sessionID },
- body: {
- model: {
- providerID: model.providerID,
- modelID: model.modelID,
- },
- parts,
- agent,
- },
- query: {
- directory,
+ sessionID,
+ model: {
+ providerID: model.providerID,
+ modelID: model.modelID,
},
+ parts,
+ agent,
+ directory,
})
return done
}
const command = await this.config.sdk.command
- .list({ throwOnError: true, query: { directory } })
- .then((x) => x.data.find((c) => c.name === cmd.name))
+ .list({ directory }, { throwOnError: true })
+ .then((x) => x.data!.find((c) => c.name === cmd.name))
if (command) {
await this.sdk.session.command({
- path: { id: sessionID },
- body: {
- command: command.name,
- arguments: cmd.args,
- model: model.providerID + "/" + model.modelID,
- agent,
- },
- query: {
- directory,
- },
+ sessionID,
+ command: command.name,
+ arguments: cmd.args,
+ model: model.providerID + "/" + model.modelID,
+ agent,
+ directory,
})
return done
}
switch (cmd.name) {
case "compact":
- await this.config.sdk.session.summarize({
- path: { id: sessionID },
- throwOnError: true,
- query: {
+ await this.config.sdk.session.summarize(
+ {
+ sessionID,
directory,
+ providerID: model.providerID,
+ modelID: model.modelID,
},
- })
+ { throwOnError: true },
+ )
break
}
@@ -703,13 +927,13 @@ export namespace ACP {
async cancel(params: CancelNotification) {
const session = this.sessionManager.get(params.sessionId)
- await this.config.sdk.session.abort({
- path: { id: params.sessionId },
- throwOnError: true,
- query: {
+ await this.config.sdk.session.abort(
+ {
+ sessionID: params.sessionId,
directory: session.cwd,
},
- })
+ { throwOnError: true },
+ )
}
}
@@ -766,10 +990,10 @@ export namespace ACP {
if (configured) return configured
const model = await sdk.config
- .get({ throwOnError: true, query: { directory: cwd } })
+ .get({ directory: cwd }, { throwOnError: true })
.then((resp) => {
const cfg = resp.data
- if (!cfg.model) return undefined
+ if (!cfg || !cfg.model) return undefined
const parsed = Provider.parseModel(cfg.model)
return {
providerID: parsed.providerID,
diff --git a/packages/opencode/src/acp/session.ts b/packages/opencode/src/acp/session.ts
index 63948a8c1..70b658347 100644
--- a/packages/opencode/src/acp/session.ts
+++ b/packages/opencode/src/acp/session.ts
@@ -1,7 +1,7 @@
import { RequestError, type McpServer } from "@agentclientprotocol/sdk"
import type { ACPSessionState } from "./types"
import { Log } from "@/util/log"
-import type { OpencodeClient } from "@opencode-ai/sdk"
+import type { OpencodeClient } from "@opencode-ai/sdk/v2"
const log = Log.create({ service: "acp-session-manager" })
@@ -15,16 +15,14 @@ export class ACPSessionManager {
async create(cwd: string, mcpServers: McpServer[], model?: ACPSessionState["model"]): Promise {
const session = await this.sdk.session
- .create({
- body: {
+ .create(
+ {
title: `ACP Session ${crypto.randomUUID()}`,
- },
- query: {
directory: cwd,
},
- throwOnError: true,
- })
- .then((x) => x.data)
+ { throwOnError: true },
+ )
+ .then((x) => x.data!)
const sessionId = session.id
const resolvedModel = model
@@ -42,6 +40,37 @@ export class ACPSessionManager {
return state
}
+ async load(
+ sessionId: string,
+ cwd: string,
+ mcpServers: McpServer[],
+ model?: ACPSessionState["model"],
+ ): Promise {
+ const session = await this.sdk.session
+ .get(
+ {
+ sessionID: sessionId,
+ directory: cwd,
+ },
+ { throwOnError: true },
+ )
+ .then((x) => x.data!)
+
+ const resolvedModel = model
+
+ const state: ACPSessionState = {
+ id: sessionId,
+ cwd,
+ mcpServers,
+ createdAt: new Date(session.time.created),
+ model: resolvedModel,
+ }
+ log.info("loading_session", { state })
+
+ this.sessions.set(sessionId, state)
+ return state
+ }
+
get(sessionId: string): ACPSessionState {
const session = this.sessions.get(sessionId)
if (!session) {
diff --git a/packages/opencode/src/acp/types.ts b/packages/opencode/src/acp/types.ts
index 8507228ed..42b230912 100644
--- a/packages/opencode/src/acp/types.ts
+++ b/packages/opencode/src/acp/types.ts
@@ -1,5 +1,5 @@
import type { McpServer } from "@agentclientprotocol/sdk"
-import type { OpencodeClient } from "@opencode-ai/sdk"
+import type { OpencodeClient } from "@opencode-ai/sdk/v2"
export interface ACPSessionState {
id: string
diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index 234b99b3e..add120f91 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/packages/opencode/src/agent/agent.ts
@@ -2,18 +2,24 @@ import { Config } from "../config/config"
import z from "zod"
import { Provider } from "../provider/provider"
import { generateObject, type ModelMessage } from "ai"
-import PROMPT_GENERATE from "./generate.txt"
import { SystemPrompt } from "../session/system"
import { Instance } from "../project/instance"
import { mergeDeep } from "remeda"
+import PROMPT_GENERATE from "./generate.txt"
+import PROMPT_COMPACTION from "./prompt/compaction.txt"
+import PROMPT_EXPLORE from "./prompt/explore.txt"
+import PROMPT_SUMMARY from "./prompt/summary.txt"
+import PROMPT_TITLE from "./prompt/title.txt"
+
export namespace Agent {
export const Info = z
.object({
name: z.string(),
description: z.string().optional(),
mode: z.enum(["subagent", "primary", "all"]),
- builtIn: z.boolean(),
+ native: z.boolean().optional(),
+ hidden: z.boolean().optional(),
topP: z.number().optional(),
temperature: z.number().optional(),
color: z.string().optional(),
@@ -33,6 +39,7 @@ export namespace Agent {
prompt: z.string().optional(),
tools: z.record(z.string(), z.boolean()),
options: z.record(z.string(), z.any()),
+ maxSteps: z.number().int().positive().optional(),
})
.meta({
ref: "Agent",
@@ -100,10 +107,27 @@ export namespace Agent {
)
const result: Record = {
+ build: {
+ name: "build",
+ tools: { ...defaultTools },
+ options: {},
+ permission: agentPermission,
+ mode: "primary",
+ native: true,
+ },
+ plan: {
+ name: "plan",
+ options: {},
+ permission: planPermission,
+ tools: {
+ ...defaultTools,
+ },
+ mode: "primary",
+ native: true,
+ },
general: {
name: "general",
- description:
- "General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.",
+ description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`,
tools: {
todoread: false,
todowrite: false,
@@ -112,7 +136,8 @@ export namespace Agent {
options: {},
permission: agentPermission,
mode: "subagent",
- builtIn: true,
+ native: true,
+ hidden: true,
},
explore: {
name: "explore",
@@ -124,48 +149,43 @@ export namespace Agent {
...defaultTools,
},
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.`,
- prompt: [
- `You are a file search specialist. You excel at thoroughly navigating and exploring codebases.`,
- ``,
- `Your strengths:`,
- `- Rapidly finding files using glob patterns`,
- `- Searching code and text with powerful regex patterns`,
- `- Reading and analyzing file contents`,
- ``,
- `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`,
- `- Do not create any files, or run bash commands that modify the user's system state in any way`,
- ``,
- `Complete the user's search request efficiently and report your findings clearly.`,
- ].join("\n"),
+ prompt: PROMPT_EXPLORE,
options: {},
permission: agentPermission,
mode: "subagent",
- builtIn: true,
+ native: true,
},
- build: {
- name: "build",
- tools: { ...defaultTools },
+ compaction: {
+ name: "compaction",
+ mode: "primary",
+ native: true,
+ hidden: true,
+ prompt: PROMPT_COMPACTION,
+ tools: {
+ "*": false,
+ },
options: {},
permission: agentPermission,
- mode: "primary",
- builtIn: true,
},
- plan: {
- name: "plan",
- options: {},
- permission: planPermission,
- tools: {
- ...defaultTools,
- },
+ title: {
+ name: "title",
mode: "primary",
- builtIn: true,
+ options: {},
+ native: true,
+ hidden: true,
+ permission: agentPermission,
+ prompt: PROMPT_TITLE,
+ tools: {},
+ },
+ summary: {
+ name: "summary",
+ mode: "primary",
+ options: {},
+ native: true,
+ hidden: true,
+ permission: agentPermission,
+ prompt: PROMPT_SUMMARY,
+ tools: {},
},
}
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
@@ -181,9 +201,22 @@ export namespace Agent {
permission: agentPermission,
options: {},
tools: {},
- builtIn: false,
+ native: false,
}
- const { name, model, prompt, tools, description, temperature, top_p, mode, permission, color, ...extra } = value
+ const {
+ name,
+ model,
+ prompt,
+ tools,
+ description,
+ temperature,
+ top_p,
+ mode,
+ permission,
+ color,
+ maxSteps,
+ ...extra
+ } = value
item.options = {
...item.options,
...extra,
@@ -206,6 +239,7 @@ export namespace Agent {
if (color) item.color = color
// just here for consistency & to prevent it from being added as an option
if (name) item.name = name
+ if (maxSteps != undefined) item.maxSteps = maxSteps
if (permission ?? cfg.permission) {
item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})
@@ -222,15 +256,23 @@ export namespace Agent {
return state().then((x) => Object.values(x))
}
- export async function generate(input: { description: string }) {
- const defaultModel = await Provider.defaultModel()
+ export async function generate(input: { description: string; model?: { providerID: string; modelID: string } }) {
+ const cfg = await Config.get()
+ const defaultModel = input.model ?? (await Provider.defaultModel())
const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
+ const language = await Provider.getLanguage(model)
const system = SystemPrompt.header(defaultModel.providerID)
system.push(PROMPT_GENERATE)
const existing = await list()
const result = await generateObject({
+ experimental_telemetry: {
+ isEnabled: cfg.experimental?.openTelemetry,
+ metadata: {
+ userId: cfg.username ?? "unknown",
+ },
+ },
temperature: 0.3,
- prompt: [
+ messages: [
...system.map(
(item): ModelMessage => ({
role: "system",
@@ -242,7 +284,7 @@ export namespace Agent {
content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`,
},
],
- model: model.language,
+ model: language,
schema: z.object({
identifier: z.string(),
whenToUse: z.string(),
diff --git a/packages/opencode/src/session/prompt/compaction.txt b/packages/opencode/src/agent/prompt/compaction.txt
similarity index 79%
rename from packages/opencode/src/session/prompt/compaction.txt
rename to packages/opencode/src/agent/prompt/compaction.txt
index 4751c8d71..b919671a0 100644
--- a/packages/opencode/src/session/prompt/compaction.txt
+++ b/packages/opencode/src/agent/prompt/compaction.txt
@@ -6,5 +6,7 @@ Focus on information that would be helpful for continuing the conversation, incl
- What is currently being worked on
- Which files are being modified
- What needs to be done next
+- Key user requests, constraints, or preferences that should persist
+- Important technical decisions and why they were made
Your summary should be comprehensive enough to provide context but concise enough to be quickly understood.
diff --git a/packages/opencode/src/agent/prompt/explore.txt b/packages/opencode/src/agent/prompt/explore.txt
new file mode 100644
index 000000000..5761077cb
--- /dev/null
+++ b/packages/opencode/src/agent/prompt/explore.txt
@@ -0,0 +1,18 @@
+You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
+
+Your strengths:
+- Rapidly finding files using glob patterns
+- Searching code and text with powerful regex patterns
+- Reading and analyzing file contents
+
+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
+- Do not create any files, or run bash commands that modify the user's system state in any way
+
+Complete the user's search request efficiently and report your findings clearly.
diff --git a/packages/opencode/src/session/prompt/summarize.txt b/packages/opencode/src/agent/prompt/summary.txt
similarity index 100%
rename from packages/opencode/src/session/prompt/summarize.txt
rename to packages/opencode/src/agent/prompt/summary.txt
diff --git a/packages/opencode/src/session/prompt/title.txt b/packages/opencode/src/agent/prompt/title.txt
similarity index 84%
rename from packages/opencode/src/session/prompt/title.txt
rename to packages/opencode/src/agent/prompt/title.txt
index e297dc460..f67aaa95b 100644
--- a/packages/opencode/src/session/prompt/title.txt
+++ b/packages/opencode/src/agent/prompt/title.txt
@@ -22,8 +22,8 @@ Your output must be:
- The title should NEVER include "summarizing" or "generating" when generating a title
- DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT
- Always output something meaningful, even if the input is minimal.
-- If the user message is short or conversational (e.g. “hello”, “lol”, “whats up”, “hey”):
- → create a title that reflects the user’s tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.)
+- If the user message is short or conversational (e.g. "hello", "lol", "whats up", "hey"):
+ → create a title that reflects the user's tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.)
diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts
index 883b9acc6..b9c8a78ca 100644
--- a/packages/opencode/src/auth/index.ts
+++ b/packages/opencode/src/auth/index.ts
@@ -35,16 +35,22 @@ export namespace Auth {
const filepath = path.join(Global.Path.data, "auth.json")
export async function get(providerID: string) {
- const file = Bun.file(filepath)
- return file
- .json()
- .catch(() => ({}))
- .then((x) => x[providerID] as Info | undefined)
+ const auth = await all()
+ return auth[providerID]
}
export async function all(): Promise> {
const file = Bun.file(filepath)
- return file.json().catch(() => ({}))
+ const data = await file.json().catch(() => ({}) as Record)
+ return Object.entries(data).reduce(
+ (acc, [key, value]) => {
+ const parsed = Info.safeParse(value)
+ if (!parsed.success) return acc
+ acc[key] = parsed.data
+ return acc
+ },
+ {} as Record,
+ )
}
export async function set(key: string, info: Info) {
diff --git a/packages/opencode/src/bun/index.ts b/packages/opencode/src/bun/index.ts
index edf74c310..5456d0a5b 100644
--- a/packages/opencode/src/bun/index.ts
+++ b/packages/opencode/src/bun/index.ts
@@ -85,49 +85,29 @@ export namespace BunProc {
version,
})
- const total = 3
- const wait = 500
+ await BunProc.run(args, {
+ cwd: Global.Path.cache,
+ }).catch((e) => {
+ throw new InstallFailedError(
+ { pkg, version },
+ {
+ cause: e,
+ },
+ )
+ })
- const runInstall = async (count: number = 1): Promise => {
- log.info("bun install attempt", {
- pkg,
- version,
- attempt: count,
- total,
- })
- await BunProc.run(args, {
- cwd: Global.Path.cache,
- }).catch(async (error) => {
- log.warn("bun install failed", {
- pkg,
- version,
- attempt: count,
- total,
- error,
- })
- if (count >= total) {
- throw new InstallFailedError(
- { pkg, version },
- {
- cause: error,
- },
- )
- }
- const delay = wait * count
- log.info("bun install retrying", {
- pkg,
- version,
- next: count + 1,
- delay,
- })
- await Bun.sleep(delay)
- return runInstall(count + 1)
- })
+ // Resolve actual version from installed package when using "latest"
+ // This ensures subsequent starts use the cached version until explicitly updated
+ let resolvedVersion = version
+ if (version === "latest") {
+ const installedPkgJson = Bun.file(path.join(mod, "package.json"))
+ const installedPkg = await installedPkgJson.json().catch(() => null)
+ if (installedPkg?.version) {
+ resolvedVersion = installedPkg.version
+ }
}
- await runInstall()
-
- parsed.dependencies[pkg] = version
+ parsed.dependencies[pkg] = resolvedVersion
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
return mod
}
diff --git a/packages/opencode/src/bus/bus-event.ts b/packages/opencode/src/bus/bus-event.ts
new file mode 100644
index 000000000..7fe13833c
--- /dev/null
+++ b/packages/opencode/src/bus/bus-event.ts
@@ -0,0 +1,43 @@
+import z from "zod"
+import type { ZodType } from "zod"
+import { Log } from "../util/log"
+
+export namespace BusEvent {
+ const log = Log.create({ service: "event" })
+
+ export type Definition = ReturnType
+
+ const registry = new Map()
+
+ export function define(type: Type, properties: Properties) {
+ const result = {
+ type,
+ properties,
+ }
+ registry.set(type, result)
+ return result
+ }
+
+ export function payloads() {
+ return z
+ .discriminatedUnion(
+ "type",
+ registry
+ .entries()
+ .map(([type, def]) => {
+ return z
+ .object({
+ type: z.literal(type),
+ properties: def.properties,
+ })
+ .meta({
+ ref: "Event" + "." + def.type,
+ })
+ })
+ .toArray() as any,
+ )
+ .meta({
+ ref: "Event",
+ })
+ }
+}
diff --git a/packages/opencode/src/bus/global.ts b/packages/opencode/src/bus/global.ts
index b592cd398..43386dd6b 100644
--- a/packages/opencode/src/bus/global.ts
+++ b/packages/opencode/src/bus/global.ts
@@ -3,7 +3,7 @@ import { EventEmitter } from "events"
export const GlobalBus = new EventEmitter<{
event: [
{
- directory: string
+ directory?: string
payload: any
},
]
diff --git a/packages/opencode/src/bus/index.ts b/packages/opencode/src/bus/index.ts
index aace87585..edb093f19 100644
--- a/packages/opencode/src/bus/index.ts
+++ b/packages/opencode/src/bus/index.ts
@@ -1,58 +1,44 @@
import z from "zod"
-import type { ZodType } from "zod"
import { Log } from "../util/log"
import { Instance } from "../project/instance"
+import { BusEvent } from "./bus-event"
import { GlobalBus } from "./global"
export namespace Bus {
const log = Log.create({ service: "bus" })
type Subscription = (event: any) => void
- const state = Instance.state(() => {
- const subscriptions = new Map()
+ export const InstanceDisposed = BusEvent.define(
+ "server.instance.disposed",
+ z.object({
+ directory: z.string(),
+ }),
+ )
- return {
- subscriptions,
- }
- })
+ const state = Instance.state(
+ () => {
+ const subscriptions = new Map()
- export type EventDefinition = ReturnType
+ return {
+ subscriptions,
+ }
+ },
+ async (entry) => {
+ const wildcard = entry.subscriptions.get("*")
+ if (!wildcard) return
+ const event = {
+ type: InstanceDisposed.type,
+ properties: {
+ directory: Instance.directory,
+ },
+ }
+ for (const sub of [...wildcard]) {
+ sub(event)
+ }
+ },
+ )
- const registry = new Map()
-
- export function event(type: Type, properties: Properties) {
- const result = {
- type,
- properties,
- }
- registry.set(type, result)
- return result
- }
-
- export function payloads() {
- return z
- .discriminatedUnion(
- "type",
- registry
- .entries()
- .map(([type, def]) => {
- return z
- .object({
- type: z.literal(type),
- properties: def.properties,
- })
- .meta({
- ref: "Event" + "." + def.type,
- })
- })
- .toArray() as any,
- )
- .meta({
- ref: "Event",
- })
- }
-
- export async function publish(
+ export async function publish(
def: Definition,
properties: z.output,
) {
@@ -77,14 +63,14 @@ export namespace Bus {
return Promise.all(pending)
}
- export function subscribe(
+ export function subscribe(
def: Definition,
callback: (event: { type: Definition["type"]; properties: z.infer }) => void,
) {
return raw(def.type, callback)
}
- export function once(
+ export function once(
def: Definition,
callback: (event: {
type: Definition["type"]
diff --git a/packages/opencode/src/cli/cmd/acp.ts b/packages/opencode/src/cli/cmd/acp.ts
index 7d27f9416..c607e5f5b 100644
--- a/packages/opencode/src/cli/cmd/acp.ts
+++ b/packages/opencode/src/cli/cmd/acp.ts
@@ -4,7 +4,7 @@ import { cmd } from "./cmd"
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
import { ACP } from "@/acp/agent"
import { Server } from "@/server/server"
-import { createOpencodeClient } from "@opencode-ai/sdk"
+import { createOpencodeClient } from "@opencode-ai/sdk/v2"
const log = Log.create({ service: "acp-command" })
@@ -17,7 +17,7 @@ process.on("unhandledRejection", (reason, promise) => {
export const AcpCommand = cmd({
command: "acp",
- describe: "Start ACP (Agent Client Protocol) server",
+ describe: "start ACP (Agent Client Protocol) server",
builder: (yargs) => {
return yargs
.option("cwd", {
diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts
index a774c6d02..60dd9cc75 100644
--- a/packages/opencode/src/cli/cmd/agent.ts
+++ b/packages/opencode/src/cli/cmd/agent.ts
@@ -3,132 +3,223 @@ import * as prompts from "@clack/prompts"
import { UI } from "../ui"
import { Global } from "../../global"
import { Agent } from "../../agent/agent"
+import { Provider } from "../../provider/provider"
import path from "path"
+import fs from "fs/promises"
import matter from "gray-matter"
import { Instance } from "../../project/instance"
import { EOL } from "os"
+import type { Argv } from "yargs"
+
+type AgentMode = "all" | "primary" | "subagent"
+
+const AVAILABLE_TOOLS = [
+ "bash",
+ "read",
+ "write",
+ "edit",
+ "list",
+ "glob",
+ "grep",
+ "webfetch",
+ "task",
+ "todowrite",
+ "todoread",
+]
const AgentCreateCommand = cmd({
command: "create",
describe: "create a new agent",
- async handler() {
+ builder: (yargs: Argv) =>
+ yargs
+ .option("path", {
+ type: "string",
+ describe: "directory path to generate the agent file",
+ })
+ .option("description", {
+ type: "string",
+ describe: "what the agent should do",
+ })
+ .option("mode", {
+ type: "string",
+ describe: "agent mode",
+ choices: ["all", "primary", "subagent"] as const,
+ })
+ .option("tools", {
+ type: "string",
+ describe: `comma-separated list of tools to enable (default: all). Available: "${AVAILABLE_TOOLS.join(", ")}"`,
+ })
+ .option("model", {
+ type: "string",
+ alias: ["m"],
+ describe: "model to use in the format of provider/model",
+ }),
+ async handler(args) {
await Instance.provide({
directory: process.cwd(),
async fn() {
- UI.empty()
- prompts.intro("Create agent")
- const project = Instance.project
+ const cliPath = args.path
+ const cliDescription = args.description
+ const cliMode = args.mode as AgentMode | undefined
+ const cliTools = args.tools
- let scope: "global" | "project" = "global"
- if (project.vcs === "git") {
- const scopeResult = await prompts.select({
- message: "Location",
- options: [
- {
- label: "Current project",
- value: "project" as const,
- hint: Instance.worktree,
- },
- {
- label: "Global",
- value: "global" as const,
- hint: Global.Path.config,
- },
- ],
- })
- if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
- scope = scopeResult
+ const isFullyNonInteractive = cliPath && cliDescription && cliMode && cliTools !== undefined
+
+ if (!isFullyNonInteractive) {
+ UI.empty()
+ prompts.intro("Create agent")
}
- const query = await prompts.text({
- message: "Description",
- placeholder: "What should this agent do?",
- validate: (x) => (x && x.length > 0 ? undefined : "Required"),
- })
- if (prompts.isCancel(query)) throw new UI.CancelledError()
+ const project = Instance.project
+ // Determine scope/path
+ let targetPath: string
+ if (cliPath) {
+ targetPath = path.join(cliPath, "agent")
+ } else {
+ let scope: "global" | "project" = "global"
+ if (project.vcs === "git") {
+ const scopeResult = await prompts.select({
+ message: "Location",
+ options: [
+ {
+ label: "Current project",
+ value: "project" as const,
+ hint: Instance.worktree,
+ },
+ {
+ label: "Global",
+ value: "global" as const,
+ hint: Global.Path.config,
+ },
+ ],
+ })
+ if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
+ scope = scopeResult
+ }
+ targetPath = path.join(
+ scope === "global" ? Global.Path.config : path.join(Instance.worktree, ".opencode"),
+ "agent",
+ )
+ }
+
+ // Get description
+ let description: string
+ if (cliDescription) {
+ description = cliDescription
+ } else {
+ const query = await prompts.text({
+ message: "Description",
+ placeholder: "What should this agent do?",
+ validate: (x) => (x && x.length > 0 ? undefined : "Required"),
+ })
+ if (prompts.isCancel(query)) throw new UI.CancelledError()
+ description = query
+ }
+
+ // Generate agent
const spinner = prompts.spinner()
-
spinner.start("Generating agent configuration...")
- const generated = await Agent.generate({ description: query }).catch((error) => {
+ const model = args.model ? Provider.parseModel(args.model) : undefined
+ const generated = await Agent.generate({ description, model }).catch((error) => {
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
+ if (isFullyNonInteractive) process.exit(1)
throw new UI.CancelledError()
})
spinner.stop(`Agent ${generated.identifier} generated`)
- const availableTools = [
- "bash",
- "read",
- "write",
- "edit",
- "list",
- "glob",
- "grep",
- "webfetch",
- "task",
- "todowrite",
- "todoread",
- ]
+ // Select tools
+ let selectedTools: string[]
+ if (cliTools !== undefined) {
+ selectedTools = cliTools ? cliTools.split(",").map((t) => t.trim()) : AVAILABLE_TOOLS
+ } else {
+ const result = await prompts.multiselect({
+ message: "Select tools to enable",
+ options: AVAILABLE_TOOLS.map((tool) => ({
+ label: tool,
+ value: tool,
+ })),
+ initialValues: AVAILABLE_TOOLS,
+ })
+ if (prompts.isCancel(result)) throw new UI.CancelledError()
+ selectedTools = result
+ }
- const selectedTools = await prompts.multiselect({
- message: "Select tools to enable",
- options: availableTools.map((tool) => ({
- label: tool,
- value: tool,
- })),
- initialValues: availableTools,
- })
- if (prompts.isCancel(selectedTools)) throw new UI.CancelledError()
-
- const modeResult = await prompts.select({
- message: "Agent mode",
- options: [
- {
- label: "All",
- value: "all" as const,
- hint: "Can function in both primary and subagent roles",
- },
- {
- label: "Primary",
- value: "primary" as const,
- hint: "Acts as a primary/main agent",
- },
- {
- label: "Subagent",
- value: "subagent" as const,
- hint: "Can be used as a subagent by other agents",
- },
- ],
- initialValue: "all",
- })
- if (prompts.isCancel(modeResult)) throw new UI.CancelledError()
+ // Get mode
+ let mode: AgentMode
+ if (cliMode) {
+ mode = cliMode
+ } else {
+ const modeResult = await prompts.select({
+ message: "Agent mode",
+ options: [
+ {
+ label: "All",
+ value: "all" as const,
+ hint: "Can function in both primary and subagent roles",
+ },
+ {
+ label: "Primary",
+ value: "primary" as const,
+ hint: "Acts as a primary/main agent",
+ },
+ {
+ label: "Subagent",
+ value: "subagent" as const,
+ hint: "Can be used as a subagent by other agents",
+ },
+ ],
+ initialValue: "all" as const,
+ })
+ if (prompts.isCancel(modeResult)) throw new UI.CancelledError()
+ mode = modeResult
+ }
+ // Build tools config
const tools: Record = {}
- for (const tool of availableTools) {
+ for (const tool of AVAILABLE_TOOLS) {
if (!selectedTools.includes(tool)) {
tools[tool] = false
}
}
- const frontmatter: any = {
+ // Build frontmatter
+ const frontmatter: {
+ description: string
+ mode: AgentMode
+ tools?: Record
+ } = {
description: generated.whenToUse,
- mode: modeResult,
+ mode,
}
if (Object.keys(tools).length > 0) {
frontmatter.tools = tools
}
+ // Write file
const content = matter.stringify(generated.systemPrompt, frontmatter)
- const filePath = path.join(
- scope === "global" ? Global.Path.config : path.join(Instance.worktree, ".opencode"),
- `agent`,
- `${generated.identifier}.md`,
- )
+ const filePath = path.join(targetPath, `${generated.identifier}.md`)
+
+ await fs.mkdir(targetPath, { recursive: true })
+
+ const file = Bun.file(filePath)
+ if (await file.exists()) {
+ if (isFullyNonInteractive) {
+ console.error(`Error: Agent file already exists: ${filePath}`)
+ process.exit(1)
+ }
+ prompts.log.error(`Agent file already exists: ${filePath}`)
+ throw new UI.CancelledError()
+ }
await Bun.write(filePath, content)
- prompts.log.success(`Agent created: ${filePath}`)
- prompts.outro("Done")
+ if (isFullyNonInteractive) {
+ console.log(filePath)
+ } else {
+ prompts.log.success(`Agent created: ${filePath}`)
+ prompts.outro("Done")
+ }
},
})
},
@@ -143,8 +234,8 @@ const AgentListCommand = cmd({
async fn() {
const agents = await Agent.list()
const sortedAgents = agents.sort((a, b) => {
- if (a.builtIn !== b.builtIn) {
- return a.builtIn ? -1 : 1
+ if (a.native !== b.native) {
+ return a.native ? -1 : 1
}
return a.name.localeCompare(b.name)
})
diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts
index ae24fbef5..658329fb6 100644
--- a/packages/opencode/src/cli/cmd/auth.ts
+++ b/packages/opencode/src/cli/cmd/auth.ts
@@ -6,9 +6,158 @@ import { ModelsDev } from "../../provider/models"
import { map, pipe, sortBy, values } from "remeda"
import path from "path"
import os from "os"
+import { Config } from "../../config/config"
import { Global } from "../../global"
import { Plugin } from "../../plugin"
import { Instance } from "../../project/instance"
+import type { Hooks } from "@opencode-ai/plugin"
+
+type PluginAuth = NonNullable
+
+/**
+ * Handle plugin-based authentication flow.
+ * Returns true if auth was handled, false if it should fall through to default handling.
+ */
+async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): Promise {
+ let index = 0
+ if (plugin.auth.methods.length > 1) {
+ const method = await prompts.select({
+ message: "Login method",
+ options: [
+ ...plugin.auth.methods.map((x, index) => ({
+ label: x.label,
+ value: index.toString(),
+ })),
+ ],
+ })
+ if (prompts.isCancel(method)) throw new UI.CancelledError()
+ index = parseInt(method)
+ }
+ const method = plugin.auth.methods[index]
+
+ // Handle prompts for all auth types
+ await new Promise((resolve) => setTimeout(resolve, 10))
+ const inputs: Record = {}
+ if (method.prompts) {
+ for (const prompt of method.prompts) {
+ if (prompt.condition && !prompt.condition(inputs)) {
+ continue
+ }
+ if (prompt.type === "select") {
+ const value = await prompts.select({
+ message: prompt.message,
+ options: prompt.options,
+ })
+ if (prompts.isCancel(value)) throw new UI.CancelledError()
+ inputs[prompt.key] = value
+ } else {
+ const value = await prompts.text({
+ message: prompt.message,
+ placeholder: prompt.placeholder,
+ validate: prompt.validate ? (v) => prompt.validate!(v ?? "") : undefined,
+ })
+ if (prompts.isCancel(value)) throw new UI.CancelledError()
+ inputs[prompt.key] = value
+ }
+ }
+ }
+
+ if (method.type === "oauth") {
+ const authorize = await method.authorize(inputs)
+
+ if (authorize.url) {
+ prompts.log.info("Go to: " + authorize.url)
+ }
+
+ if (authorize.method === "auto") {
+ if (authorize.instructions) {
+ prompts.log.info(authorize.instructions)
+ }
+ const spinner = prompts.spinner()
+ spinner.start("Waiting for authorization...")
+ const result = await authorize.callback()
+ if (result.type === "failed") {
+ spinner.stop("Failed to authorize", 1)
+ }
+ if (result.type === "success") {
+ const saveProvider = result.provider ?? provider
+ if ("refresh" in result) {
+ const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
+ await Auth.set(saveProvider, {
+ type: "oauth",
+ refresh,
+ access,
+ expires,
+ ...extraFields,
+ })
+ }
+ if ("key" in result) {
+ await Auth.set(saveProvider, {
+ type: "api",
+ key: result.key,
+ })
+ }
+ spinner.stop("Login successful")
+ }
+ }
+
+ if (authorize.method === "code") {
+ const code = await prompts.text({
+ message: "Paste the authorization code here: ",
+ validate: (x) => (x && x.length > 0 ? undefined : "Required"),
+ })
+ if (prompts.isCancel(code)) throw new UI.CancelledError()
+ const result = await authorize.callback(code)
+ if (result.type === "failed") {
+ prompts.log.error("Failed to authorize")
+ }
+ if (result.type === "success") {
+ const saveProvider = result.provider ?? provider
+ if ("refresh" in result) {
+ const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
+ await Auth.set(saveProvider, {
+ type: "oauth",
+ refresh,
+ access,
+ expires,
+ ...extraFields,
+ })
+ }
+ if ("key" in result) {
+ await Auth.set(saveProvider, {
+ type: "api",
+ key: result.key,
+ })
+ }
+ prompts.log.success("Login successful")
+ }
+ }
+
+ prompts.outro("Done")
+ return true
+ }
+
+ if (method.type === "api") {
+ if (method.authorize) {
+ const result = await method.authorize(inputs)
+ if (result.type === "failed") {
+ prompts.log.error("Failed to authorize")
+ }
+ if (result.type === "success") {
+ const saveProvider = result.provider ?? provider
+ await Auth.set(saveProvider, {
+ type: "api",
+ key: result.key,
+ })
+ prompts.log.success("Login successful")
+ }
+ prompts.outro("Done")
+ return true
+ }
+ }
+
+ return false
+}
export const AuthCommand = cmd({
command: "auth",
@@ -28,7 +177,7 @@ export const AuthListCommand = cmd({
const homedir = os.homedir()
const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
- const results = await Auth.all().then((x) => Object.entries(x))
+ const results = Object.entries(await Auth.all())
const database = await ModelsDev.get()
for (const [providerID, result] of results) {
@@ -103,7 +252,22 @@ export const AuthLoginCommand = cmd({
return
}
await ModelsDev.refresh().catch(() => {})
- const providers = await ModelsDev.get()
+
+ const config = await Config.get()
+
+ const disabled = new Set(config.disabled_providers ?? [])
+ const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
+
+ const providers = await ModelsDev.get().then((x) => {
+ const filtered: Record = {}
+ for (const [key, value] of Object.entries(x)) {
+ if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
+ filtered[key] = value
+ }
+ }
+ return filtered
+ })
+
const priority: Record = {
opencode: 0,
anthropic: 1,
@@ -127,7 +291,10 @@ export const AuthLoginCommand = cmd({
map((x) => ({
label: x.name,
value: x.id,
- hint: priority[x.id] <= 1 ? "recommended" : undefined,
+ hint: {
+ opencode: "recommended",
+ anthropic: "Claude Max or API key",
+ }[x.id],
})),
),
{
@@ -141,142 +308,8 @@ export const AuthLoginCommand = cmd({
const plugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider))
if (plugin && plugin.auth) {
- let index = 0
- if (plugin.auth.methods.length > 1) {
- const method = await prompts.select({
- message: "Login method",
- options: [
- ...plugin.auth.methods.map((x, index) => ({
- label: x.label,
- value: index.toString(),
- })),
- ],
- })
- if (prompts.isCancel(method)) throw new UI.CancelledError()
- index = parseInt(method)
- }
- const method = plugin.auth.methods[index]
-
- // Handle prompts for all auth types
- await new Promise((resolve) => setTimeout(resolve, 10))
- const inputs: Record = {}
- if (method.prompts) {
- for (const prompt of method.prompts) {
- if (prompt.condition && !prompt.condition(inputs)) {
- continue
- }
- if (prompt.type === "select") {
- const value = await prompts.select({
- message: prompt.message,
- options: prompt.options,
- })
- if (prompts.isCancel(value)) throw new UI.CancelledError()
- inputs[prompt.key] = value
- } else {
- const value = await prompts.text({
- message: prompt.message,
- placeholder: prompt.placeholder,
- validate: prompt.validate ? (v) => prompt.validate!(v ?? "") : undefined,
- })
- if (prompts.isCancel(value)) throw new UI.CancelledError()
- inputs[prompt.key] = value
- }
- }
- }
-
- if (method.type === "oauth") {
- const authorize = await method.authorize(inputs)
-
- if (authorize.url) {
- prompts.log.info("Go to: " + authorize.url)
- }
-
- if (authorize.method === "auto") {
- if (authorize.instructions) {
- prompts.log.info(authorize.instructions)
- }
- const spinner = prompts.spinner()
- spinner.start("Waiting for authorization...")
- const result = await authorize.callback()
- if (result.type === "failed") {
- spinner.stop("Failed to authorize", 1)
- }
- if (result.type === "success") {
- const saveProvider = result.provider ?? provider
- if ("refresh" in result) {
- const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
- await Auth.set(saveProvider, {
- type: "oauth",
- refresh,
- access,
- expires,
- ...extraFields,
- })
- }
- if ("key" in result) {
- await Auth.set(saveProvider, {
- type: "api",
- key: result.key,
- })
- }
- spinner.stop("Login successful")
- }
- }
-
- if (authorize.method === "code") {
- const code = await prompts.text({
- message: "Paste the authorization code here: ",
- validate: (x) => (x && x.length > 0 ? undefined : "Required"),
- })
- if (prompts.isCancel(code)) throw new UI.CancelledError()
- const result = await authorize.callback(code)
- if (result.type === "failed") {
- prompts.log.error("Failed to authorize")
- }
- if (result.type === "success") {
- const saveProvider = result.provider ?? provider
- if ("refresh" in result) {
- const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
- await Auth.set(saveProvider, {
- type: "oauth",
- refresh,
- access,
- expires,
- ...extraFields,
- })
- }
- if ("key" in result) {
- await Auth.set(saveProvider, {
- type: "api",
- key: result.key,
- })
- }
- prompts.log.success("Login successful")
- }
- }
-
- prompts.outro("Done")
- return
- }
-
- if (method.type === "api") {
- if (method.authorize) {
- const result = await method.authorize(inputs)
- if (result.type === "failed") {
- prompts.log.error("Failed to authorize")
- }
- if (result.type === "success") {
- const saveProvider = result.provider ?? provider
- await Auth.set(saveProvider, {
- type: "api",
- key: result.key,
- })
- prompts.log.success("Login successful")
- }
- prompts.outro("Done")
- return
- }
- }
+ const handled = await handlePluginAuth({ auth: plugin.auth }, provider)
+ if (handled) return
}
if (provider === "other") {
@@ -287,6 +320,14 @@ export const AuthLoginCommand = cmd({
if (prompts.isCancel(provider)) throw new UI.CancelledError()
provider = provider.replace(/^@ai-sdk\//, "")
if (prompts.isCancel(provider)) throw new UI.CancelledError()
+
+ // Check if a plugin provides auth for this custom provider
+ const customPlugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider))
+ if (customPlugin && customPlugin.auth) {
+ const handled = await handlePluginAuth({ auth: customPlugin.auth }, provider)
+ if (handled) return
+ }
+
prompts.log.warn(
`This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
)
diff --git a/packages/opencode/src/cli/cmd/cmd.ts b/packages/opencode/src/cli/cmd/cmd.ts
index ee6da7b45..fe6d62d7b 100644
--- a/packages/opencode/src/cli/cmd/cmd.ts
+++ b/packages/opencode/src/cli/cmd/cmd.ts
@@ -1,5 +1,7 @@
import type { CommandModule } from "yargs"
-export function cmd(input: CommandModule) {
+type WithDoubleDash = T & { "--"?: string[] }
+
+export function cmd(input: CommandModule>) {
return input
}
diff --git a/packages/opencode/src/cli/cmd/debug/lsp.ts b/packages/opencode/src/cli/cmd/debug/lsp.ts
index 2f5977195..97cb1a0f3 100644
--- a/packages/opencode/src/cli/cmd/debug/lsp.ts
+++ b/packages/opencode/src/cli/cmd/debug/lsp.ts
@@ -17,6 +17,7 @@ const DiagnosticsCommand = cmd({
async handler(args) {
await bootstrap(process.cwd(), async () => {
await LSP.touchFile(args.file, true)
+ await Bun.sleep(1000)
process.stdout.write(JSON.stringify(await LSP.diagnostics(), null, 2) + EOL)
})
},
diff --git a/packages/opencode/src/cli/cmd/generate.ts b/packages/opencode/src/cli/cmd/generate.ts
index c29a22a82..fad4514c8 100644
--- a/packages/opencode/src/cli/cmd/generate.ts
+++ b/packages/opencode/src/cli/cmd/generate.ts
@@ -5,6 +5,26 @@ export const GenerateCommand = {
command: "generate",
handler: async () => {
const specs = await Server.openapi()
+ for (const item of Object.values(specs.paths)) {
+ for (const method of ["get", "post", "put", "delete", "patch"] as const) {
+ const operation = item[method]
+ if (!operation?.operationId) continue
+ // @ts-expect-error
+ operation["x-codeSamples"] = [
+ {
+ lang: "js",
+ source: [
+ `import { createOpencodeClient } from "@opencode-ai/sdk`,
+ ``,
+ `const client = createOpencodeClient()`,
+ `await client.${operation.operationId}({`,
+ ` ...`,
+ `})`,
+ ].join("\n"),
+ },
+ ]
+ }
+ }
const json = JSON.stringify(specs, null, 2)
// Wait for stdout to finish writing before process.exit() is called
diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts
index b255e17d1..480a38230 100644
--- a/packages/opencode/src/cli/cmd/github.ts
+++ b/packages/opencode/src/cli/cmd/github.ts
@@ -124,6 +124,8 @@ type IssueQueryResponse = {
}
}
+const AGENT_USERNAME = "opencode-agent[bot]"
+const AGENT_REACTION = "eyes"
const WORKFLOW_FILE = ".github/workflows/opencode.yml"
export const GithubCommand = cmd({
@@ -276,7 +278,7 @@ export const GithubInstallCommand = cmd({
process.platform === "darwin"
? `open "${url}"`
: process.platform === "win32"
- ? `start "${url}"`
+ ? `start "" "${url}"`
: `xdg-open "${url}"`
exec(command, (error) => {
@@ -403,27 +405,39 @@ export const GithubRunCommand = cmd({
let appToken: string
let octoRest: Octokit
let octoGraph: typeof graphql
- let commentId: number
let gitConfig: string
let session: { id: string; title: string; version: string }
let shareId: string | undefined
let exitCode = 0
type PromptFiles = Awaited>["promptFiles"]
+ const triggerCommentId = payload.comment.id
+ const useGithubToken = normalizeUseGithubToken()
try {
- const actionToken = isMock ? args.token! : await getOidcToken()
- appToken = await exchangeForAppToken(actionToken)
+ if (useGithubToken) {
+ const githubToken = process.env["GITHUB_TOKEN"]
+ if (!githubToken) {
+ throw new Error(
+ "GITHUB_TOKEN environment variable is not set. When using use_github_token, you must provide GITHUB_TOKEN.",
+ )
+ }
+ appToken = githubToken
+ } else {
+ const actionToken = isMock ? args.token! : await getOidcToken()
+ appToken = await exchangeForAppToken(actionToken)
+ }
octoRest = new Octokit({ auth: appToken })
octoGraph = graphql.defaults({
headers: { authorization: `token ${appToken}` },
})
const { userPrompt, promptFiles } = await getUserPrompt()
- await configureGit(appToken)
+ if (!useGithubToken) {
+ await configureGit(appToken)
+ }
await assertPermissions()
- const comment = await createComment()
- commentId = comment.data.id
+ await addReaction()
// Setup opencode session
const repoData = await fetchRepo()
@@ -455,7 +469,8 @@ export const GithubRunCommand = cmd({
await pushToLocalBranch(summary, uncommittedChanges)
}
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
- await updateComment(`${response}${footer({ image: !hasShared })}`)
+ await createComment(`${response}${footer({ image: !hasShared })}`)
+ await removeReaction()
}
// Fork PR
else {
@@ -469,7 +484,8 @@ export const GithubRunCommand = cmd({
await pushToForkBranch(summary, prData, uncommittedChanges)
}
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
- await updateComment(`${response}${footer({ image: !hasShared })}`)
+ await createComment(`${response}${footer({ image: !hasShared })}`)
+ await removeReaction()
}
}
// Issue
@@ -489,9 +505,11 @@ export const GithubRunCommand = cmd({
summary,
`${response}\n\nCloses #${issueId}${footer({ image: true })}`,
)
- await updateComment(`Created PR #${pr}${footer({ image: true })}`)
+ await createComment(`Created PR #${pr}${footer({ image: true })}`)
+ await removeReaction()
} else {
- await updateComment(`${response}${footer({ image: true })}`)
+ await createComment(`${response}${footer({ image: true })}`)
+ await removeReaction()
}
}
} catch (e: any) {
@@ -503,13 +521,16 @@ export const GithubRunCommand = cmd({
} else if (e instanceof Error) {
msg = e.message
}
- await updateComment(`${msg}${footer()}`)
+ await createComment(`${msg}${footer()}`)
+ await removeReaction()
core.setFailed(msg)
// Also output the clean error message for the action to capture
//core.setOutput("prepare_error", e.message);
} finally {
- await restoreGitConfig()
- await revokeAppToken()
+ if (!useGithubToken) {
+ await restoreGitConfig()
+ await revokeAppToken()
+ }
}
process.exit(exitCode)
@@ -538,6 +559,14 @@ export const GithubRunCommand = cmd({
throw new Error(`Invalid share value: ${value}. Share must be a boolean.`)
}
+ function normalizeUseGithubToken() {
+ const value = process.env["USE_GITHUB_TOKEN"]
+ if (!value) return false
+ if (value === "true") return true
+ if (value === "false") return false
+ throw new Error(`Invalid use_github_token value: ${value}. Must be a boolean.`)
+ }
+
function isIssueCommentEvent(
event: IssueCommentEvent | PullRequestReviewCommentEvent,
): event is IssueCommentEvent {
@@ -562,6 +591,11 @@ export const GithubRunCommand = cmd({
}
async function getUserPrompt() {
+ const customPrompt = process.env["PROMPT"]
+ if (customPrompt) {
+ return { userPrompt: customPrompt, promptFiles: [] }
+ }
+
const reviewContext = getReviewCommentContext()
let prompt = (() => {
const body = payload.comment.body.trim()
@@ -803,8 +837,8 @@ export const GithubRunCommand = cmd({
await $`git config --local --unset-all ${config}`
await $`git config --local ${config} "AUTHORIZATION: basic ${newCredentials}"`
- await $`git config --global user.name "opencode-agent[bot]"`
- await $`git config --global user.email "opencode-agent[bot]@users.noreply.github.com"`
+ await $`git config --global user.name "${AGENT_USERNAME}"`
+ await $`git config --global user.email "${AGENT_USERNAME}@users.noreply.github.com"`
}
async function restoreGitConfig() {
@@ -926,24 +960,42 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)
}
- async function createComment() {
+ async function addReaction() {
+ console.log("Adding reaction...")
+ return await octoRest.rest.reactions.createForIssueComment({
+ owner,
+ repo,
+ comment_id: triggerCommentId,
+ content: AGENT_REACTION,
+ })
+ }
+
+ async function removeReaction() {
+ console.log("Removing reaction...")
+ const reactions = await octoRest.rest.reactions.listForIssueComment({
+ owner,
+ repo,
+ comment_id: triggerCommentId,
+ content: AGENT_REACTION,
+ })
+
+ const eyesReaction = reactions.data.find((r) => r.user?.login === AGENT_USERNAME)
+ if (!eyesReaction) return
+
+ await octoRest.rest.reactions.deleteForIssueComment({
+ owner,
+ repo,
+ comment_id: triggerCommentId,
+ reaction_id: eyesReaction.id,
+ })
+ }
+
+ async function createComment(body: string) {
console.log("Creating comment...")
return await octoRest.rest.issues.createComment({
owner,
repo,
issue_number: issueId,
- body: `[Working...](${runUrl})`,
- })
- }
-
- async function updateComment(body: string) {
- if (!commentId) return
-
- console.log("Updating comment...")
- return await octoRest.rest.issues.updateComment({
- owner,
- repo,
- comment_id: commentId,
body,
})
}
@@ -1024,7 +1076,7 @@ query($owner: String!, $repo: String!, $number: Int!) {
const comments = (issue.comments?.nodes || [])
.filter((c) => {
const id = parseInt(c.databaseId)
- return id !== commentId && id !== payload.comment.id
+ return id !== payload.comment.id
})
.map((c) => ` - ${c.author.login} at ${c.createdAt}: ${c.body}`)
@@ -1143,7 +1195,7 @@ query($owner: String!, $repo: String!, $number: Int!) {
const comments = (pr.comments?.nodes || [])
.filter((c) => {
const id = parseInt(c.databaseId)
- return id !== commentId && id !== payload.comment.id
+ return id !== payload.comment.id
})
.map((c) => `- ${c.author.login} at ${c.createdAt}: ${c.body}`)
diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts
index df0046b23..9ca4b3bff 100644
--- a/packages/opencode/src/cli/cmd/mcp.ts
+++ b/packages/opencode/src/cli/cmd/mcp.ts
@@ -3,13 +3,272 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
+import { MCP } from "../../mcp"
+import { McpAuth } from "../../mcp/auth"
+import { Config } from "../../config/config"
+import { Instance } from "../../project/instance"
+import path from "path"
+import os from "os"
+import { Global } from "../../global"
export const McpCommand = cmd({
command: "mcp",
- builder: (yargs) => yargs.command(McpAddCommand).demandCommand(),
+ builder: (yargs) =>
+ yargs
+ .command(McpAddCommand)
+ .command(McpListCommand)
+ .command(McpAuthCommand)
+ .command(McpLogoutCommand)
+ .demandCommand(),
async handler() {},
})
+export const McpListCommand = cmd({
+ command: "list",
+ aliases: ["ls"],
+ describe: "list MCP servers and their status",
+ async handler() {
+ await Instance.provide({
+ directory: process.cwd(),
+ async fn() {
+ UI.empty()
+ prompts.intro("MCP Servers")
+
+ const config = await Config.get()
+ const mcpServers = config.mcp ?? {}
+ const statuses = await MCP.status()
+
+ if (Object.keys(mcpServers).length === 0) {
+ prompts.log.warn("No MCP servers configured")
+ prompts.outro("Add servers with: opencode mcp add")
+ return
+ }
+
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
+ const status = statuses[name]
+ const hasOAuth = serverConfig.type === "remote" && !!serverConfig.oauth
+ const hasStoredTokens = await MCP.hasStoredTokens(name)
+
+ let statusIcon: string
+ let statusText: string
+ let hint = ""
+
+ if (!status) {
+ statusIcon = "○"
+ statusText = "not initialized"
+ } else if (status.status === "connected") {
+ statusIcon = "✓"
+ statusText = "connected"
+ if (hasOAuth && hasStoredTokens) {
+ hint = " (OAuth)"
+ }
+ } else if (status.status === "disabled") {
+ statusIcon = "○"
+ statusText = "disabled"
+ } else if (status.status === "needs_auth") {
+ statusIcon = "⚠"
+ statusText = "needs authentication"
+ } else if (status.status === "needs_client_registration") {
+ statusIcon = "✗"
+ statusText = "needs client registration"
+ hint = "\n " + status.error
+ } else {
+ statusIcon = "✗"
+ statusText = "failed"
+ hint = "\n " + status.error
+ }
+
+ const typeHint = serverConfig.type === "remote" ? serverConfig.url : serverConfig.command.join(" ")
+ prompts.log.info(
+ `${statusIcon} ${name} ${UI.Style.TEXT_DIM}${statusText}${hint}\n ${UI.Style.TEXT_DIM}${typeHint}`,
+ )
+ }
+
+ prompts.outro(`${Object.keys(mcpServers).length} server(s)`)
+ },
+ })
+ },
+})
+
+export const McpAuthCommand = cmd({
+ command: "auth [name]",
+ describe: "authenticate with an OAuth-enabled MCP server",
+ builder: (yargs) =>
+ yargs.positional("name", {
+ describe: "name of the MCP server",
+ type: "string",
+ }),
+ async handler(args) {
+ await Instance.provide({
+ directory: process.cwd(),
+ async fn() {
+ UI.empty()
+ prompts.intro("MCP OAuth Authentication")
+
+ const config = await Config.get()
+ const mcpServers = config.mcp ?? {}
+
+ // Get OAuth-enabled servers
+ const oauthServers = Object.entries(mcpServers).filter(([_, cfg]) => cfg.type === "remote" && !!cfg.oauth)
+
+ if (oauthServers.length === 0) {
+ prompts.log.warn("No OAuth-enabled MCP servers configured")
+ prompts.log.info("Add OAuth config to a remote MCP server in opencode.json:")
+ prompts.log.info(`
+ "mcp": {
+ "my-server": {
+ "type": "remote",
+ "url": "https://example.com/mcp",
+ "oauth": {
+ "scope": "tools:read"
+ }
+ }
+ }`)
+ prompts.outro("Done")
+ return
+ }
+
+ let serverName = args.name
+ if (!serverName) {
+ const selected = await prompts.select({
+ message: "Select MCP server to authenticate",
+ options: oauthServers.map(([name, cfg]) => ({
+ label: name,
+ value: name,
+ hint: cfg.type === "remote" ? cfg.url : undefined,
+ })),
+ })
+ if (prompts.isCancel(selected)) throw new UI.CancelledError()
+ serverName = selected
+ }
+
+ const serverConfig = mcpServers[serverName]
+ if (!serverConfig) {
+ prompts.log.error(`MCP server not found: ${serverName}`)
+ prompts.outro("Done")
+ return
+ }
+
+ if (serverConfig.type !== "remote" || !serverConfig.oauth) {
+ prompts.log.error(`MCP server ${serverName} does not have OAuth configured`)
+ prompts.outro("Done")
+ return
+ }
+
+ // Check if already authenticated
+ const hasTokens = await MCP.hasStoredTokens(serverName)
+ if (hasTokens) {
+ const confirm = await prompts.confirm({
+ message: `${serverName} already has stored credentials. Re-authenticate?`,
+ })
+ if (prompts.isCancel(confirm) || !confirm) {
+ prompts.outro("Cancelled")
+ return
+ }
+ }
+
+ const spinner = prompts.spinner()
+ spinner.start("Starting OAuth flow...")
+
+ try {
+ const status = await MCP.authenticate(serverName)
+
+ if (status.status === "connected") {
+ spinner.stop("Authentication successful!")
+ } else if (status.status === "needs_client_registration") {
+ spinner.stop("Authentication failed", 1)
+ prompts.log.error(status.error)
+ prompts.log.info("Add clientId to your MCP server config:")
+ prompts.log.info(`
+ "mcp": {
+ "${serverName}": {
+ "type": "remote",
+ "url": "${serverConfig.url}",
+ "oauth": {
+ "clientId": "your-client-id",
+ "clientSecret": "your-client-secret"
+ }
+ }
+ }`)
+ } else if (status.status === "failed") {
+ spinner.stop("Authentication failed", 1)
+ prompts.log.error(status.error)
+ } else {
+ spinner.stop("Unexpected status: " + status.status, 1)
+ }
+ } catch (error) {
+ spinner.stop("Authentication failed", 1)
+ prompts.log.error(error instanceof Error ? error.message : String(error))
+ }
+
+ prompts.outro("Done")
+ },
+ })
+ },
+})
+
+export const McpLogoutCommand = cmd({
+ command: "logout [name]",
+ describe: "remove OAuth credentials for an MCP server",
+ builder: (yargs) =>
+ yargs.positional("name", {
+ describe: "name of the MCP server",
+ type: "string",
+ }),
+ async handler(args) {
+ await Instance.provide({
+ directory: process.cwd(),
+ async fn() {
+ UI.empty()
+ prompts.intro("MCP OAuth Logout")
+
+ const authPath = path.join(Global.Path.data, "mcp-auth.json")
+ const credentials = await McpAuth.all()
+ const serverNames = Object.keys(credentials)
+
+ if (serverNames.length === 0) {
+ prompts.log.warn("No MCP OAuth credentials stored")
+ prompts.outro("Done")
+ return
+ }
+
+ let serverName = args.name
+ if (!serverName) {
+ const selected = await prompts.select({
+ message: "Select MCP server to logout",
+ options: serverNames.map((name) => {
+ const entry = credentials[name]
+ const hasTokens = !!entry.tokens
+ const hasClient = !!entry.clientInfo
+ let hint = ""
+ if (hasTokens && hasClient) hint = "tokens + client"
+ else if (hasTokens) hint = "tokens"
+ else if (hasClient) hint = "client registration"
+ return {
+ label: name,
+ value: name,
+ hint,
+ }
+ }),
+ })
+ if (prompts.isCancel(selected)) throw new UI.CancelledError()
+ serverName = selected
+ }
+
+ if (!credentials[serverName]) {
+ prompts.log.error(`No credentials found for: ${serverName}`)
+ prompts.outro("Done")
+ return
+ }
+
+ await MCP.removeAuth(serverName)
+ prompts.log.success(`Removed OAuth credentials for ${serverName}`)
+ prompts.outro("Done")
+ },
+ })
+ },
+})
+
export const McpAddCommand = cmd({
command: "add",
describe: "add an MCP server",
@@ -66,13 +325,74 @@ export const McpAddCommand = cmd({
})
if (prompts.isCancel(url)) throw new UI.CancelledError()
- const client = new Client({
- name: "opencode",
- version: "1.0.0",
+ const useOAuth = await prompts.confirm({
+ message: "Does this server require OAuth authentication?",
+ initialValue: false,
})
- const transport = new StreamableHTTPClientTransport(new URL(url))
- await client.connect(transport)
- prompts.log.info(`Remote MCP server "${name}" configured with URL: ${url}`)
+ if (prompts.isCancel(useOAuth)) throw new UI.CancelledError()
+
+ if (useOAuth) {
+ const hasClientId = await prompts.confirm({
+ message: "Do you have a pre-registered client ID?",
+ initialValue: false,
+ })
+ if (prompts.isCancel(hasClientId)) throw new UI.CancelledError()
+
+ if (hasClientId) {
+ const clientId = await prompts.text({
+ message: "Enter client ID",
+ validate: (x) => (x && x.length > 0 ? undefined : "Required"),
+ })
+ if (prompts.isCancel(clientId)) throw new UI.CancelledError()
+
+ const hasSecret = await prompts.confirm({
+ message: "Do you have a client secret?",
+ initialValue: false,
+ })
+ if (prompts.isCancel(hasSecret)) throw new UI.CancelledError()
+
+ let clientSecret: string | undefined
+ if (hasSecret) {
+ const secret = await prompts.password({
+ message: "Enter client secret",
+ })
+ if (prompts.isCancel(secret)) throw new UI.CancelledError()
+ clientSecret = secret
+ }
+
+ prompts.log.info(`Remote MCP server "${name}" configured with OAuth (client ID: ${clientId})`)
+ prompts.log.info("Add this to your opencode.json:")
+ prompts.log.info(`
+ "mcp": {
+ "${name}": {
+ "type": "remote",
+ "url": "${url}",
+ "oauth": {
+ "clientId": "${clientId}"${clientSecret ? `,\n "clientSecret": "${clientSecret}"` : ""}
+ }
+ }
+ }`)
+ } else {
+ prompts.log.info(`Remote MCP server "${name}" configured with OAuth (dynamic registration)`)
+ prompts.log.info("Add this to your opencode.json:")
+ prompts.log.info(`
+ "mcp": {
+ "${name}": {
+ "type": "remote",
+ "url": "${url}",
+ "oauth": {}
+ }
+ }`)
+ }
+ } else {
+ const client = new Client({
+ name: "opencode",
+ version: "1.0.0",
+ })
+ const transport = new StreamableHTTPClientTransport(new URL(url))
+ await client.connect(transport)
+ prompts.log.info(`Remote MCP server "${name}" configured with URL: ${url}`)
+ }
}
prompts.outro("MCP server added successfully")
diff --git a/packages/opencode/src/cli/cmd/models.ts b/packages/opencode/src/cli/cmd/models.ts
index 1ae4ae12c..156dae91c 100644
--- a/packages/opencode/src/cli/cmd/models.ts
+++ b/packages/opencode/src/cli/cmd/models.ts
@@ -38,7 +38,7 @@ export const ModelsCommand = cmd({
function printModels(providerID: string, verbose?: boolean) {
const provider = providers[providerID]
- const sortedModels = Object.entries(provider.info.models).sort(([a], [b]) => a.localeCompare(b))
+ const sortedModels = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
for (const [modelID, model] of sortedModels) {
process.stdout.write(`${providerID}/${modelID}`)
process.stdout.write(EOL)
diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts
index b646f0b15..23456c75e 100644
--- a/packages/opencode/src/cli/cmd/run.ts
+++ b/packages/opencode/src/cli/cmd/run.ts
@@ -7,7 +7,7 @@ import { bootstrap } from "../bootstrap"
import { Command } from "../../command"
import { EOL } from "os"
import { select } from "@clack/prompts"
-import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk"
+import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2"
import { Server } from "../../server/server"
import { Provider } from "../../provider/provider"
@@ -88,7 +88,7 @@ export const RunCommand = cmd({
})
},
handler: async (args) => {
- let message = args.message.join(" ")
+ let message = [...args.message, ...(args["--"] || [])].join(" ")
const fileParts: any[] = []
if (args.file) {
@@ -212,9 +212,10 @@ export const RunCommand = cmd({
initialValue: "once",
}).catch(() => "reject")
const response = (result.toString().includes("cancel") ? "reject" : result) as "once" | "always" | "reject"
- await sdk.postSessionIdPermissionsPermissionId({
- path: { id: sessionID, permissionID: permission.id },
- body: { response },
+ await sdk.permission.respond({
+ sessionID,
+ permissionID: permission.id,
+ response,
})
}
}
@@ -222,23 +223,19 @@ export const RunCommand = cmd({
if (args.command) {
await sdk.session.command({
- path: { id: sessionID },
- body: {
- agent: args.agent || "build",
- model: args.model,
- command: args.command,
- arguments: message,
- },
+ sessionID,
+ agent: args.agent || "build",
+ model: args.model,
+ command: args.command,
+ arguments: message,
})
} else {
const modelParam = args.model ? Provider.parseModel(args.model) : undefined
await sdk.session.prompt({
- path: { id: sessionID },
- body: {
- agent: args.agent || "build",
- model: modelParam,
- parts: [...fileParts, { type: "text", text: message }],
- },
+ sessionID,
+ agent: args.agent || "build",
+ model: modelParam,
+ parts: [...fileParts, { type: "text", text: message }],
})
}
@@ -263,7 +260,7 @@ export const RunCommand = cmd({
: args.title
: undefined
- const result = await sdk.session.create({ body: title ? { title } : {} })
+ const result = await sdk.session.create(title ? { title } : {})
return result.data?.id
})()
@@ -274,14 +271,14 @@ export const RunCommand = cmd({
const cfgResult = await sdk.config.get()
if (cfgResult.data && (cfgResult.data.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share)) {
- const shareResult = await sdk.session.share({ path: { id: sessionID } }).catch((error) => {
+ const shareResult = await sdk.session.share({ sessionID }).catch((error) => {
if (error instanceof Error && error.message.includes("disabled")) {
UI.println(UI.Style.TEXT_DANGER_BOLD + "! " + error.message)
}
return { error }
})
- if (!shareResult.error) {
- UI.println(UI.Style.TEXT_INFO_BOLD + "~ https://opencode.ai/s/" + sessionID.slice(-8))
+ if (!shareResult.error && "data" in shareResult && shareResult.data?.share?.url) {
+ UI.println(UI.Style.TEXT_INFO_BOLD + "~ " + shareResult.data.share.url)
}
}
@@ -315,7 +312,7 @@ export const RunCommand = cmd({
: args.title
: undefined
- const result = await sdk.session.create({ body: title ? { title } : {} })
+ const result = await sdk.session.create(title ? { title } : {})
return result.data?.id
})()
@@ -327,14 +324,14 @@ export const RunCommand = cmd({
const cfgResult = await sdk.config.get()
if (cfgResult.data && (cfgResult.data.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share)) {
- const shareResult = await sdk.session.share({ path: { id: sessionID } }).catch((error) => {
+ const shareResult = await sdk.session.share({ sessionID }).catch((error) => {
if (error instanceof Error && error.message.includes("disabled")) {
UI.println(UI.Style.TEXT_DANGER_BOLD + "! " + error.message)
}
return { error }
})
- if (!shareResult.error) {
- UI.println(UI.Style.TEXT_INFO_BOLD + "~ https://opencode.ai/s/" + sessionID.slice(-8))
+ if (!shareResult.error && "data" in shareResult && shareResult.data?.share?.url) {
+ UI.println(UI.Style.TEXT_INFO_BOLD + "~ " + shareResult.data.share.url)
}
}
diff --git a/packages/opencode/src/cli/cmd/session.ts b/packages/opencode/src/cli/cmd/session.ts
new file mode 100644
index 000000000..c8b5b0336
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/session.ts
@@ -0,0 +1,106 @@
+import type { Argv } from "yargs"
+import { cmd } from "./cmd"
+import { Session } from "../../session"
+import { bootstrap } from "../bootstrap"
+import { UI } from "../ui"
+import { Locale } from "../../util/locale"
+import { EOL } from "os"
+
+export const SessionCommand = cmd({
+ command: "session",
+ describe: "manage sessions",
+ builder: (yargs: Argv) => yargs.command(SessionListCommand).demandCommand(),
+ async handler() {},
+})
+
+export const SessionListCommand = cmd({
+ command: "list",
+ describe: "list sessions",
+ builder: (yargs: Argv) => {
+ return yargs
+ .option("max-count", {
+ alias: "n",
+ describe: "limit to N most recent sessions",
+ type: "number",
+ })
+ .option("format", {
+ describe: "output format",
+ type: "string",
+ choices: ["table", "json"],
+ default: "table",
+ })
+ },
+ handler: async (args) => {
+ await bootstrap(process.cwd(), async () => {
+ const sessions = []
+ for await (const session of Session.list()) {
+ if (!session.parentID) {
+ sessions.push(session)
+ }
+ }
+
+ sessions.sort((a, b) => b.time.updated - a.time.updated)
+
+ const limitedSessions = args.maxCount ? sessions.slice(0, args.maxCount) : sessions
+
+ if (limitedSessions.length === 0) {
+ return
+ }
+
+ let output: string
+ if (args.format === "json") {
+ output = formatSessionJSON(limitedSessions)
+ } else {
+ output = formatSessionTable(limitedSessions)
+ }
+
+ const shouldPaginate = process.stdout.isTTY && !args.maxCount && args.format === "table"
+
+ if (shouldPaginate) {
+ const proc = Bun.spawn({
+ cmd: ["less", "-R", "-S"],
+ stdin: "pipe",
+ stdout: "inherit",
+ stderr: "inherit",
+ })
+
+ proc.stdin.write(output)
+ proc.stdin.end()
+ await proc.exited
+ } else {
+ console.log(output)
+ }
+ })
+ },
+})
+
+function formatSessionTable(sessions: Session.Info[]): string {
+ const lines: string[] = []
+
+ const maxIdWidth = Math.max(20, ...sessions.map((s) => s.id.length))
+ const maxTitleWidth = Math.max(25, ...sessions.map((s) => s.title.length))
+
+ const header = `Session ID${" ".repeat(maxIdWidth - 10)} Title${" ".repeat(maxTitleWidth - 5)} Updated`
+ lines.push(header)
+ lines.push("─".repeat(header.length))
+ for (const session of sessions) {
+ const truncatedTitle = Locale.truncate(session.title, maxTitleWidth)
+ const timeStr = Locale.todayTimeOrDateTime(session.time.updated)
+ const line = `${session.id.padEnd(maxIdWidth)} ${truncatedTitle.padEnd(maxTitleWidth)} ${timeStr}`
+ lines.push(line)
+ }
+
+ return lines.join(EOL)
+}
+
+function formatSessionJSON(sessions: Session.Info[]): string {
+ const jsonData = sessions.map((session) => ({
+ id: session.id,
+ title: session.title,
+ updated: session.time.updated,
+ created: session.time.created,
+ projectId: session.projectID,
+ directory: session.directory,
+ }))
+ return JSON.stringify(jsonData, null, 2)
+}
diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index 5ec737256..a1a8a5e80 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -2,15 +2,17 @@ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentu
import { Clipboard } from "@tui/util/clipboard"
import { TextAttributes } from "@opentui/core"
import { RouteProvider, useRoute } from "@tui/context/route"
-import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show } from "solid-js"
+import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js"
import { Installation } from "@/installation"
import { Global } from "@/global"
+import { Flag } from "@/flag/flag"
import { DialogProvider, useDialog } from "@tui/ui/dialog"
import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider"
import { SDKProvider, useSDK } from "@tui/context/sdk"
import { SyncProvider, useSync } from "@tui/context/sync"
import { LocalProvider, useLocal } from "@tui/context/local"
-import { DialogModel } from "@tui/component/dialog-model"
+import { DialogModel, useConnected } from "@tui/component/dialog-model"
+import { DialogMcp } from "@tui/component/dialog-mcp"
import { DialogStatus } from "@tui/component/dialog-status"
import { DialogThemeList } from "@tui/component/dialog-theme-list"
import { DialogHelp } from "./ui/dialog-help"
@@ -30,6 +32,8 @@ import { TuiEvent } from "./event"
import { KVProvider, useKV } from "./context/kv"
import { Provider } from "@/provider/provider"
import { ArgsProvider, useArgs, type Args } from "./context/args"
+import open from "open"
+import { PromptRefProvider, usePromptRef } from "./context/prompt"
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
// can't set raw mode if not a TTY
@@ -103,7 +107,9 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise {
return (
- }>
+ }
+ >
@@ -117,7 +123,9 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise
-
+
+
+
@@ -138,7 +146,7 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise {
console.log(JSON.stringify(route.data))
})
+ // Update terminal window title based on current route and session
+ createEffect(() => {
+ if (route.data.type === "home") {
+ renderer.setTerminalTitle("OpenCode")
+ return
+ }
+
+ if (route.data.type === "session") {
+ const session = sync.session.get(route.data.sessionID)
+ if (!session || SessionApi.isDefaultTitle(session.title)) {
+ renderer.setTerminalTitle("OpenCode")
+ return
+ }
+
+ // Truncate title to 40 chars max
+ const title = session.title.length > 40 ? session.title.slice(0, 37) + "..." : session.title
+ renderer.setTerminalTitle(`OC | ${title}`)
+ }
+ })
+
const args = useArgs()
onMount(() => {
batch(() => {
@@ -189,31 +218,51 @@ function App() {
let continued = false
createEffect(() => {
if (continued || sync.status !== "complete" || !args.continue) return
- const match = sync.data.session.at(0)?.id
+ const match = sync.data.session
+ .toSorted((a, b) => b.time.updated - a.time.updated)
+ .find((x) => x.parentID === undefined)?.id
if (match) {
continued = true
route.navigate({ type: "session", sessionID: match })
}
})
+ createEffect(
+ on(
+ () => sync.status === "complete" && sync.data.provider.length === 0,
+ (isEmpty, wasEmpty) => {
+ // only trigger when we transition into an empty-provider state
+ if (!isEmpty || wasEmpty) return
+ dialog.replace(() => )
+ },
+ ),
+ )
+
+ const connected = useConnected()
command.register(() => [
{
title: "Switch session",
value: "session.list",
keybind: "session_list",
category: "Session",
+ suggested: sync.data.session.length > 0,
onSelect: () => {
dialog.replace(() => )
},
},
{
title: "New session",
+ suggested: route.data.type === "session",
value: "session.new",
keybind: "session_new",
category: "Session",
onSelect: () => {
+ const current = promptRef.current
+ // Don't require focus - if there's any text, preserve it
+ const currentPrompt = current?.current?.input ? current.current : undefined
route.navigate({
type: "home",
+ initialPrompt: currentPrompt,
})
dialog.clear()
},
@@ -222,6 +271,7 @@ function App() {
title: "Switch model",
value: "model.list",
keybind: "model_list",
+ suggested: true,
category: "Agent",
onSelect: () => {
dialog.replace(() => )
@@ -229,6 +279,7 @@ function App() {
},
{
title: "Model cycle",
+ disabled: true,
value: "model.cycle_recent",
keybind: "model_cycle_recent",
category: "Agent",
@@ -238,6 +289,7 @@ function App() {
},
{
title: "Model cycle reverse",
+ disabled: true,
value: "model.cycle_recent_reverse",
keybind: "model_cycle_recent_reverse",
category: "Agent",
@@ -245,6 +297,24 @@ function App() {
local.model.cycle(-1)
},
},
+ {
+ title: "Favorite cycle",
+ value: "model.cycle_favorite",
+ keybind: "model_cycle_favorite",
+ category: "Agent",
+ onSelect: () => {
+ local.model.cycleFavorite(1)
+ },
+ },
+ {
+ title: "Favorite cycle reverse",
+ value: "model.cycle_favorite_reverse",
+ keybind: "model_cycle_favorite_reverse",
+ category: "Agent",
+ onSelect: () => {
+ local.model.cycleFavorite(-1)
+ },
+ },
{
title: "Switch agent",
value: "agent.list",
@@ -254,6 +324,14 @@ function App() {
dialog.replace(() => )
},
},
+ {
+ title: "Toggle MCPs",
+ value: "mcp.list",
+ category: "Agent",
+ onSelect: () => {
+ dialog.replace(() => )
+ },
+ },
{
title: "Agent cycle",
value: "agent.cycle",
@@ -274,6 +352,15 @@ function App() {
local.agent.move(-1)
},
},
+ {
+ title: "Connect provider",
+ value: "provider.connect",
+ suggested: !connected(),
+ onSelect: () => {
+ dialog.replace(() => )
+ },
+ category: "Provider",
+ },
{
title: "View status",
keybind: "status_view",
@@ -292,18 +379,11 @@ function App() {
category: "System",
},
{
- title: "Connect provider",
- value: "provider.connect",
- onSelect: () => {
- dialog.replace(() => )
- },
- category: "System",
- },
- {
- title: `Switch to ${mode() === "dark" ? "light" : "dark"} mode`,
+ title: "Toggle appearance",
value: "theme.switch_mode",
- onSelect: () => {
+ onSelect: (dialog) => {
setMode(mode() === "dark" ? "light" : "dark")
+ dialog.clear()
},
category: "System",
},
@@ -315,6 +395,15 @@ function App() {
},
category: "System",
},
+ {
+ title: "Open docs",
+ value: "docs.open",
+ onSelect: () => {
+ open("https://opencode.ai/docs").catch(() => {})
+ dialog.clear()
+ },
+ category: "System",
+ },
{
title: "Exit the app",
value: "app.exit",
@@ -357,8 +446,9 @@ function App() {
])
createEffect(() => {
- const providerID = local.model.current().providerID
- if (providerID === "openrouter" && !kv.get("openrouter_warning", false)) {
+ const currentModel = local.model.current()
+ if (!currentModel) return
+ if (currentModel.providerID === "openrouter" && !kv.get("openrouter_warning", false)) {
untrack(() => {
DialogAlert.show(
dialog,
@@ -438,6 +528,10 @@ function App() {
height={dimensions().height}
backgroundColor={theme.background}
onMouseUp={async () => {
+ if (Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) {
+ renderer.clearSelection()
+ return
+ }
const text = renderer.getSelection()?.getSelectedText()
if (text && text.length > 0) {
const base64 = Buffer.from(text).toString("base64")
@@ -464,7 +558,12 @@ function App() {
)
}
-function ErrorComponent(props: { error: Error; reset: () => void; onExit: () => Promise }) {
+function ErrorComponent(props: {
+ error: Error
+ reset: () => void
+ onExit: () => Promise
+ mode?: "dark" | "light"
+}) {
const term = useTerminalDimensions()
useKeyboard((evt) => {
if (evt.ctrl && evt.name === "c") {
@@ -475,6 +574,15 @@ function ErrorComponent(props: { error: Error; reset: () => void; onExit: () =>
const issueURL = new URL("https://github.com/sst/opencode/issues/new?template=bug-report.yml")
+ // Choose safe fallback colors per mode since theme context may not be available
+ const isLight = props.mode === "light"
+ const colors = {
+ bg: isLight ? "#ffffff" : "#0a0a0a",
+ text: isLight ? "#1a1a1a" : "#eeeeee",
+ muted: isLight ? "#8a8a8a" : "#808080",
+ primary: isLight ? "#3b7dd8" : "#fab283",
+ }
+
if (props.error.message) {
issueURL.searchParams.set("title", `opentui: fatal: ${props.error.message}`)
}
@@ -495,27 +603,31 @@ function ErrorComponent(props: { error: Error; reset: () => void; onExit: () =>
}
return (
-
+
- Please report an issue.
-
- Copy issue URL (exception info pre-filled)
+
+ Please report an issue.
+
+
+
+ Copy issue URL (exception info pre-filled)
+
- {copied() && Successfully copied}
+ {copied() && Successfully copied}
- A fatal error occurred!
-
- Reset TUI
+ A fatal error occurred!
+
+ Reset TUI
-
- Exit
+
+ Exit
- {props.error.stack}
+ {props.error.stack}
- {props.error.message}
+ {props.error.message}
)
}
diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts
index 7da6507ea..5d1a4ded2 100644
--- a/packages/opencode/src/cli/cmd/tui/attach.ts
+++ b/packages/opencode/src/cli/cmd/tui/attach.ts
@@ -14,12 +14,17 @@ export const AttachCommand = cmd({
.option("dir", {
type: "string",
description: "directory to run in",
+ })
+ .option("session", {
+ alias: ["s"],
+ type: "string",
+ describe: "session id to continue",
}),
handler: async (args) => {
if (args.dir) process.chdir(args.dir)
await tui({
url: args.url,
- args: {},
+ args: { sessionID: args.session },
})
},
})
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx
index 65aaeb22b..365a22445 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx
@@ -12,7 +12,7 @@ export function DialogAgent() {
return {
value: item.name,
title: item.name,
- description: item.builtIn ? "native" : item.description,
+ description: item.native ? "native" : item.description,
}
}),
)
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx
index b9ba4a9ba..d2130488e 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx
@@ -1,5 +1,5 @@
import { useDialog } from "@tui/ui/dialog"
-import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
+import { DialogSelect, type DialogSelectOption, type DialogSelectRef } from "@tui/ui/dialog-select"
import {
createContext,
createMemo,
@@ -11,13 +11,14 @@ import {
} from "solid-js"
import { useKeyboard } from "@opentui/solid"
import { useKeybind } from "@tui/context/keybind"
-import type { KeybindsConfig } from "@opencode-ai/sdk"
+import type { KeybindsConfig } from "@opencode-ai/sdk/v2"
type Context = ReturnType
const ctx = createContext()
export type CommandOption = DialogSelectOption & {
keybind?: keyof KeybindsConfig
+ suggested?: boolean
}
function init() {
@@ -26,7 +27,19 @@ function init() {
const dialog = useDialog()
const keybind = useKeybind()
const options = createMemo(() => {
- return registrations().flatMap((x) => x())
+ const all = registrations().flatMap((x) => x())
+ const suggested = all.filter((x) => x.suggested)
+ return [
+ ...suggested.map((x) => ({
+ ...x,
+ category: "Suggested",
+ value: "suggested." + x.value,
+ })),
+ ...all,
+ ].map((x) => ({
+ ...x,
+ footer: x.keybind ? keybind.print(x.keybind) : undefined,
+ }))
})
const suspended = () => suspendCount() > 0
@@ -99,14 +112,12 @@ export function CommandProvider(props: ParentProps) {
}
function DialogCommand(props: { options: CommandOption[] }) {
- const keybind = useKeybind()
+ let ref: DialogSelectRef
return (
(ref = r)}
title="Commands"
- options={props.options.map((x) => ({
- ...x,
- footer: x.keybind ? keybind.print(x.keybind) : undefined,
- }))}
+ options={props.options.filter((x) => !ref?.filter || !x.value.startsWith("suggested."))}
/>
)
}
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx
new file mode 100644
index 000000000..9cfa30d4d
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx
@@ -0,0 +1,86 @@
+import { createMemo, createSignal } from "solid-js"
+import { useLocal } from "@tui/context/local"
+import { useSync } from "@tui/context/sync"
+import { map, pipe, entries, sortBy } from "remeda"
+import { DialogSelect, type DialogSelectRef, type DialogSelectOption } from "@tui/ui/dialog-select"
+import { useTheme } from "../context/theme"
+import { Keybind } from "@/util/keybind"
+import { TextAttributes } from "@opentui/core"
+import { useSDK } from "@tui/context/sdk"
+
+function Status(props: { enabled: boolean; loading: boolean }) {
+ const { theme } = useTheme()
+ if (props.loading) {
+ return ⋯ Loading
+ }
+ if (props.enabled) {
+ return ✓ Enabled
+ }
+ return ○ Disabled
+}
+
+export function DialogMcp() {
+ const local = useLocal()
+ const sync = useSync()
+ const sdk = useSDK()
+ const [, setRef] = createSignal>()
+ const [loading, setLoading] = createSignal(null)
+
+ const options = createMemo(() => {
+ // Track sync data and loading state to trigger re-render when they change
+ const mcpData = sync.data.mcp
+ const loadingMcp = loading()
+
+ return pipe(
+ mcpData ?? {},
+ entries(),
+ sortBy(([name]) => name),
+ map(([name, status]) => ({
+ value: name,
+ title: name,
+ description: status.status === "failed" ? "failed" : status.status,
+ footer: ,
+ category: undefined,
+ })),
+ )
+ })
+
+ const keybinds = createMemo(() => [
+ {
+ keybind: Keybind.parse("space")[0],
+ title: "toggle",
+ onTrigger: async (option: DialogSelectOption) => {
+ // Prevent toggling while an operation is already in progress
+ if (loading() !== null) return
+
+ setLoading(option.value)
+ try {
+ await local.mcp.toggle(option.value)
+ // Refresh MCP status from server
+ const status = await sdk.client.mcp.status()
+ if (status.data) {
+ sync.set("mcp", status.data)
+ } else {
+ console.error("Failed to refresh MCP status: no data returned")
+ }
+ } catch (error) {
+ console.error("Failed to toggle MCP:", error)
+ } finally {
+ setLoading(null)
+ }
+ },
+ },
+ ])
+
+ return (
+ {
+ // Don't close on select, only on escape
+ }}
+ />
+ )
+}
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
index 4aaac6123..38fd57458 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
@@ -6,28 +6,41 @@ import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
import { useDialog } from "@tui/ui/dialog"
import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
import { Keybind } from "@/util/keybind"
-import { iife } from "@/util/iife"
-export function DialogModel() {
+export function useConnected() {
+ const sync = useSync()
+ return createMemo(() =>
+ sync.data.provider.some((x) => x.id !== "opencode" || Object.values(x.models).some((y) => y.cost?.input !== 0)),
+ )
+}
+
+export function DialogModel(props: { providerID?: string }) {
const local = useLocal()
const sync = useSync()
const dialog = useDialog()
const [ref, setRef] = createSignal>()
- const connected = createMemo(() =>
- sync.data.provider.some((x) => x.id !== "opencode" || Object.values(x.models).some((y) => y.cost?.input !== 0)),
- )
-
+ const connected = useConnected()
const providers = createDialogProviderOptions()
+ const showExtra = createMemo(() => {
+ if (!connected()) return false
+ if (props.providerID) return false
+ return true
+ })
+
const options = createMemo(() => {
const query = ref()?.filter
- const favorites = connected() ? local.model.favorite() : []
+ const favorites = showExtra() ? local.model.favorite() : []
const recents = local.model.recent()
- const recentList = recents
- .filter((item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID))
- .slice(0, 5)
+ const recentList = showExtra()
+ ? recents
+ .filter(
+ (item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
+ )
+ .slice(0, 5)
+ : []
const favoriteOptions = !query
? favorites.flatMap((item) => {
@@ -108,6 +121,8 @@ export function DialogModel() {
pipe(
provider.models,
entries(),
+ filter(([_, info]) => info.status !== "deprecated"),
+ filter(([_, info]) => (props.providerID ? info.providerID === props.providerID : true)),
map(([model, info]) => {
const value = {
providerID: provider.id,
@@ -149,7 +164,10 @@ export function DialogModel() {
if (inRecents) return false
return true
}),
- sortBy((x) => x.title),
+ sortBy(
+ (x) => x.footer !== "Free",
+ (x) => x.title,
+ ),
),
),
),
@@ -168,11 +186,20 @@ export function DialogModel() {
]
})
+ const provider = createMemo(() =>
+ props.providerID ? sync.data.provider.find((x) => x.id === props.providerID) : null,
+ )
+
+ const title = createMemo(() => {
+ if (provider()) return provider()!.name
+ return "Select model"
+ })
+
return (
)
@@ -188,7 +215,7 @@ export function DialogModel() {
},
]}
ref={setRef}
- title="Select model"
+ title={title()}
current={local.model.current()}
options={options()}
/>
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
index 8ba7845f2..5cc114f92 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
@@ -7,7 +7,7 @@ import { useSDK } from "../context/sdk"
import { DialogPrompt } from "../ui/dialog-prompt"
import { useTheme } from "../context/theme"
import { TextAttributes } from "@opentui/core"
-import type { ProviderAuthAuthorization } from "@opencode-ai/sdk"
+import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2"
import { DialogModel } from "./dialog-model"
const PROVIDER_PRIORITY: Record = {
@@ -64,12 +64,8 @@ export function createDialogProviderOptions() {
const method = methods[index]
if (method.type === "oauth") {
const result = await sdk.client.provider.oauth.authorize({
- path: {
- id: provider.id,
- },
- body: {
- method: index,
- },
+ providerID: provider.id,
+ method: index,
})
if (result.data?.method === "code") {
dialog.replace(() => (
@@ -111,12 +107,8 @@ function AutoMethod(props: AutoMethodProps) {
onMount(async () => {
const result = await sdk.client.provider.oauth.callback({
- path: {
- id: props.providerID,
- },
- body: {
- method: props.index,
- },
+ providerID: props.providerID,
+ method: props.index,
})
if (result.error) {
dialog.clear()
@@ -124,13 +116,15 @@ function AutoMethod(props: AutoMethodProps) {
}
await sdk.client.instance.dispose()
await sync.bootstrap()
- dialog.replace(() => )
+ dialog.replace(() => )
})
return (
- {props.title}
+
+ {props.title}
+
esc
@@ -161,18 +155,14 @@ function CodeMethod(props: CodeMethodProps) {
placeholder="Authorization code"
onConfirm={async (value) => {
const { error } = await sdk.client.provider.oauth.callback({
- path: {
- id: props.providerID,
- },
- body: {
- method: props.index,
- code: value,
- },
+ providerID: props.providerID,
+ method: props.index,
+ code: value,
})
if (!error) {
await sdk.client.instance.dispose()
await sync.bootstrap()
- dialog.replace(() => )
+ dialog.replace(() => )
return
}
setError(true)
@@ -210,7 +200,7 @@ function ApiMethod(props: ApiMethodProps) {
OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API key.
-
+
Go to https://opencode.ai/zen to get a key
@@ -219,17 +209,15 @@ function ApiMethod(props: ApiMethodProps) {
onConfirm={async (value) => {
if (!value) return
sdk.client.auth.set({
- path: {
- id: props.providerID,
- },
- body: {
+ providerID: props.providerID,
+ auth: {
type: "api",
key: value,
},
})
await sdk.client.instance.dispose()
await sync.bootstrap()
- dialog.replace(() => )
+ dialog.replace(() => )
}}
/>
)
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
index 5e0095a8d..f5e0efa49 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
@@ -8,6 +8,7 @@ import { Keybind } from "@/util/keybind"
import { useTheme } from "../context/theme"
import { useSDK } from "../context/sdk"
import { DialogSessionRename } from "./dialog-session-rename"
+import "opentui-spinner/solid"
export function DialogSessionList() {
const dialog = useDialog()
@@ -22,10 +23,13 @@ export function DialogSessionList() {
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
+ const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
+
const options = createMemo(() => {
const today = new Date().toDateString()
return sync.data.session
.filter((x) => x.parentID === undefined)
+ .toSorted((a, b) => b.time.updated - a.time.updated)
.map((x) => {
const date = new Date(x.time.updated)
let category = date.toDateString()
@@ -33,12 +37,15 @@ export function DialogSessionList() {
category = "Today"
}
const isDeleting = toDelete() === x.id
+ const status = sync.data.session_status[x.id]
+ const isWorking = status?.type === "busy"
return {
title: isDeleting ? `Press ${deleteKeybind} again to confirm` : x.title,
bg: isDeleting ? theme.error : undefined,
value: x.id,
category,
footer: Locale.time(x.time.updated),
+ gutter: isWorking ? : undefined,
}
})
.slice(0, 150)
@@ -74,9 +81,7 @@ export function DialogSessionList() {
onTrigger: async (option) => {
if (toDelete() === option.value) {
sdk.client.session.delete({
- path: {
- id: option.value,
- },
+ sessionID: option.value,
})
setToDelete(undefined)
// dialog.clear()
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx
index aaf033200..141340d55 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx
@@ -20,12 +20,8 @@ export function DialogSessionRename(props: DialogSessionRenameProps) {
value={session()?.title}
onConfirm={(value) => {
sdk.client.session.update({
- path: {
- id: props.session,
- },
- body: {
- title: value,
- },
+ sessionID: props.session,
+ title: value,
})
dialog.clear()
}}
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
index e427e24e9..4e485b033 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
@@ -19,7 +19,7 @@ export function DialogStatus() {
esc
- 0} fallback={No MCP Servers}>
+ 0} fallback={No MCP Servers}>
{Object.keys(sync.data.mcp).length} MCP Servers
@@ -28,11 +28,15 @@ export function DialogStatus() {
+ )[item.status],
}}
>
•
@@ -40,10 +44,16 @@ export function DialogStatus() {
{key}{" "}
-
+
Connected
{(val) => val().error}
Disabled in configuration
+
+ Needs authentication (run: opencode mcp auth {key})
+
+
+ {(val) => (val() as { error: string }).error}
+
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-tag.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-tag.tsx
index 78eeded24..6d6c62450 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-tag.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-tag.tsx
@@ -16,9 +16,7 @@ export function DialogTag(props: { onSelect?: (value: string) => void }) {
() => [store.filter],
async () => {
const result = await sdk.client.find.files({
- query: {
- query: store.filter,
- },
+ query: store.filter,
})
if (result.error) return []
const sliced = (result.data ?? []).slice(0, 5)
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx
index c6d22be7b..f4072c978 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx
@@ -5,10 +5,12 @@ import { onCleanup, onMount } from "solid-js"
export function DialogThemeList() {
const theme = useTheme()
- const options = Object.keys(theme.all()).map((value) => ({
- title: value,
- value: value,
- }))
+ const options = Object.keys(theme.all())
+ .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }))
+ .map((value) => ({
+ title: value,
+ value: value,
+ }))
const dialog = useDialog()
let confirmed = false
let ref: DialogSelectRef
diff --git a/packages/opencode/src/cli/cmd/tui/component/logo.tsx b/packages/opencode/src/cli/cmd/tui/component/logo.tsx
index 59db5fe7d..d1be06a7f 100644
--- a/packages/opencode/src/cli/cmd/tui/component/logo.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/logo.tsx
@@ -1,4 +1,3 @@
-import { Installation } from "@/installation"
import { TextAttributes } from "@opentui/core"
import { For } from "solid-js"
import { useTheme } from "@tui/context/theme"
@@ -14,16 +13,15 @@ export function Logo() {
{(line, index) => (
- {line}
-
+
+ {line}
+
+
{LOGO_RIGHT[index()]}
)}
-
- {Installation.VERSION}
-
)
}
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
index f74a176ec..6fde66944 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
@@ -1,13 +1,14 @@
import type { BoxRenderable, TextareaRenderable, KeyEvent } from "@opentui/core"
import fuzzysort from "fuzzysort"
import { firstBy } from "remeda"
-import { createMemo, createResource, createEffect, onMount, For, Show } from "solid-js"
+import { createMemo, createResource, createEffect, onMount, onCleanup, For, Show, createSignal } from "solid-js"
import { createStore } from "solid-js/store"
import { useSDK } from "@tui/context/sdk"
import { useSync } from "@tui/context/sync"
import { useTheme, selectedForeground } from "@tui/context/theme"
import { SplitBorder } from "@tui/component/border"
import { useCommandDialog } from "@tui/component/dialog-command"
+import { useTerminalDimensions } from "@opentui/solid"
import { Locale } from "@/util/locale"
import type { PromptInfo } from "./history"
@@ -41,13 +42,43 @@ export function Autocomplete(props: {
const sync = useSync()
const command = useCommandDialog()
const { theme } = useTheme()
+ const dimensions = useTerminalDimensions()
const [store, setStore] = createStore({
index: 0,
selected: 0,
visible: false as AutocompleteRef["visible"],
- position: { x: 0, y: 0, width: 0 },
})
+
+ const [positionTick, setPositionTick] = createSignal(0)
+
+ createEffect(() => {
+ if (store.visible) {
+ let lastPos = { x: 0, y: 0, width: 0 }
+ const interval = setInterval(() => {
+ const anchor = props.anchor()
+ if (anchor.x !== lastPos.x || anchor.y !== lastPos.y || anchor.width !== lastPos.width) {
+ lastPos = { x: anchor.x, y: anchor.y, width: anchor.width }
+ setPositionTick((t) => t + 1)
+ }
+ }, 50)
+
+ onCleanup(() => clearInterval(interval))
+ }
+ })
+
+ const position = createMemo(() => {
+ if (!store.visible) return { x: 0, y: 0, width: 0 }
+ const dims = dimensions()
+ positionTick()
+ const anchor = props.anchor()
+ return {
+ x: anchor.x,
+ y: anchor.y,
+ width: anchor.width,
+ }
+ })
+
const filter = createMemo(() => {
if (!store.visible) return
// Track props.value to make memo reactive to text changes
@@ -109,16 +140,14 @@ export function Autocomplete(props: {
// Get files from SDK
const result = await sdk.client.find.files({
- query: {
- query: query ?? "",
- },
+ query: query ?? "",
})
const options: AutocompleteOption[] = []
// Add file options
if (!result.error && result.data) {
- const width = store.position.width - 4
+ const width = props.anchor().width - 4
options.push(
...result.data.map(
(item): AutocompleteOption => ({
@@ -155,7 +184,7 @@ export function Autocomplete(props: {
const agents = createMemo(() => {
const agents = sync.data.agent
return agents
- .filter((agent) => !agent.builtIn && agent.mode !== "primary")
+ .filter((agent) => !agent.hidden && agent.mode !== "primary")
.map(
(agent): AutocompleteOption => ({
display: "@" + agent.name,
@@ -278,10 +307,14 @@ export function Autocomplete(props: {
},
{
display: "/status",
- aliases: ["/mcp"],
description: "show status",
onSelect: () => command.trigger("opencode.status"),
},
+ {
+ display: "/mcp",
+ description: "toggle MCPs",
+ onSelect: () => command.trigger("mcp.list"),
+ },
{
display: "/theme",
description: "toggle theme",
@@ -331,6 +364,13 @@ export function Autocomplete(props: {
const result = fuzzysort.go(currentFilter, mixed, {
keys: [(obj) => obj.display.trimEnd(), "description", (obj) => obj.aliases?.join(" ") ?? ""],
limit: 10,
+ scoreFn: (objResults) => {
+ const displayResult = objResults[0]
+ if (displayResult && displayResult.target.startsWith(store.visible + currentFilter)) {
+ return objResults.score * 2
+ }
+ return objResults.score
+ },
})
return result.map((arr) => arr.obj)
})
@@ -352,8 +392,8 @@ export function Autocomplete(props: {
function select() {
const selected = options()[store.selected]
if (!selected) return
- selected.onSelect?.()
hide()
+ selected.onSelect?.()
}
function show(mode: "@" | "/") {
@@ -361,11 +401,6 @@ export function Autocomplete(props: {
setStore({
visible: mode,
index: props.input().cursorOffset,
- position: {
- x: props.anchor().x,
- y: props.anchor().y,
- width: props.anchor().width,
- },
})
}
@@ -374,6 +409,10 @@ export function Autocomplete(props: {
if (store.visible === "/" && !text.endsWith(" ") && text.startsWith("/")) {
const cursor = props.input().logicalCursor
props.input().deleteRange(0, 0, cursor.row, cursor.col)
+ // Sync the prompt store immediately since onContentChange is async
+ props.setPrompt((draft) => {
+ draft.input = props.input().plainText
+ })
}
command.keybinds(true)
setStore("visible", false)
@@ -453,9 +492,9 @@ export function Autocomplete(props: {
- No matching items
+ No matching items
}
>
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx
index 4fd60dd36..e90503e9f 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx
@@ -5,10 +5,11 @@ import { createStore, produce } from "solid-js/store"
import { clone } from "remeda"
import { createSimpleContext } from "../../context/helper"
import { appendFile, writeFile } from "fs/promises"
-import type { AgentPart, FilePart, TextPart } from "@opencode-ai/sdk"
+import type { AgentPart, FilePart, TextPart } from "@opencode-ai/sdk/v2"
export type PromptInfo = {
input: string
+ mode?: "normal" | "shell"
parts: (
| Omit
| Omit
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
index ae11fcc23..938405f68 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -10,6 +10,7 @@ import { useSync } from "@tui/context/sync"
import { Identifier } from "@/id/id"
import { createStore, produce } from "solid-js/store"
import { useKeybind } from "@tui/context/keybind"
+import { Keybind } from "@/util/keybind"
import { usePromptHistory, type PromptInfo } from "./history"
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
import { useCommandDialog } from "../dialog-command"
@@ -17,11 +18,15 @@ import { useRenderer } from "@opentui/solid"
import { Editor } from "@tui/util/editor"
import { useExit } from "../../context/exit"
import { Clipboard } from "../../util/clipboard"
-import type { FilePart } from "@opencode-ai/sdk"
+import type { FilePart } from "@opencode-ai/sdk/v2"
import { TuiEvent } from "../../event"
import { iife } from "@/util/iife"
import { Locale } from "@/util/locale"
import { createColors, createFrames } from "../../ui/spinner.ts"
+import { useDialog } from "@tui/ui/dialog"
+import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
+import { DialogAlert } from "../../ui/dialog-alert"
+import { useToast } from "../../ui/toast"
export type PromptProps = {
sessionID?: string
@@ -34,10 +39,69 @@ export type PromptProps = {
export type PromptRef = {
focused: boolean
+ current: PromptInfo
set(prompt: PromptInfo): void
reset(): void
blur(): void
focus(): void
+ submit(): void
+}
+
+const PLACEHOLDERS = ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"]
+
+const TEXTAREA_ACTIONS = [
+ "submit",
+ "newline",
+ "move-left",
+ "move-right",
+ "move-up",
+ "move-down",
+ "select-left",
+ "select-right",
+ "select-up",
+ "select-down",
+ "line-home",
+ "line-end",
+ "select-line-home",
+ "select-line-end",
+ "visual-line-home",
+ "visual-line-end",
+ "select-visual-line-home",
+ "select-visual-line-end",
+ "buffer-home",
+ "buffer-end",
+ "select-buffer-home",
+ "select-buffer-end",
+ "delete-line",
+ "delete-to-line-end",
+ "delete-to-line-start",
+ "backspace",
+ "delete",
+ "undo",
+ "redo",
+ "word-forward",
+ "word-backward",
+ "select-word-forward",
+ "select-word-backward",
+ "delete-word-forward",
+ "delete-word-backward",
+] as const
+
+function mapTextareaKeybindings(
+ keybinds: Record,
+ action: (typeof TEXTAREA_ACTIONS)[number],
+): KeyBinding[] {
+ const configKey = `input_${action.replace(/-/g, "_")}`
+ const bindings = keybinds[configKey]
+ if (!bindings) return []
+ return bindings.map((binding) => ({
+ name: binding.name,
+ ctrl: binding.ctrl || undefined,
+ meta: binding.meta || undefined,
+ shift: binding.shift || undefined,
+ super: binding.super || undefined,
+ action,
+ }))
}
export function Prompt(props: PromptProps) {
@@ -50,33 +114,32 @@ export function Prompt(props: PromptProps) {
const sdk = useSDK()
const route = useRoute()
const sync = useSync()
+ const dialog = useDialog()
+ const toast = useToast()
const status = createMemo(() => sync.data.session_status[props.sessionID ?? ""] ?? { type: "idle" })
const history = usePromptHistory()
const command = useCommandDialog()
const renderer = useRenderer()
const { theme, syntax } = useTheme()
+ function promptModelWarning() {
+ toast.show({
+ variant: "warning",
+ message: "Connect a provider to send prompts",
+ duration: 3000,
+ })
+ if (sync.data.provider.length === 0) {
+ dialog.replace(() => )
+ }
+ }
+
const textareaKeybindings = createMemo(() => {
- const newlineBindings = keybind.all.input_newline || []
- const submitBindings = keybind.all.input_submit || []
+ const keybinds = keybind.all
return [
{ name: "return", action: "submit" },
{ name: "return", meta: true, action: "newline" },
- ...newlineBindings.map((binding) => ({
- name: binding.name,
- ctrl: binding.ctrl || undefined,
- meta: binding.meta || undefined,
- shift: binding.shift || undefined,
- action: "newline" as const,
- })),
- ...submitBindings.map((binding) => ({
- name: binding.name,
- ctrl: binding.ctrl || undefined,
- meta: binding.meta || undefined,
- shift: binding.shift || undefined,
- action: "submit" as const,
- })),
+ ...TEXTAREA_ACTIONS.flatMap((action) => mapTextareaKeybindings(keybinds, action)),
] satisfies KeyBinding[]
})
@@ -87,90 +150,6 @@ export function Prompt(props: PromptProps) {
command.register(() => {
return [
- {
- title: "Open editor",
- category: "Session",
- keybind: "editor_open",
- value: "prompt.editor",
- onSelect: async (dialog, trigger) => {
- dialog.clear()
-
- // replace summarized text parts with the actual text
- const text = store.prompt.parts
- .filter((p) => p.type === "text")
- .reduce((acc, p) => {
- if (!p.source) return acc
- return acc.replace(p.source.text.value, p.text)
- }, store.prompt.input)
-
- const nonTextParts = store.prompt.parts.filter((p) => p.type !== "text")
-
- const value = trigger === "prompt" ? "" : text
- const content = await Editor.open({ value, renderer })
- if (!content) return
-
- input.setText(content, { history: false })
-
- // Update positions for nonTextParts based on their location in new content
- // Filter out parts whose virtual text was deleted
- // this handles a case where the user edits the text in the editor
- // such that the virtual text moves around or is deleted
- const updatedNonTextParts = nonTextParts
- .map((part) => {
- let virtualText = ""
- if (part.type === "file" && part.source?.text) {
- virtualText = part.source.text.value
- } else if (part.type === "agent" && part.source) {
- virtualText = part.source.value
- }
-
- if (!virtualText) return part
-
- const newStart = content.indexOf(virtualText)
- // if the virtual text is deleted, remove the part
- if (newStart === -1) return null
-
- const newEnd = newStart + virtualText.length
-
- if (part.type === "file" && part.source?.text) {
- return {
- ...part,
- source: {
- ...part.source,
- text: {
- ...part.source.text,
- start: newStart,
- end: newEnd,
- },
- },
- }
- }
-
- if (part.type === "agent" && part.source) {
- return {
- ...part,
- source: {
- ...part.source,
- start: newStart,
- end: newEnd,
- },
- }
- }
-
- return part
- })
- .filter((part) => part !== null)
-
- setStore("prompt", {
- input: content,
- // keep only the non-text parts because the text parts were
- // already expanded inline
- parts: updatedNonTextParts,
- })
- restoreExtmarksFromParts(updatedNonTextParts)
- input.cursorOffset = Bun.stringWidth(content)
- },
- },
{
title: "Clear prompt",
value: "prompt.clear",
@@ -235,15 +214,97 @@ export function Prompt(props: PromptProps) {
if (store.interrupt >= 2) {
sdk.client.session.abort({
- path: {
- id: props.sessionID,
- },
+ sessionID: props.sessionID,
})
setStore("interrupt", 0)
}
dialog.clear()
},
},
+ {
+ title: "Open editor",
+ category: "Session",
+ keybind: "editor_open",
+ value: "prompt.editor",
+ onSelect: async (dialog, trigger) => {
+ dialog.clear()
+
+ // replace summarized text parts with the actual text
+ const text = store.prompt.parts
+ .filter((p) => p.type === "text")
+ .reduce((acc, p) => {
+ if (!p.source) return acc
+ return acc.replace(p.source.text.value, p.text)
+ }, store.prompt.input)
+
+ const nonTextParts = store.prompt.parts.filter((p) => p.type !== "text")
+
+ const value = trigger === "prompt" ? "" : text
+ const content = await Editor.open({ value, renderer })
+ if (!content) return
+
+ input.setText(content)
+
+ // Update positions for nonTextParts based on their location in new content
+ // Filter out parts whose virtual text was deleted
+ // this handles a case where the user edits the text in the editor
+ // such that the virtual text moves around or is deleted
+ const updatedNonTextParts = nonTextParts
+ .map((part) => {
+ let virtualText = ""
+ if (part.type === "file" && part.source?.text) {
+ virtualText = part.source.text.value
+ } else if (part.type === "agent" && part.source) {
+ virtualText = part.source.value
+ }
+
+ if (!virtualText) return part
+
+ const newStart = content.indexOf(virtualText)
+ // if the virtual text is deleted, remove the part
+ if (newStart === -1) return null
+
+ const newEnd = newStart + virtualText.length
+
+ if (part.type === "file" && part.source?.text) {
+ return {
+ ...part,
+ source: {
+ ...part.source,
+ text: {
+ ...part.source.text,
+ start: newStart,
+ end: newEnd,
+ },
+ },
+ }
+ }
+
+ if (part.type === "agent" && part.source) {
+ return {
+ ...part,
+ source: {
+ ...part.source,
+ start: newStart,
+ end: newEnd,
+ },
+ }
+ }
+
+ return part
+ })
+ .filter((part) => part !== null)
+
+ setStore("prompt", {
+ input: content,
+ // keep only the non-text parts because the text parts were
+ // already expanded inline
+ parts: updatedNonTextParts,
+ })
+ restoreExtmarksFromParts(updatedNonTextParts)
+ input.cursorOffset = Bun.stringWidth(content)
+ },
+ },
]
})
@@ -253,7 +314,7 @@ export function Prompt(props: PromptProps) {
createEffect(() => {
if (props.disabled) input.cursorColor = theme.backgroundElement
- if (!props.disabled) input.cursorColor = theme.primary
+ if (!props.disabled) input.cursorColor = theme.text
})
const [store, setStore] = createStore<{
@@ -261,7 +322,9 @@ export function Prompt(props: PromptProps) {
mode: "normal" | "shell"
extmarkToPartIndex: Map
interrupt: number
+ placeholder: number
}>({
+ placeholder: Math.floor(Math.random() * PLACEHOLDERS.length),
prompt: {
input: "",
parts: [],
@@ -361,6 +424,9 @@ export function Prompt(props: PromptProps) {
get focused() {
return input.focused
},
+ get current() {
+ return store.prompt
+ },
focus() {
input.focus()
},
@@ -368,7 +434,7 @@ export function Prompt(props: PromptProps) {
input.blur()
},
set(prompt) {
- input.setText(prompt.input, { history: false })
+ input.setText(prompt.input)
setStore("prompt", prompt)
restoreExtmarksFromParts(prompt.parts)
input.gotoBufferEnd()
@@ -382,12 +448,25 @@ export function Prompt(props: PromptProps) {
})
setStore("extmarkToPartIndex", new Map())
},
+ submit() {
+ submit()
+ },
})
async function submit() {
if (props.disabled) return
- if (autocomplete.visible) return
+ if (autocomplete?.visible) return
if (!store.prompt.input) return
+ const trimmed = store.prompt.input.trim()
+ if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
+ exit()
+ return
+ }
+ const selectedModel = local.model.current()
+ if (!selectedModel) {
+ promptModelWarning()
+ return
+ }
const sessionID = props.sessionID
? props.sessionID
: await (async () => {
@@ -416,19 +495,18 @@ export function Prompt(props: PromptProps) {
// Filter out text parts (pasted content) since they're now expanded inline
const nonTextParts = store.prompt.parts.filter((part) => part.type !== "text")
+ // Capture mode before it gets reset
+ const currentMode = store.mode
+
if (store.mode === "shell") {
sdk.client.session.shell({
- path: {
- id: sessionID,
- },
- body: {
- agent: local.agent.current().name,
- model: {
- providerID: local.model.current().providerID,
- modelID: local.model.current().modelID,
- },
- command: inputText,
+ sessionID,
+ agent: local.agent.current().name,
+ model: {
+ providerID: selectedModel.providerID,
+ modelID: selectedModel.modelID,
},
+ command: inputText,
})
setStore("mode", "normal")
} else if (
@@ -441,42 +519,37 @@ export function Prompt(props: PromptProps) {
) {
let [command, ...args] = inputText.split(" ")
sdk.client.session.command({
- path: {
- id: sessionID,
- },
- body: {
- command: command.slice(1),
- arguments: args.join(" "),
- agent: local.agent.current().name,
- model: `${local.model.current().providerID}/${local.model.current().modelID}`,
- messageID,
- },
+ sessionID,
+ command: command.slice(1),
+ arguments: args.join(" "),
+ agent: local.agent.current().name,
+ model: `${selectedModel.providerID}/${selectedModel.modelID}`,
+ messageID,
})
} else {
sdk.client.session.prompt({
- path: {
- id: sessionID,
- },
- body: {
- ...local.model.current(),
- messageID,
- agent: local.agent.current().name,
- model: local.model.current(),
- parts: [
- {
- id: Identifier.ascending("part"),
- type: "text",
- text: inputText,
- },
- ...nonTextParts.map((x) => ({
- id: Identifier.ascending("part"),
- ...x,
- })),
- ],
- },
+ sessionID,
+ ...selectedModel,
+ messageID,
+ agent: local.agent.current().name,
+ model: selectedModel,
+ parts: [
+ {
+ id: Identifier.ascending("part"),
+ type: "text",
+ text: inputText,
+ },
+ ...nonTextParts.map((x) => ({
+ id: Identifier.ascending("part"),
+ ...x,
+ })),
+ ],
})
}
- history.append(store.prompt)
+ history.append({
+ ...store.prompt,
+ mode: currentMode,
+ })
input.extmarks.clear()
setStore("prompt", {
input: "",
@@ -586,12 +659,16 @@ export function Prompt(props: PromptProps) {
frames: createFrames({
color,
style: "blocks",
- inactiveFactor: 0.25,
+ inactiveFactor: 0.6,
+ // enableFading: false,
+ minAlpha: 0.3,
}),
color: createColors({
color,
style: "blocks",
- inactiveFactor: 0.25,
+ inactiveFactor: 0.6,
+ // enableFading: false,
+ minAlpha: 0.3,
}),
}
})
@@ -637,9 +714,9 @@ export function Prompt(props: PromptProps) {
flexGrow={1}
>
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
index b6e363c86..b9ef2580b 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -25,7 +25,7 @@ import {
type ScrollAcceleration,
} from "@opentui/core"
import { Prompt, type PromptRef } from "@tui/component/prompt"
-import type { AssistantMessage, Part, ToolPart, UserMessage, TextPart, ReasoningPart } from "@opencode-ai/sdk"
+import type { AssistantMessage, Part, ToolPart, UserMessage, TextPart, ReasoningPart } from "@opencode-ai/sdk/v2"
import { useLocal } from "@tui/context/local"
import { Locale } from "@/util/locale"
import type { Tool } from "@/tool/tool"
@@ -63,6 +63,8 @@ import { useKV } from "../../context/kv.tsx"
import { Editor } from "../../util/editor"
import stripAnsi from "strip-ansi"
import { Footer } from "./footer.tsx"
+import { usePromptRef } from "../../context/prompt"
+import { Filesystem } from "@/util/filesystem"
addDefaultParsers(parsers.parsers)
@@ -81,6 +83,8 @@ const context = createContext<{
conceal: () => boolean
showThinking: () => boolean
showTimestamps: () => boolean
+ usernameVisible: () => boolean
+ showDetails: () => boolean
diffWrapMode: () => "word" | "none"
sync: ReturnType
}>()
@@ -97,6 +101,7 @@ export function Session() {
const sync = useSync()
const kv = useKV()
const { theme } = useTheme()
+ const promptRef = usePromptRef()
const session = createMemo(() => sync.session.get(route.sessionID)!)
const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
const permissions = createMemo(() => sync.data.permission[route.sessionID] ?? [])
@@ -114,6 +119,9 @@ export function Session() {
const [conceal, setConceal] = createSignal(true)
const [showThinking, setShowThinking] = createSignal(kv.get("thinking_visibility", true))
const [showTimestamps, setShowTimestamps] = createSignal(kv.get("timestamps", "hide") === "show")
+ const [usernameVisible, setUsernameVisible] = createSignal(kv.get("username_visible", true))
+ const [showDetails, setShowDetails] = createSignal(kv.get("tool_details_visibility", true))
+ const [showScrollbar, setShowScrollbar] = createSignal(kv.get("scrollbar_visible", false))
const [diffWrapMode, setDiffWrapMode] = createSignal<"word" | "none">("word")
const wide = createMemo(() => dimensions().width > 120)
@@ -134,7 +142,7 @@ export function Session() {
return new CustomSpeedScroll(tui.scroll_speed)
}
- return new CustomSpeedScroll(process.platform === "win32" ? 3 : 1)
+ return new CustomSpeedScroll(3)
})
createEffect(async () => {
@@ -143,7 +151,8 @@ export function Session() {
.then(() => {
if (scroll) scroll.scrollBy(100_000)
})
- .catch(() => {
+ .catch((e) => {
+ console.error(e)
toast.show({
message: `Session not found: ${route.sessionID}`,
variant: "error",
@@ -195,14 +204,10 @@ export function Session() {
return
})
if (response) {
- sdk.client.postSessionIdPermissionsPermissionId({
- path: {
- permissionID: first.id,
- id: route.sessionID,
- },
- body: {
- response: response,
- },
+ sdk.client.permission.respond({
+ permissionID: first.id,
+ sessionID: route.sessionID,
+ response: response,
})
}
}
@@ -235,6 +240,32 @@ export function Session() {
const command = useCommandDialog()
command.register(() => [
+ ...(sync.data.config.share !== "disabled"
+ ? [
+ {
+ title: "Share session",
+ value: "session.share",
+ suggested: route.type === "session",
+ keybind: "session_share" as const,
+ disabled: !!session()?.share?.url,
+ category: "Session",
+ onSelect: async (dialog: any) => {
+ await sdk.client.session
+ .share({
+ sessionID: route.sessionID,
+ })
+ .then((res) =>
+ Clipboard.copy(res.data!.share!.url).catch(() =>
+ toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }),
+ ),
+ )
+ .then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" }))
+ .catch(() => toast.show({ message: "Failed to share session", variant: "error" }))
+ dialog.clear()
+ },
+ },
+ ]
+ : []),
{
title: "Rename session",
value: "session.rename",
@@ -259,6 +290,7 @@ export function Session() {
if (child) scroll.scrollBy(child.y - scroll.y - 1)
}}
sessionID={route.sessionID}
+ setPrompt={(promptInfo) => prompt.set(promptInfo)}
/>
))
},
@@ -269,57 +301,36 @@ export function Session() {
keybind: "session_compact",
category: "Session",
onSelect: (dialog) => {
+ const selectedModel = local.model.current()
+ if (!selectedModel) {
+ toast.show({
+ variant: "warning",
+ message: "Connect a provider to summarize this session",
+ duration: 3000,
+ })
+ return
+ }
sdk.client.session.summarize({
- path: {
- id: route.sessionID,
- },
- body: {
- modelID: local.model.current().modelID,
- providerID: local.model.current().providerID,
- },
+ sessionID: route.sessionID,
+ modelID: selectedModel.modelID,
+ providerID: selectedModel.providerID,
})
dialog.clear()
},
},
- ...(sync.data.config.share !== "disabled"
- ? [
- {
- title: "Share session",
- value: "session.share",
- keybind: "session_share" as const,
- disabled: !!session()?.share?.url,
- category: "Session",
- onSelect: async (dialog: any) => {
- await sdk.client.session
- .share({
- path: {
- id: route.sessionID,
- },
- })
- .then((res) =>
- Clipboard.copy(res.data!.share!.url).catch(() =>
- toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }),
- ),
- )
- .then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" }))
- .catch(() => toast.show({ message: "Failed to share session", variant: "error" }))
- dialog.clear()
- },
- },
- ]
- : []),
{
title: "Unshare session",
value: "session.unshare",
keybind: "session_unshare",
disabled: !session()?.share?.url,
category: "Session",
- onSelect: (dialog) => {
- sdk.client.session.unshare({
- path: {
- id: route.sessionID,
- },
- })
+ onSelect: async (dialog) => {
+ await sdk.client.session
+ .unshare({
+ sessionID: route.sessionID,
+ })
+ .then(() => toast.show({ message: "Session unshared successfully", variant: "success" }))
+ .catch(() => toast.show({ message: "Failed to unshare session", variant: "error" }))
dialog.clear()
},
},
@@ -330,18 +341,14 @@ export function Session() {
category: "Session",
onSelect: async (dialog) => {
const status = sync.data.session_status[route.sessionID]
- if (status?.type !== "idle") await sdk.client.session.abort({ path: { id: route.sessionID } }).catch(() => {})
+ if (status?.type !== "idle") await sdk.client.session.abort({ sessionID: route.sessionID }).catch(() => {})
const revert = session().revert?.messageID
const message = messages().findLast((x) => (!revert || x.id < revert) && x.role === "user")
if (!message) return
sdk.client.session
.revert({
- path: {
- id: route.sessionID,
- },
- body: {
- messageID: message.id,
- },
+ sessionID: route.sessionID,
+ messageID: message.id,
})
.then(() => {
toBottom()
@@ -375,20 +382,14 @@ export function Session() {
const message = messages().find((x) => x.role === "user" && x.id > messageID)
if (!message) {
sdk.client.session.unrevert({
- path: {
- id: route.sessionID,
- },
+ sessionID: route.sessionID,
})
prompt.set({ input: "", parts: [] })
return
}
sdk.client.session.revert({
- path: {
- id: route.sessionID,
- },
- body: {
- messageID: message.id,
- },
+ sessionID: route.sessionID,
+ messageID: message.id,
})
},
},
@@ -408,6 +409,20 @@ export function Session() {
dialog.clear()
},
},
+ {
+ title: usernameVisible() ? "Hide username" : "Show username",
+ value: "session.username_visible.toggle",
+ keybind: "username_toggle",
+ category: "Session",
+ onSelect: (dialog) => {
+ setUsernameVisible((prev) => {
+ const next = !prev
+ kv.set("username_visible", next)
+ return next
+ })
+ dialog.clear()
+ },
+ },
{
title: "Toggle code concealment",
value: "session.toggle.conceal",
@@ -419,7 +434,7 @@ export function Session() {
},
},
{
- title: "Toggle timestamps",
+ title: showTimestamps() ? "Hide timestamps" : "Show timestamps",
value: "session.toggle.timestamps",
category: "Session",
onSelect: (dialog) => {
@@ -453,6 +468,32 @@ export function Session() {
dialog.clear()
},
},
+ {
+ title: showDetails() ? "Hide tool details" : "Show tool details",
+ value: "session.toggle.actions",
+ keybind: "tool_details",
+ category: "Session",
+ onSelect: (dialog) => {
+ const newValue = !showDetails()
+ setShowDetails(newValue)
+ kv.set("tool_details_visibility", newValue)
+ dialog.clear()
+ },
+ },
+ {
+ title: "Toggle session scrollbar",
+ value: "session.toggle.scrollbar",
+ keybind: "scrollbar_toggle",
+ category: "Session",
+ onSelect: (dialog) => {
+ setShowScrollbar((prev) => {
+ const next = !prev
+ kv.set("scrollbar_visible", next)
+ return next
+ })
+ dialog.clear()
+ },
+ },
{
title: "Page up",
value: "session.page.up",
@@ -519,6 +560,37 @@ export function Session() {
dialog.clear()
},
},
+ {
+ title: "Jump to last user message",
+ value: "session.messages_last_user",
+ keybind: "messages_last_user",
+ category: "Session",
+ onSelect: () => {
+ const messages = sync.data.message[route.sessionID]
+ if (!messages || !messages.length) return
+
+ // Find the most recent user message with non-ignored, non-synthetic text parts
+ for (let i = messages.length - 1; i >= 0; i--) {
+ const message = messages[i]
+ if (!message || message.role !== "user") continue
+
+ const parts = sync.data.part[message.id]
+ if (!parts || !Array.isArray(parts)) continue
+
+ const hasValidTextPart = parts.some(
+ (part) => part && part.type === "text" && !part.synthetic && !part.ignored,
+ )
+
+ if (hasValidTextPart) {
+ const child = scroll.getChildren().find((child) => {
+ return child.id === message.id
+ })
+ if (child) scroll.scrollBy(child.y - scroll.y - 1)
+ break
+ }
+ }
+ },
+ },
{
title: "Copy last assistant message",
value: "messages.copy",
@@ -754,6 +826,8 @@ export function Session() {
conceal,
showThinking,
showTimestamps,
+ usernameVisible,
+ showDetails,
diffWrapMode,
sync,
}}
@@ -766,9 +840,9 @@ export function Session() {
(scroll = r)}
- scrollbarOptions={{
- paddingLeft: 2,
- visible: false,
+ verticalScrollbarOptions={{
+ paddingLeft: 1,
+ visible: showScrollbar(),
trackOptions: {
backgroundColor: theme.backgroundElement,
foregroundColor: theme.border,
@@ -825,7 +899,7 @@ export function Session() {
{(file) => (
-
+
{file.filename}
0}>
+{file.additions}
@@ -877,7 +951,10 @@ export function Session() {
(prompt = r)}
+ ref={(r) => {
+ prompt = r
+ promptRef.set(r)
+ }}
disabled={permissions().length > 0}
onSubmit={() => {
toBottom()
@@ -917,13 +994,14 @@ function UserMessage(props: {
pending?: string
}) {
const ctx = use()
+ const local = useLocal()
const text = createMemo(() => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0])
const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : [])))
const sync = useSync()
const { theme } = useTheme()
const [hover, setHover] = createSignal(false)
const queued = createMemo(() => props.pending && props.message.id > props.pending)
- const color = createMemo(() => (queued() ? theme.accent : theme.secondary))
+ const color = createMemo(() => (queued() ? theme.accent : local.agent.color(props.message.agent)))
const compaction = createMemo(() => props.parts.find((x) => x.type === "compaction"))
@@ -972,17 +1050,19 @@ function UserMessage(props: {
- {sync.data.config.username ?? "You"}{" "}
+ {ctx.usernameVisible() ? `${sync.data.config.username ?? "You "}` : "You "}
- {ctx.showTimestamps()
- ? Locale.todayTimeOrDateTime(props.message.time.created)
- : Locale.time(props.message.time.created)}
-
+
+
+ {ctx.usernameVisible() ? " · " : " "}
+ {Locale.todayTimeOrDateTime(props.message.time.created)}
+
+
}
>
+
QUEUED
@@ -1078,7 +1158,11 @@ const PART_MAPPING = {
function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: AssistantMessage }) {
const { theme, subtleSyntax } = useTheme()
const ctx = use()
- const content = createMemo(() => props.part.text.trim())
+ const content = createMemo(() => {
+ // Filter out redacted reasoning chunks from OpenRouter
+ // OpenRouter sends encrypted reasoning data that appears as [REDACTED]
+ return props.part.text.replace("[REDACTED]", "").trim()
+ })
return (
{
+ // Hide tool if showDetails is false and tool completed successfully
+ // But always show if there's an error or permission is required
+ const shouldHide =
+ !showDetails() &&
+ props.part.state.status === "completed" &&
+ !sync.data.permission[props.message.sessionID]?.some((x) => x.callID === props.part.callID)
+
+ if (shouldHide) {
+ return undefined
+ }
+
const render = ToolRegistry.render(props.part.tool) ?? GenericTool
const metadata = props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {})
@@ -1201,15 +1297,15 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess
Permission required to run this tool:
-
+
enter
accept
-
+
a
accept always
-
+
d
deny
@@ -1319,7 +1415,10 @@ ToolRegistry.register({
return props.input.content
})
- const diagnostics = createMemo(() => props.metadata.diagnostics?.[props.input.filePath ?? ""] ?? [])
+ const diagnostics = createMemo(() => {
+ const filePath = Filesystem.normalizePath(props.input.filePath ?? "")
+ return props.metadata.diagnostics?.[filePath] ?? []
+ })
return (
<>
@@ -1327,7 +1426,13 @@ ToolRegistry.register({
Wrote {props.input.filePath}
-
+
@@ -1406,11 +1511,15 @@ ToolRegistry.register({
- {(task) => (
-
- ∟ {Locale.titlecase(task.tool)} {task.state.status === "completed" ? task.state.title : ""}
-
- )}
+ {(task, index) => {
+ const summary = props.metadata.summary ?? []
+ return (
+
+ {index() === summary.length - 1 ? "└" : "├"} {Locale.titlecase(task.tool)}{" "}
+ {task.state.status === "completed" ? task.state.title : ""}
+
+ )
+ }}
@@ -1482,7 +1591,8 @@ ToolRegistry.register({
const diffContent = createMemo(() => props.metadata.diff ?? props.permission["diff"])
const diagnostics = createMemo(() => {
- const arr = props.metadata.diagnostics?.[props.input.filePath ?? ""] ?? []
+ const filePath = Filesystem.normalizePath(props.input.filePath ?? "")
+ const arr = props.metadata.diagnostics?.[filePath] ?? []
return arr.filter((x) => x.severity === 1).slice(0, 3)
})
@@ -1504,6 +1614,7 @@ ToolRegistry.register({
showLineNumbers={true}
width="100%"
wrapMode={ctx.diffWrapMode()}
+ fg={theme.text}
addedBg={theme.diffAddedBg}
removedBg={theme.diffRemovedBg}
contextBg={theme.diffContextBg}
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
index c63f5116a..b64a18ae2 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
@@ -4,11 +4,12 @@ import { createStore } from "solid-js/store"
import { useTheme } from "../../context/theme"
import { Locale } from "@/util/locale"
import path from "path"
-import type { AssistantMessage } from "@opencode-ai/sdk"
+import type { AssistantMessage } from "@opencode-ai/sdk/v2"
import { Global } from "@/global"
import { Installation } from "@/installation"
import { useKeybind } from "../../context/keybind"
import { useDirectory } from "../../context/directory"
+import { useKV } from "../../context/kv"
export function Sidebar(props: { sessionID: string }) {
const sync = useSync()
@@ -48,12 +49,13 @@ export function Sidebar(props: { sessionID: string }) {
}
})
- const keybind = useKeybind()
const directory = useDirectory()
+ const kv = useKV()
const hasProviders = createMemo(() =>
sync.data.provider.some((x) => x.id !== "opencode" || Object.values(x.models).some((y) => y.cost?.input !== 0)),
)
+ const gettingStartedDismissed = createMemo(() => kv.get("dismissed_getting_started", false))
return (
@@ -104,11 +106,15 @@ export function Sidebar(props: { sessionID: string }) {
+ )[item.status],
}}
>
•
@@ -116,10 +122,14 @@ export function Sidebar(props: { sessionID: string }) {
{key}{" "}
-
+
Connected
{(val) => {val().error}}
- Disabled in configuration
+ Disabled
+ Needs auth
+
+ Needs client ID
+
@@ -240,8 +250,8 @@ export function Sidebar(props: { sessionID: string }) {
-
-
+
+
- ⬖
+
+ ⬖
+
-
- Getting started
-
+
+
+ Getting started
+
+ kv.set("dismissed_getting_started", true)}>
+ ✕
+
+
OpenCode includes free models so you can start immediately.
Connect from 75+ providers to use other models, including Claude, GPT, Gemini etc
- Connect provider
+ Connect provider
/connect
- {directory()}
+
+ {directory().split("/").slice(0, -1).join("/")}/
+ {directory().split("/").at(-1)}
+
• Open
diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts
index 79638c5e8..3cf8937a9 100644
--- a/packages/opencode/src/cli/cmd/tui/thread.ts
+++ b/packages/opencode/src/cli/cmd/tui/thread.ts
@@ -60,7 +60,6 @@ export const TuiThreadCommand = cmd({
const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
const localWorker = new URL("./worker.ts", import.meta.url)
const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url)
- const execDir = path.dirname(process.execPath)
const workerPath = await iife(async () => {
if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
if (await Bun.file(distWorker).exists()) return distWorker
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx
index 96ef982d7..45e946fa7 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx
@@ -22,7 +22,9 @@ export function DialogAlert(props: DialogAlertProps) {
return (
- {props.title}
+
+ {props.title}
+
esc
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx
index 9d0e7d2c7..8431a3946 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx
@@ -34,7 +34,9 @@ export function DialogConfirm(props: DialogConfirmProps) {
return (
- {props.title}
+
+ {props.title}
+
esc
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx
index db9648f2c..056ce41da 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx
@@ -18,7 +18,9 @@ export function DialogHelp() {
return (
- Help
+
+ Help
+
esc/enter
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx
index 9ae370658..1b9acb589 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx
@@ -35,7 +35,9 @@ export function DialogPrompt(props: DialogPromptProps) {
return (
- {props.title}
+
+ {props.title}
+
esc
@@ -49,6 +51,9 @@ export function DialogPrompt(props: DialogPromptProps) {
ref={(val: TextareaRenderable) => (textarea = val)}
initialValue={props.value}
placeholder={props.placeholder ?? "Enter text"}
+ textColor={theme.text}
+ focusedTextColor={theme.text}
+ cursorColor={theme.text}
/>
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
index f6d79946c..8e9c17f70 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
@@ -36,6 +36,7 @@ export interface DialogSelectOption {
category?: string
disabled?: boolean
bg?: RGBA
+ gutter?: JSX.Element
onSelect?: (ctx: DialogContext, trigger?: "prompt") => void
}
@@ -239,7 +240,7 @@ export function DialogSelect(props: DialogSelectProps) {
moveTo(index)
}}
backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)}
- paddingLeft={current() ? 1 : 3}
+ paddingLeft={current() || option.gutter ? 1 : 3}
paddingRight={3}
gap={1}
>
@@ -249,6 +250,7 @@ export function DialogSelect(props: DialogSelectProps) {
description={option.description !== category ? option.description : undefined}
active={active()}
current={current()}
+ gutter={option.gutter}
/>
)
@@ -282,6 +284,7 @@ function Option(props: {
active?: boolean
current?: boolean
footer?: JSX.Element | string
+ gutter?: JSX.Element
onMouseOver?: () => void
}) {
const { theme } = useTheme()
@@ -294,16 +297,22 @@ function Option(props: {
●
+
+
+ {props.gutter}
+
+
- {Locale.truncate(props.title, 62)}
- {props.description}
+ {Locale.truncate(props.title, 61)}
+
+ {props.description}
+
diff --git a/packages/opencode/src/cli/cmd/tui/ui/spinner.ts b/packages/opencode/src/cli/cmd/tui/ui/spinner.ts
index 5c99acccb..c185ea7b8 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/spinner.ts
+++ b/packages/opencode/src/cli/cmd/tui/ui/spinner.ts
@@ -8,6 +8,8 @@ interface AdvancedGradientOptions {
defaultColor?: ColorInput
direction?: "forward" | "backward" | "bidirectional"
holdFrames?: { start?: number; end?: number }
+ enableFading?: boolean
+ minAlpha?: number
}
interface ScannerState {
@@ -137,13 +139,16 @@ function calculateColorIndex(
}
function createKnightRiderTrail(options: AdvancedGradientOptions): ColorGenerator {
- const { colors, defaultColor } = options
+ const { colors, defaultColor, enableFading = true, minAlpha = 0 } = options
// Use the provided defaultColor if it's an RGBA instance, otherwise convert/default
// We use RGBA.fromHex for the fallback to ensure we have an RGBA object.
// Note: If defaultColor is a string, we convert it once here.
const defaultRgba = defaultColor instanceof RGBA ? defaultColor : RGBA.fromHex((defaultColor as string) || "#000000")
+ // Store the base alpha from the inactive factor
+ const baseInactiveAlpha = defaultRgba.a
+
let cachedFrameIndex = -1
let cachedState: ScannerState | null = null
@@ -160,22 +165,22 @@ function createKnightRiderTrail(options: AdvancedGradientOptions): ColorGenerato
// Calculate global fade for inactive dots during hold or movement
const { isHolding, holdProgress, holdTotal, movementProgress, movementTotal } = state
- let alpha = 1.0
- if (isHolding && holdTotal > 0) {
- // Fade out linearly
- const progress = Math.min(holdProgress / holdTotal, 1)
- alpha = Math.max(0, 1 - progress)
- } else if (!isHolding && movementTotal > 0) {
- // Fade in linearly during movement
- const progress = Math.min(movementProgress / Math.max(1, movementTotal - 1), 1)
- alpha = progress
+ let fadeFactor = 1.0
+ if (enableFading) {
+ if (isHolding && holdTotal > 0) {
+ // Fade out linearly to minAlpha
+ const progress = Math.min(holdProgress / holdTotal, 1)
+ fadeFactor = Math.max(minAlpha, 1 - progress * (1 - minAlpha))
+ } else if (!isHolding && movementTotal > 0) {
+ // Fade in linearly from minAlpha during movement
+ const progress = Math.min(movementProgress / Math.max(1, movementTotal - 1), 1)
+ fadeFactor = minAlpha + progress * (1 - minAlpha)
+ }
}
- // Mutate the alpha of the default RGBA object
- // This assumes single-threaded, synchronous rendering per frame
- // where we can modify the state for the current frame.
- // Since this is run for every char in the frame, setting it repeatedly to the same value is fine.
- defaultRgba.a = alpha
+ // Combine base inactive alpha with the fade factor
+ // This ensures inactiveFactor is respected while still allowing fading animation
+ defaultRgba.a = baseInactiveAlpha * fadeFactor
if (index === -1) {
return defaultRgba
@@ -186,10 +191,10 @@ function createKnightRiderTrail(options: AdvancedGradientOptions): ColorGenerato
}
/**
- * Derives a gradient of tail colors from a single bright color
+ * Derives a gradient of tail colors from a single bright color using alpha falloff
* @param brightColor The brightest color (center/head of the scanner)
* @param steps Number of gradient steps (default: 6)
- * @returns Array of RGBA colors from brightest to darkest
+ * @returns Array of RGBA colors with alpha-based trail fade (background-independent)
*/
export function deriveTrailColors(brightColor: ColorInput, steps: number = 6): RGBA[] {
const baseRgba = brightColor instanceof RGBA ? brightColor : RGBA.fromHex(brightColor as string)
@@ -197,45 +202,45 @@ export function deriveTrailColors(brightColor: ColorInput, steps: number = 6): R
const colors: RGBA[] = []
for (let i = 0; i < steps; i++) {
- // Progressive darkening:
- // i=0: 100% brightness (original color)
- // i=1: add slight bloom/glare (lighten)
- // i=2+: progressively darken
- let factor: number
+ // Alpha-based falloff with optional bloom effect
+ let alpha: number
+ let brightnessFactor: number
if (i === 0) {
- factor = 1.0 // Original brightness
+ // Lead position: full brightness and opacity
+ alpha = 1.0
+ brightnessFactor = 1.0
} else if (i === 1) {
- factor = 1.2 // Slight bloom/glare effect
+ // Slight bloom/glare effect: brighten color but reduce opacity slightly
+ alpha = 0.9
+ brightnessFactor = 1.15
} else {
- // Exponential decay for natural-looking trail fade
- factor = Math.pow(0.6, i - 1)
+ // Exponential alpha decay for natural-looking trail fade
+ alpha = Math.pow(0.65, i - 1)
+ brightnessFactor = 1.0
}
- const r = Math.min(1.0, baseRgba.r * factor)
- const g = Math.min(1.0, baseRgba.g * factor)
- const b = Math.min(1.0, baseRgba.b * factor)
+ const r = Math.min(1.0, baseRgba.r * brightnessFactor)
+ const g = Math.min(1.0, baseRgba.g * brightnessFactor)
+ const b = Math.min(1.0, baseRgba.b * brightnessFactor)
- colors.push(RGBA.fromValues(r, g, b, 1.0))
+ colors.push(RGBA.fromValues(r, g, b, alpha))
}
return colors
}
/**
- * Derives the inactive/default color from a bright color
+ * Derives the inactive/default color from a bright color using alpha
* @param brightColor The brightest color (center/head of the scanner)
- * @param factor Brightness factor for inactive color (default: 0.2)
- * @returns A much darker version suitable for inactive dots
+ * @param factor Alpha factor for inactive color (default: 0.2, range: 0-1)
+ * @returns The same color with reduced alpha for background-independent dimming
*/
export function deriveInactiveColor(brightColor: ColorInput, factor: number = 0.2): RGBA {
const baseRgba = brightColor instanceof RGBA ? brightColor : RGBA.fromHex(brightColor as string)
- const r = baseRgba.r * factor
- const g = baseRgba.g * factor
- const b = baseRgba.b * factor
-
- return RGBA.fromValues(r, g, b, 1.0)
+ // Use the full color brightness but adjust alpha for background-independent dimming
+ return RGBA.fromValues(baseRgba.r, baseRgba.g, baseRgba.b, factor)
}
export type KnightRiderStyle = "blocks" | "diamonds"
@@ -251,8 +256,12 @@ export interface KnightRiderOptions {
/** Number of trail steps when using single color (default: 6) */
trailSteps?: number
defaultColor?: ColorInput
- /** Brightness factor for inactive color when using single color (default: 0.2) */
+ /** Alpha factor for inactive color when using single color (default: 0.2, range: 0-1) */
inactiveFactor?: number
+ /** Enable fading of inactive dots during hold and movement (default: true) */
+ enableFading?: boolean
+ /** Minimum alpha value when fading (default: 0, range: 0-1) */
+ minAlpha?: number
}
/**
@@ -289,6 +298,8 @@ export function createFrames(options: KnightRiderOptions = {}): string[] {
defaultColor,
direction: "bidirectional" as const,
holdFrames: { start: holdStart, end: holdEnd },
+ enableFading: options.enableFading,
+ minAlpha: options.minAlpha,
}
// Bidirectional cycle: Forward (width) + Hold End + Backward (width-1) + Hold Start
@@ -349,6 +360,8 @@ export function createColors(options: KnightRiderOptions = {}): ColorGenerator {
defaultColor,
direction: "bidirectional" as const,
holdFrames: { start: holdStart, end: holdEnd },
+ enableFading: options.enableFading,
+ minAlpha: options.minAlpha,
}
return createKnightRiderTrail(trailOptions)
diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts
index c62630e0c..398aff5af 100644
--- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts
+++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts
@@ -61,7 +61,7 @@ export namespace Clipboard {
const getCopyMethod = lazy(() => {
const os = platform()
- if (os === "darwin" && Bun.which("oascript")) {
+ if (os === "darwin" && Bun.which("osascript")) {
console.log("clipboard: using osascript")
return async (text: string) => {
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts
index 50274f442..76f78f3fa 100644
--- a/packages/opencode/src/cli/cmd/tui/worker.ts
+++ b/packages/opencode/src/cli/cmd/tui/worker.ts
@@ -2,8 +2,10 @@ import { Installation } from "@/installation"
import { Server } from "@/server/server"
import { Log } from "@/util/log"
import { Instance } from "@/project/instance"
+import { InstanceBootstrap } from "@/project/bootstrap"
import { Rpc } from "@/util/rpc"
import { upgrade } from "@/cli/upgrade"
+import type { BunWebSocketData } from "hono/bun"
await Log.init({
print: process.argv.includes("--print-logs"),
@@ -26,7 +28,7 @@ process.on("uncaughtException", (e) => {
})
})
-let server: Bun.Server
+let server: Bun.Server
export const rpc = {
async server(input: { port: number; hostname: string }) {
if (server) await server.stop(true)
@@ -43,6 +45,7 @@ export const rpc = {
async checkUpgrade(input: { directory: string }) {
await Instance.provide({
directory: input.directory,
+ init: InstanceBootstrap,
fn: async () => {
await upgrade().catch(() => {})
},
@@ -51,7 +54,9 @@ export const rpc = {
async shutdown() {
Log.Default.info("worker shutting down")
await Instance.disposeAll()
- await server.stop(true)
+ // TODO: this should be awaited, but ws connections are
+ // causing this to hang, need to revisit this
+ server.stop(true)
},
}
diff --git a/packages/opencode/src/cli/cmd/uninstall.ts b/packages/opencode/src/cli/cmd/uninstall.ts
new file mode 100644
index 000000000..62210d575
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/uninstall.ts
@@ -0,0 +1,344 @@
+import type { Argv } from "yargs"
+import { UI } from "../ui"
+import * as prompts from "@clack/prompts"
+import { Installation } from "../../installation"
+import { Global } from "../../global"
+import { $ } from "bun"
+import fs from "fs/promises"
+import path from "path"
+import os from "os"
+
+interface UninstallArgs {
+ keepConfig: boolean
+ keepData: boolean
+ dryRun: boolean
+ force: boolean
+}
+
+interface RemovalTargets {
+ directories: Array<{ path: string; label: string; keep: boolean }>
+ shellConfig: string | null
+ binary: string | null
+}
+
+export const UninstallCommand = {
+ command: "uninstall",
+ describe: "uninstall opencode and remove all related files",
+ builder: (yargs: Argv) =>
+ yargs
+ .option("keep-config", {
+ alias: "c",
+ type: "boolean",
+ describe: "keep configuration files",
+ default: false,
+ })
+ .option("keep-data", {
+ alias: "d",
+ type: "boolean",
+ describe: "keep session data and snapshots",
+ default: false,
+ })
+ .option("dry-run", {
+ type: "boolean",
+ describe: "show what would be removed without removing",
+ default: false,
+ })
+ .option("force", {
+ alias: "f",
+ type: "boolean",
+ describe: "skip confirmation prompts",
+ default: false,
+ }),
+
+ handler: async (args: UninstallArgs) => {
+ UI.empty()
+ UI.println(UI.logo(" "))
+ UI.empty()
+ prompts.intro("Uninstall OpenCode")
+
+ const method = await Installation.method()
+ prompts.log.info(`Installation method: ${method}`)
+
+ const targets = await collectRemovalTargets(args, method)
+
+ await showRemovalSummary(targets, method)
+
+ if (!args.force && !args.dryRun) {
+ const confirm = await prompts.confirm({
+ message: "Are you sure you want to uninstall?",
+ initialValue: false,
+ })
+ if (!confirm || prompts.isCancel(confirm)) {
+ prompts.outro("Cancelled")
+ return
+ }
+ }
+
+ if (args.dryRun) {
+ prompts.log.warn("Dry run - no changes made")
+ prompts.outro("Done")
+ return
+ }
+
+ await executeUninstall(method, targets)
+
+ prompts.outro("Done")
+ },
+}
+
+async function collectRemovalTargets(args: UninstallArgs, method: Installation.Method): Promise {
+ const directories: RemovalTargets["directories"] = [
+ { path: Global.Path.data, label: "Data", keep: args.keepData },
+ { path: Global.Path.cache, label: "Cache", keep: false },
+ { path: Global.Path.config, label: "Config", keep: args.keepConfig },
+ { path: Global.Path.state, label: "State", keep: false },
+ ]
+
+ const shellConfig = method === "curl" ? await getShellConfigFile() : null
+ const binary = method === "curl" ? process.execPath : null
+
+ return { directories, shellConfig, binary }
+}
+
+async function showRemovalSummary(targets: RemovalTargets, method: Installation.Method) {
+ prompts.log.message("The following will be removed:")
+
+ for (const dir of targets.directories) {
+ const exists = await fs
+ .access(dir.path)
+ .then(() => true)
+ .catch(() => false)
+ if (!exists) continue
+
+ const size = await getDirectorySize(dir.path)
+ const sizeStr = formatSize(size)
+ const status = dir.keep ? UI.Style.TEXT_DIM + "(keeping)" : ""
+ const prefix = dir.keep ? "○" : "✓"
+
+ prompts.log.info(` ${prefix} ${dir.label}: ${shortenPath(dir.path)} ${UI.Style.TEXT_DIM}(${sizeStr})${status}`)
+ }
+
+ if (targets.binary) {
+ prompts.log.info(` ✓ Binary: ${shortenPath(targets.binary)}`)
+ }
+
+ if (targets.shellConfig) {
+ prompts.log.info(` ✓ Shell PATH in ${shortenPath(targets.shellConfig)}`)
+ }
+
+ if (method !== "curl" && method !== "unknown") {
+ const cmds: Record = {
+ npm: "npm uninstall -g opencode-ai",
+ pnpm: "pnpm uninstall -g opencode-ai",
+ bun: "bun remove -g opencode-ai",
+ yarn: "yarn global remove opencode-ai",
+ brew: "brew uninstall opencode",
+ }
+ prompts.log.info(` ✓ Package: ${cmds[method] || method}`)
+ }
+}
+
+async function executeUninstall(method: Installation.Method, targets: RemovalTargets) {
+ const spinner = prompts.spinner()
+ const errors: string[] = []
+
+ for (const dir of targets.directories) {
+ if (dir.keep) {
+ prompts.log.step(`Skipping ${dir.label} (--keep-${dir.label.toLowerCase()})`)
+ continue
+ }
+
+ const exists = await fs
+ .access(dir.path)
+ .then(() => true)
+ .catch(() => false)
+ if (!exists) continue
+
+ spinner.start(`Removing ${dir.label}...`)
+ const err = await fs.rm(dir.path, { recursive: true, force: true }).catch((e) => e)
+ if (err) {
+ spinner.stop(`Failed to remove ${dir.label}`, 1)
+ errors.push(`${dir.label}: ${err.message}`)
+ continue
+ }
+ spinner.stop(`Removed ${dir.label}`)
+ }
+
+ if (targets.shellConfig) {
+ spinner.start("Cleaning shell config...")
+ const err = await cleanShellConfig(targets.shellConfig).catch((e) => e)
+ if (err) {
+ spinner.stop("Failed to clean shell config", 1)
+ errors.push(`Shell config: ${err.message}`)
+ } else {
+ spinner.stop("Cleaned shell config")
+ }
+ }
+
+ if (method !== "curl" && method !== "unknown") {
+ const cmds: Record = {
+ npm: ["npm", "uninstall", "-g", "opencode-ai"],
+ pnpm: ["pnpm", "uninstall", "-g", "opencode-ai"],
+ bun: ["bun", "remove", "-g", "opencode-ai"],
+ yarn: ["yarn", "global", "remove", "opencode-ai"],
+ brew: ["brew", "uninstall", "opencode"],
+ }
+
+ const cmd = cmds[method]
+ if (cmd) {
+ spinner.start(`Running ${cmd.join(" ")}...`)
+ const result = await $`${cmd}`.quiet().nothrow()
+ if (result.exitCode !== 0) {
+ spinner.stop(`Package manager uninstall failed`, 1)
+ prompts.log.warn(`You may need to run manually: ${cmd.join(" ")}`)
+ errors.push(`Package manager: exit code ${result.exitCode}`)
+ } else {
+ spinner.stop("Package removed")
+ }
+ }
+ }
+
+ if (method === "curl" && targets.binary) {
+ UI.empty()
+ prompts.log.message("To finish removing the binary, run:")
+ prompts.log.info(` rm "${targets.binary}"`)
+
+ const binDir = path.dirname(targets.binary)
+ if (binDir.includes(".opencode")) {
+ prompts.log.info(` rmdir "${binDir}" 2>/dev/null`)
+ }
+ }
+
+ if (errors.length > 0) {
+ UI.empty()
+ prompts.log.warn("Some operations failed:")
+ for (const err of errors) {
+ prompts.log.error(` ${err}`)
+ }
+ }
+
+ UI.empty()
+ prompts.log.success("Thank you for using OpenCode!")
+}
+
+async function getShellConfigFile(): Promise {
+ const shell = path.basename(process.env.SHELL || "bash")
+ const home = os.homedir()
+ const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(home, ".config")
+
+ const configFiles: Record = {
+ fish: [path.join(xdgConfig, "fish", "config.fish")],
+ zsh: [
+ path.join(home, ".zshrc"),
+ path.join(home, ".zshenv"),
+ path.join(xdgConfig, "zsh", ".zshrc"),
+ path.join(xdgConfig, "zsh", ".zshenv"),
+ ],
+ bash: [
+ path.join(home, ".bashrc"),
+ path.join(home, ".bash_profile"),
+ path.join(home, ".profile"),
+ path.join(xdgConfig, "bash", ".bashrc"),
+ path.join(xdgConfig, "bash", ".bash_profile"),
+ ],
+ ash: [path.join(home, ".ashrc"), path.join(home, ".profile")],
+ sh: [path.join(home, ".profile")],
+ }
+
+ const candidates = configFiles[shell] || configFiles.bash
+
+ for (const file of candidates) {
+ const exists = await fs
+ .access(file)
+ .then(() => true)
+ .catch(() => false)
+ if (!exists) continue
+
+ const content = await Bun.file(file)
+ .text()
+ .catch(() => "")
+ if (content.includes("# opencode") || content.includes(".opencode/bin")) {
+ return file
+ }
+ }
+
+ return null
+}
+
+async function cleanShellConfig(file: string) {
+ const content = await Bun.file(file).text()
+ const lines = content.split("\n")
+
+ const filtered: string[] = []
+ let skip = false
+
+ for (const line of lines) {
+ const trimmed = line.trim()
+
+ if (trimmed === "# opencode") {
+ skip = true
+ continue
+ }
+
+ if (skip) {
+ skip = false
+ if (trimmed.includes(".opencode/bin") || trimmed.includes("fish_add_path")) {
+ continue
+ }
+ }
+
+ if (
+ (trimmed.startsWith("export PATH=") && trimmed.includes(".opencode/bin")) ||
+ (trimmed.startsWith("fish_add_path") && trimmed.includes(".opencode"))
+ ) {
+ continue
+ }
+
+ filtered.push(line)
+ }
+
+ while (filtered.length > 0 && filtered[filtered.length - 1].trim() === "") {
+ filtered.pop()
+ }
+
+ const output = filtered.join("\n") + "\n"
+ await Bun.write(file, output)
+}
+
+async function getDirectorySize(dir: string): Promise {
+ let total = 0
+
+ const walk = async (current: string) => {
+ const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => [])
+
+ for (const entry of entries) {
+ const full = path.join(current, entry.name)
+ if (entry.isDirectory()) {
+ await walk(full)
+ continue
+ }
+ if (entry.isFile()) {
+ const stat = await fs.stat(full).catch(() => null)
+ if (stat) total += stat.size
+ }
+ }
+ }
+
+ await walk(dir)
+ return total
+}
+
+function formatSize(bytes: number): string {
+ if (bytes < 1024) return `${bytes} B`
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
+}
+
+function shortenPath(p: string): string {
+ const home = os.homedir()
+ if (p.startsWith(home)) {
+ return p.replace(home, "~")
+ }
+ return p
+}
diff --git a/packages/opencode/src/cli/upgrade.ts b/packages/opencode/src/cli/upgrade.ts
index 2bea760b3..2d46ae39f 100644
--- a/packages/opencode/src/cli/upgrade.ts
+++ b/packages/opencode/src/cli/upgrade.ts
@@ -5,7 +5,8 @@ import { Installation } from "@/installation"
export async function upgrade() {
const config = await Config.global()
- const latest = await Installation.latest().catch(() => {})
+ const method = await Installation.method()
+ const latest = await Installation.latest(method).catch(() => {})
if (!latest) return
if (Installation.VERSION === latest) return
@@ -17,7 +18,6 @@ export async function upgrade() {
return
}
- const method = await Installation.method()
if (method === "unknown") return
await Installation.upgrade(method, latest)
.then(() => Bus.publish(Installation.Event.Updated, { version: latest }))
diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts
index 5e1ad9dc4..0a9bfc620 100644
--- a/packages/opencode/src/command/index.ts
+++ b/packages/opencode/src/command/index.ts
@@ -1,17 +1,15 @@
+import { Bus } from "@/bus"
+import { BusEvent } from "@/bus/bus-event"
import z from "zod"
import { Config } from "../config/config"
import { Instance } from "../project/instance"
-import PROMPT_INITIALIZE from "./template/initialize.txt"
-import { Bus } from "../bus"
import { Identifier } from "../id/id"
+import PROMPT_INITIALIZE from "./template/initialize.txt"
+import PROMPT_REVIEW from "./template/review.txt"
export namespace Command {
- export const Default = {
- INIT: "init",
- } as const
-
export const Event = {
- Executed: Bus.event(
+ Executed: BusEvent.define(
"command.executed",
z.object({
name: z.string(),
@@ -36,10 +34,27 @@ export namespace Command {
})
export type Info = z.infer
+ export const Default = {
+ INIT: "init",
+ REVIEW: "review",
+ } as const
+
const state = Instance.state(async () => {
const cfg = await Config.get()
- const result: Record = {}
+ const result: Record = {
+ [Default.INIT]: {
+ name: Default.INIT,
+ description: "create/update AGENTS.md",
+ template: PROMPT_INITIALIZE.replace("${path}", Instance.worktree),
+ },
+ [Default.REVIEW]: {
+ name: Default.REVIEW,
+ description: "review changes [commit|branch|pr], defaults to uncommitted",
+ template: PROMPT_REVIEW.replace("${path}", Instance.worktree),
+ subtask: true,
+ },
+ }
for (const [name, command] of Object.entries(cfg.command ?? {})) {
result[name] = {
@@ -52,14 +67,6 @@ export namespace Command {
}
}
- if (result[Default.INIT] === undefined) {
- result[Default.INIT] = {
- name: Default.INIT,
- description: "create/update AGENTS.md",
- template: PROMPT_INITIALIZE.replace("${path}", Instance.worktree),
- }
- }
-
return result
})
diff --git a/packages/opencode/src/command/template/review.txt b/packages/opencode/src/command/template/review.txt
new file mode 100644
index 000000000..6ae94ce64
--- /dev/null
+++ b/packages/opencode/src/command/template/review.txt
@@ -0,0 +1,73 @@
+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
+
+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.
+
+---
+
+## What to Look For
+
+**Bugs** - Your primary focus.
+- Logic errors, off-by-one mistakes, incorrect conditionals
+- Edge cases: null/empty inputs, error conditions, race conditions
+- Security issues: injection, auth bypass, data exposure
+- Broken error handling that swallows failures
+
+**Structure** - Does the code fit the codebase?
+- Does it follow existing patterns and conventions?
+- Are there established abstractions it should use but doesn't?
+
+**Performance** - Only flag if obviously problematic.
+- O(n²) on unbounded data, N+1 queries, blocking I/O on hot paths
+
+## 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 flag style preferences as issues
+- 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
+
+## 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.
+- **Exa 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.
+
+## Tone and Approach
+
+1. If there is a bug, be direct and clear about why it is a bug.
+2. You should clearly communicate severity of issues, do not claim issues are more severe than they actually are.
+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 in a manner that allows reader to quickly understand issue without reading too closely.
+6. AVOID flattery, do not give any comments that are not helpful to the reader. Avoid phrasing like "Great job ...", "Thanks for ...".
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index a49612090..9cf3507e1 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -1,5 +1,6 @@
import { Log } from "../util/log"
import path from "path"
+import { pathToFileURL } from "url"
import os from "os"
import z from "zod"
import { Filesystem } from "../util/filesystem"
@@ -297,7 +298,7 @@ export namespace Config {
dot: true,
cwd: dir,
})) {
- plugins.push("file://" + item)
+ plugins.push(pathToFileURL(item).href)
}
return plugins
}
@@ -325,12 +326,33 @@ export namespace Config {
ref: "McpLocalConfig",
})
+ export const McpOAuth = z
+ .object({
+ clientId: z
+ .string()
+ .optional()
+ .describe("OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted."),
+ clientSecret: z.string().optional().describe("OAuth client secret (if required by the authorization server)"),
+ scope: z.string().optional().describe("OAuth scopes to request during authorization"),
+ })
+ .strict()
+ .meta({
+ ref: "McpOAuthConfig",
+ })
+ export type McpOAuth = z.infer
+
export const McpRemote = z
.object({
type: z.literal("remote").describe("Type of MCP server connection"),
url: z.string().describe("URL of the remote MCP server"),
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"),
+ oauth: z
+ .union([McpOAuth, z.literal(false)])
+ .optional()
+ .describe(
+ "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.",
+ ),
timeout: z
.number()
.int()
@@ -375,6 +397,12 @@ export namespace Config {
.regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format")
.optional()
.describe("Hex color code for the agent (e.g., #FF5733)"),
+ maxSteps: z
+ .number()
+ .int()
+ .positive()
+ .optional()
+ .describe("Maximum number of agentic iterations before forcing text-only response"),
permission: z
.object({
edit: Permission.optional(),
@@ -398,6 +426,8 @@ export namespace Config {
editor_open: z.string().optional().default("e").describe("Open external editor"),
theme_list: z.string().optional().default("t").describe("List available themes"),
sidebar_toggle: z.string().optional().default("b").describe("Toggle sidebar"),
+ scrollbar_toggle: z.string().optional().default("none").describe("Toggle session scrollbar"),
+ username_toggle: z.string().optional().default("none").describe("Toggle username visibility"),
status_view: z.string().optional().default("s").describe("View status"),
session_export: z.string().optional().default("x").describe("Export session to editor"),
session_new: z.string().optional().default("n").describe("Create a new session"),
@@ -417,6 +447,7 @@ export namespace Config {
.describe("Scroll messages down by half page"),
messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"),
messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"),
+ messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"),
messages_copy: z.string().optional().default("y").describe("Copy message"),
messages_undo: z.string().optional().default("u").describe("Undo message"),
messages_redo: z.string().optional().default("r").describe("Redo message"),
@@ -425,18 +456,97 @@ export namespace Config {
.optional()
.default("h")
.describe("Toggle code block concealment in messages"),
+ tool_details: z.string().optional().default("none").describe("Toggle tool details visibility"),
model_list: z.string().optional().default("m").describe("List available models"),
model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"),
model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"),
+ model_cycle_favorite: z.string().optional().default("none").describe("Next favorite model"),
+ model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"),
command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
agent_list: z.string().optional().default("a").describe("List agents"),
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
- input_forward_delete: z.string().optional().default("ctrl+d").describe("Forward delete"),
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
input_submit: z.string().optional().default("return").describe("Submit input"),
- input_newline: z.string().optional().default("shift+return,ctrl+j").describe("Insert newline in input"),
+ input_newline: z
+ .string()
+ .optional()
+ .default("shift+return,ctrl+return,alt+return,ctrl+j")
+ .describe("Insert newline in input"),
+ input_move_left: z.string().optional().default("left,ctrl+b").describe("Move cursor left in input"),
+ input_move_right: z.string().optional().default("right,ctrl+f").describe("Move cursor right in input"),
+ input_move_up: z.string().optional().default("up").describe("Move cursor up in input"),
+ input_move_down: z.string().optional().default("down").describe("Move cursor down in input"),
+ input_select_left: z.string().optional().default("shift+left").describe("Select left in input"),
+ input_select_right: z.string().optional().default("shift+right").describe("Select right in input"),
+ input_select_up: z.string().optional().default("shift+up").describe("Select up in input"),
+ input_select_down: z.string().optional().default("shift+down").describe("Select down in input"),
+ input_line_home: z.string().optional().default("ctrl+a").describe("Move to start of line in input"),
+ input_line_end: z.string().optional().default("ctrl+e").describe("Move to end of line in input"),
+ input_select_line_home: z
+ .string()
+ .optional()
+ .default("ctrl+shift+a")
+ .describe("Select to start of line in input"),
+ input_select_line_end: z.string().optional().default("ctrl+shift+e").describe("Select to end of line in input"),
+ input_visual_line_home: z.string().optional().default("alt+a").describe("Move to start of visual line in input"),
+ input_visual_line_end: z.string().optional().default("alt+e").describe("Move to end of visual line in input"),
+ input_select_visual_line_home: z
+ .string()
+ .optional()
+ .default("alt+shift+a")
+ .describe("Select to start of visual line in input"),
+ input_select_visual_line_end: z
+ .string()
+ .optional()
+ .default("alt+shift+e")
+ .describe("Select to end of visual line in input"),
+ input_buffer_home: z.string().optional().default("home").describe("Move to start of buffer in input"),
+ input_buffer_end: z.string().optional().default("end").describe("Move to end of buffer in input"),
+ input_select_buffer_home: z
+ .string()
+ .optional()
+ .default("shift+home")
+ .describe("Select to start of buffer in input"),
+ input_select_buffer_end: z.string().optional().default("shift+end").describe("Select to end of buffer in input"),
+ input_delete_line: z.string().optional().default("ctrl+shift+d").describe("Delete line in input"),
+ input_delete_to_line_end: z.string().optional().default("ctrl+k").describe("Delete to end of line in input"),
+ input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"),
+ input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"),
+ input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"),
+ input_undo: z.string().optional().default("ctrl+-,super+z").describe("Undo in input"),
+ input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"),
+ input_word_forward: z
+ .string()
+ .optional()
+ .default("alt+f,alt+right,ctrl+right")
+ .describe("Move word forward in input"),
+ input_word_backward: z
+ .string()
+ .optional()
+ .default("alt+b,alt+left,ctrl+left")
+ .describe("Move word backward in input"),
+ input_select_word_forward: z
+ .string()
+ .optional()
+ .default("alt+shift+f,alt+shift+right")
+ .describe("Select word forward in input"),
+ input_select_word_backward: z
+ .string()
+ .optional()
+ .default("alt+shift+b,alt+shift+left")
+ .describe("Select word backward in input"),
+ input_delete_word_forward: z
+ .string()
+ .optional()
+ .default("alt+d,alt+delete,ctrl+delete")
+ .describe("Delete word forward in input"),
+ input_delete_word_backward: z
+ .string()
+ .optional()
+ .default("ctrl+w,ctrl+backspace,alt+backspace")
+ .describe("Delete word backward in input"),
history_previous: z.string().optional().default("up").describe("Previous history item"),
history_next: z.string().optional().default("down").describe("Next history item"),
session_child_cycle: z.string().optional().default("right").describe("Next child session"),
@@ -467,6 +577,42 @@ export namespace Config {
})
export type Layout = z.infer
+ export const Provider = ModelsDev.Provider.partial()
+ .extend({
+ whitelist: z.array(z.string()).optional(),
+ blacklist: z.array(z.string()).optional(),
+ models: z.record(z.string(), ModelsDev.Model.partial()).optional(),
+ options: z
+ .object({
+ apiKey: z.string().optional(),
+ baseURL: z.string().optional(),
+ enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
+ setCacheKey: z.boolean().optional().describe("Enable promptCacheKey for this provider (default false)"),
+ timeout: z
+ .union([
+ z
+ .number()
+ .int()
+ .positive()
+ .describe(
+ "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
+ ),
+ z.literal(false).describe("Disable timeout for this provider entirely."),
+ ])
+ .optional()
+ .describe(
+ "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
+ ),
+ })
+ .catchall(z.any())
+ .optional(),
+ })
+ .strict()
+ .meta({
+ ref: "ProviderConfig",
+ })
+ export type Provider = z.infer
+
export const Info = z
.object({
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
@@ -524,52 +670,22 @@ export namespace Config {
.describe("@deprecated Use `agent` field instead."),
agent: z
.object({
+ // primary
plan: Agent.optional(),
build: Agent.optional(),
+ // subagent
general: Agent.optional(),
explore: Agent.optional(),
+ // specialized
+ title: Agent.optional(),
+ summary: Agent.optional(),
+ compaction: Agent.optional(),
})
.catchall(Agent)
.optional()
.describe("Agent configuration, see https://opencode.ai/docs/agent"),
provider: z
- .record(
- z.string(),
- ModelsDev.Provider.partial()
- .extend({
- whitelist: z.array(z.string()).optional(),
- blacklist: z.array(z.string()).optional(),
- models: z.record(z.string(), ModelsDev.Model.partial()).optional(),
- options: z
- .object({
- apiKey: z.string().optional(),
- baseURL: z.string().optional(),
- enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
- setCacheKey: z
- .boolean()
- .optional()
- .describe("Enable promptCacheKey for this provider (default false)"),
- timeout: z
- .union([
- z
- .number()
- .int()
- .positive()
- .describe(
- "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
- ),
- z.literal(false).describe("Disable timeout for this provider entirely."),
- ])
- .optional()
- .describe(
- "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
- ),
- })
- .catchall(z.any())
- .optional(),
- })
- .strict(),
- )
+ .record(z.string(), Provider)
.optional()
.describe("Custom provider configurations and model overrides"),
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
@@ -667,6 +783,15 @@ export namespace Config {
chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"),
disable_paste_summary: z.boolean().optional(),
batch_tool: z.boolean().optional().describe("Enable the batch tool"),
+ openTelemetry: z
+ .boolean()
+ .optional()
+ .describe("Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)"),
+ primary_tools: z
+ .array(z.string())
+ .optional()
+ .describe("Tools that should only be available to primary agents."),
+ continue_loop_on_deny: z.boolean().optional().describe("Continue the agent loop when a tool call is denied"),
})
.optional(),
})
diff --git a/packages/opencode/src/env/index.ts b/packages/opencode/src/env/index.ts
new file mode 100644
index 000000000..8c40c08ed
--- /dev/null
+++ b/packages/opencode/src/env/index.ts
@@ -0,0 +1,26 @@
+import { Instance } from "../project/instance"
+
+export namespace Env {
+ const state = Instance.state(() => {
+ return process.env as Record
+ })
+
+ export function get(key: string) {
+ const env = state()
+ return env[key]
+ }
+
+ export function all() {
+ return state()
+ }
+
+ export function set(key: string, value: string) {
+ const env = state()
+ env[key] = value
+ }
+
+ export function remove(key: string) {
+ const env = state()
+ delete env[key]
+ }
+}
diff --git a/packages/opencode/src/file/fzf.ts b/packages/opencode/src/file/fzf.ts
deleted file mode 100644
index 50db8901d..000000000
--- a/packages/opencode/src/file/fzf.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import path from "path"
-import { Global } from "../global"
-import fs from "fs/promises"
-import z from "zod"
-import { NamedError } from "@opencode-ai/util/error"
-import { lazy } from "../util/lazy"
-import { Log } from "../util/log"
-import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
-
-export namespace Fzf {
- const log = Log.create({ service: "fzf" })
-
- const VERSION = "0.62.0"
- const PLATFORM = {
- darwin: { extension: "tar.gz" },
- linux: { extension: "tar.gz" },
- win32: { extension: "zip" },
- } as const
-
- export const ExtractionFailedError = NamedError.create(
- "FzfExtractionFailedError",
- z.object({
- filepath: z.string(),
- stderr: z.string(),
- }),
- )
-
- export const UnsupportedPlatformError = NamedError.create(
- "FzfUnsupportedPlatformError",
- z.object({
- platform: z.string(),
- }),
- )
-
- export const DownloadFailedError = NamedError.create(
- "FzfDownloadFailedError",
- z.object({
- url: z.string(),
- status: z.number(),
- }),
- )
-
- const state = lazy(async () => {
- let filepath = Bun.which("fzf")
- if (filepath) {
- log.info("found", { filepath })
- return { filepath }
- }
- filepath = path.join(Global.Path.bin, "fzf" + (process.platform === "win32" ? ".exe" : ""))
-
- const file = Bun.file(filepath)
- if (!(await file.exists())) {
- const archMap = { x64: "amd64", arm64: "arm64" } as const
- const arch = archMap[process.arch as keyof typeof archMap] ?? "amd64"
-
- const config = PLATFORM[process.platform as keyof typeof PLATFORM]
- if (!config) throw new UnsupportedPlatformError({ platform: process.platform })
-
- const version = VERSION
- const platformName = process.platform === "win32" ? "windows" : process.platform
- const filename = `fzf-${version}-${platformName}_${arch}.${config.extension}`
- const url = `https://github.com/junegunn/fzf/releases/download/v${version}/${filename}`
-
- const response = await fetch(url)
- if (!response.ok) throw new DownloadFailedError({ url, status: response.status })
-
- const buffer = await response.arrayBuffer()
- const archivePath = path.join(Global.Path.bin, filename)
- await Bun.write(archivePath, buffer)
- if (config.extension === "tar.gz") {
- const proc = Bun.spawn(["tar", "-xzf", archivePath, "fzf"], {
- cwd: Global.Path.bin,
- stderr: "pipe",
- stdout: "pipe",
- })
- await proc.exited
- if (proc.exitCode !== 0)
- throw new ExtractionFailedError({
- filepath,
- stderr: await Bun.readableStreamToText(proc.stderr),
- })
- }
- if (config.extension === "zip") {
- const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])))
- const entries = await zipFileReader.getEntries()
- let fzfEntry: any
- for (const entry of entries) {
- if (entry.filename === "fzf.exe") {
- fzfEntry = entry
- break
- }
- }
-
- if (!fzfEntry) {
- throw new ExtractionFailedError({
- filepath: archivePath,
- stderr: "fzf.exe not found in zip archive",
- })
- }
-
- const fzfBlob = await fzfEntry.getData(new BlobWriter())
- if (!fzfBlob) {
- throw new ExtractionFailedError({
- filepath: archivePath,
- stderr: "Failed to extract fzf.exe from zip archive",
- })
- }
- await Bun.write(filepath, await fzfBlob.arrayBuffer())
- await zipFileReader.close()
- }
- await fs.unlink(archivePath)
- if (process.platform !== "win32") await fs.chmod(filepath, 0o755)
- }
-
- return {
- filepath,
- }
- })
-
- export async function filepath() {
- const { filepath } = await state()
- return filepath
- }
-}
diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts
index aae7061c1..61630ff1c 100644
--- a/packages/opencode/src/file/index.ts
+++ b/packages/opencode/src/file/index.ts
@@ -1,5 +1,6 @@
+import { BusEvent } from "@/bus/bus-event"
+import { Bus } from "@/bus"
import z from "zod"
-import { Bus } from "../bus"
import { $ } from "bun"
import type { BunFile } from "bun"
import { formatPatch, structuredPatch } from "diff"
@@ -111,7 +112,7 @@ export namespace File {
}
export const Event = {
- Edited: Bus.event(
+ Edited: BusEvent.define(
"file.edited",
z.object({
file: z.string(),
@@ -276,11 +277,16 @@ export namespace File {
const project = Instance.project
let ignored = (_: string) => false
if (project.vcs === "git") {
+ const ig = ignore()
const gitignore = Bun.file(path.join(Instance.worktree, ".gitignore"))
if (await gitignore.exists()) {
- const ig = ignore().add(await gitignore.text())
- ignored = ig.ignores.bind(ig)
+ ig.add(await gitignore.text())
}
+ const ignoreFile = Bun.file(path.join(Instance.worktree, ".ignore"))
+ if (await ignoreFile.exists()) {
+ ig.add(await ignoreFile.text())
+ }
+ ignored = ig.ignores.bind(ig)
}
const resolved = dir ? path.join(Instance.directory, dir) : Instance.directory
const nodes: Node[] = []
diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts
index 5cba5e820..770427abe 100644
--- a/packages/opencode/src/file/time.ts
+++ b/packages/opencode/src/file/time.ts
@@ -3,14 +3,20 @@ import { Log } from "../util/log"
export namespace FileTime {
const log = Log.create({ service: "file.time" })
+ // Per-session read times plus per-file write locks.
+ // All tools that overwrite existing files should run their
+ // assert/read/write/update sequence inside withLock(filepath, ...)
+ // so concurrent writes to the same file are serialized.
export const state = Instance.state(() => {
const read: {
[sessionID: string]: {
[path: string]: Date | undefined
}
} = {}
+ const locks = new Map>()
return {
read,
+ locks,
}
})
@@ -25,6 +31,26 @@ export namespace FileTime {
return state().read[sessionID]?.[file]
}
+ export async function withLock(filepath: string, fn: () => Promise): Promise {
+ const current = state()
+ const currentLock = current.locks.get(filepath) ?? Promise.resolve()
+ let release: () => void = () => {}
+ const nextLock = new Promise((resolve) => {
+ release = resolve
+ })
+ const chained = currentLock.then(() => nextLock)
+ current.locks.set(filepath, chained)
+ await currentLock
+ try {
+ return await fn()
+ } finally {
+ release()
+ if (current.locks.get(filepath) === chained) {
+ current.locks.delete(filepath)
+ }
+ }
+ }
+
export async function assert(sessionID: string, filepath: string) {
const time = get(sessionID, filepath)
if (!time) throw new Error(`You must read the file ${filepath} before overwriting it. Use the Read tool first`)
diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts
index 4459d20e9..80e524349 100644
--- a/packages/opencode/src/file/watcher.ts
+++ b/packages/opencode/src/file/watcher.ts
@@ -1,5 +1,6 @@
+import { BusEvent } from "@/bus/bus-event"
+import { Bus } from "@/bus"
import z from "zod"
-import { Bus } from "../bus"
import { Instance } from "../project/instance"
import { Log } from "../util/log"
import { FileIgnore } from "./ignore"
@@ -8,12 +9,15 @@ import { Config } from "../config/config"
import { createWrapper } from "@parcel/watcher/wrapper"
import { lazy } from "@/util/lazy"
import type ParcelWatcher from "@parcel/watcher"
+import { $ } from "bun"
+
+declare const OPENCODE_LIBC: string | undefined
export namespace FileWatcher {
const log = Log.create({ service: "file.watcher" })
export const Event = {
- Updated: Bus.event(
+ Updated: BusEvent.define(
"file.watcher.updated",
z.object({
file: z.string(),
@@ -24,7 +28,7 @@ export namespace FileWatcher {
const watcher = lazy(() => {
const binding = require(
- `@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? "-glibc" : ""}`,
+ `@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${OPENCODE_LIBC || "glibc"}` : ""}`,
)
return createWrapper(binding) as typeof import("@parcel/watcher")
})
@@ -47,7 +51,6 @@ export namespace FileWatcher {
const subscribe: ParcelWatcher.SubscribeCallback = (err, evts) => {
if (err) return
for (const evt of evts) {
- log.info("event", evt)
if (evt.type === "create") Bus.publish(Event.Updated, { file: evt.path, event: "add" })
if (evt.type === "update") Bus.publish(Event.Updated, { file: evt.path, event: "change" })
if (evt.type === "delete") Bus.publish(Event.Updated, { file: evt.path, event: "unlink" })
@@ -64,7 +67,7 @@ export namespace FileWatcher {
}),
)
- const vcsDir = Instance.project.vcsDir
+ const vcsDir = await $`git rev-parse --git-dir`.quiet().nothrow().cwd(Instance.worktree).text()
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
subs.push(
await watcher().subscribe(vcsDir, subscribe, {
@@ -83,7 +86,6 @@ export namespace FileWatcher {
)
export function init() {
- // if (!Flag.OPENCODE_EXPERIMENTAL_WATCHER) return
state()
}
}
diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts
index 4edbd5ace..d7a24708a 100644
--- a/packages/opencode/src/flag/flag.ts
+++ b/packages/opencode/src/flag/flag.ts
@@ -1,5 +1,6 @@
export namespace Flag {
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
+ export const OPENCODE_GIT_BASH_PATH = process.env["OPENCODE_GIT_BASH_PATH"]
export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"]
export const OPENCODE_CONFIG_DIR = process.env["OPENCODE_CONFIG_DIR"]
export const OPENCODE_CONFIG_CONTENT = process.env["OPENCODE_CONFIG_CONTENT"]
@@ -10,16 +11,29 @@ export namespace Flag {
export const OPENCODE_DISABLE_LSP_DOWNLOAD = truthy("OPENCODE_DISABLE_LSP_DOWNLOAD")
export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS")
export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT")
+ export const OPENCODE_DISABLE_MODELS_FETCH = truthy("OPENCODE_DISABLE_MODELS_FETCH")
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
- export const OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH =
- process.env["OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH"]
+ export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli"
// Experimental
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
- export const OPENCODE_EXPERIMENTAL_WATCHER = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WATCHER")
+ export const OPENCODE_EXPERIMENTAL_ICON_DISCOVERY =
+ OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY")
+ export const OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT = truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT")
+ export const OPENCODE_ENABLE_EXA =
+ truthy("OPENCODE_ENABLE_EXA") || OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA")
+ export const OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH = number("OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH")
+ export const OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS = number("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS")
function truthy(key: string) {
const value = process.env[key]?.toLowerCase()
return value === "true" || value === "1"
}
+
+ function number(key: string) {
+ const value = process.env[key]
+ if (!value) return undefined
+ const parsed = Number(value)
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined
+ }
}
diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts
index 404898080..c4e7c9ee8 100644
--- a/packages/opencode/src/format/formatter.ts
+++ b/packages/opencode/src/format/formatter.ts
@@ -255,3 +255,41 @@ export const dart: Info = {
return Bun.which("dart") !== null
},
}
+
+export const ocamlformat: Info = {
+ name: "ocamlformat",
+ command: ["ocamlformat", "-i", "$FILE"],
+ extensions: [".ml", ".mli"],
+ async enabled() {
+ if (!Bun.which("ocamlformat")) return false
+ const items = await Filesystem.findUp(".ocamlformat", Instance.directory, Instance.worktree)
+ return items.length > 0
+ },
+}
+
+export const terraform: Info = {
+ name: "terraform",
+ command: ["terraform", "fmt", "$FILE"],
+ extensions: [".tf", ".tfvars"],
+ async enabled() {
+ return Bun.which("terraform") !== null
+ },
+}
+
+export const latexindent: Info = {
+ name: "latexindent",
+ command: ["latexindent", "-w", "-s", "$FILE"],
+ extensions: [".tex"],
+ async enabled() {
+ return Bun.which("latexindent") !== null
+ },
+}
+
+export const gleam: Info = {
+ name: "gleam",
+ command: ["gleam", "format", "$FILE"],
+ extensions: [".gleam"],
+ async enabled() {
+ return Bun.which("gleam") !== null
+ },
+}
diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts
index e98b1628d..2504a47dc 100644
--- a/packages/opencode/src/global/index.ts
+++ b/packages/opencode/src/global/index.ts
@@ -30,7 +30,7 @@ await Promise.all([
fs.mkdir(Global.Path.bin, { recursive: true }),
])
-const CACHE_VERSION = "11"
+const CACHE_VERSION = "14"
const version = await Bun.file(path.join(Global.Path.cache, "version"))
.text()
diff --git a/packages/opencode/src/id/id.ts b/packages/opencode/src/id/id.ts
index 99eb6c9ff..ad6e22e1b 100644
--- a/packages/opencode/src/id/id.ts
+++ b/packages/opencode/src/id/id.ts
@@ -8,6 +8,7 @@ export namespace Identifier {
permission: "per",
user: "usr",
part: "prt",
+ pty: "pty",
} as const
export function schema(prefix: keyof typeof prefixes) {
diff --git a/packages/opencode/src/ide/index.ts b/packages/opencode/src/ide/index.ts
index 268f115fc..0837b2aa5 100644
--- a/packages/opencode/src/ide/index.ts
+++ b/packages/opencode/src/ide/index.ts
@@ -1,8 +1,9 @@
+import { BusEvent } from "@/bus/bus-event"
+import { Bus } from "@/bus"
import { spawn } from "bun"
import z from "zod"
import { NamedError } from "@opencode-ai/util/error"
import { Log } from "../util/log"
-import { Bus } from "../bus"
const SUPPORTED_IDES = [
{ name: "Windsurf" as const, cmd: "windsurf" },
@@ -16,7 +17,7 @@ export namespace Ide {
const log = Log.create({ service: "ide" })
export const Event = {
- Installed: Bus.event(
+ Installed: BusEvent.define(
"ide.installed",
z.object({
ide: z.string(),
diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts
index 38b6b5a3f..638ee7347 100644
--- a/packages/opencode/src/index.ts
+++ b/packages/opencode/src/index.ts
@@ -6,6 +6,7 @@ import { Log } from "./util/log"
import { AuthCommand } from "./cli/cmd/auth"
import { AgentCommand } from "./cli/cmd/agent"
import { UpgradeCommand } from "./cli/cmd/upgrade"
+import { UninstallCommand } from "./cli/cmd/uninstall"
import { ModelsCommand } from "./cli/cmd/models"
import { UI } from "./cli/ui"
import { Installation } from "./installation"
@@ -25,6 +26,7 @@ import { AcpCommand } from "./cli/cmd/acp"
import { EOL } from "os"
import { WebCommand } from "./cli/cmd/web"
import { PrCommand } from "./cli/cmd/pr"
+import { SessionCommand } from "./cli/cmd/session"
process.on("unhandledRejection", (e) => {
Log.Default.error("rejection", {
@@ -39,7 +41,9 @@ process.on("uncaughtException", (e) => {
})
const cli = yargs(hideBin(process.argv))
+ .parserConfiguration({ "populate--": true })
.scriptName("opencode")
+ .wrap(100)
.help("help", "show help")
.alias("help", "h")
.version("version", "show version number", Installation.VERSION)
@@ -84,6 +88,7 @@ const cli = yargs(hideBin(process.argv))
.command(AuthCommand)
.command(AgentCommand)
.command(UpgradeCommand)
+ .command(UninstallCommand)
.command(ServeCommand)
.command(WebCommand)
.command(ModelsCommand)
@@ -92,6 +97,7 @@ const cli = yargs(hideBin(process.argv))
.command(ImportCommand)
.command(GithubCommand)
.command(PrCommand)
+ .command(SessionCommand)
.fail((msg) => {
if (
msg.startsWith("Unknown argument") ||
diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts
index 7ac2980c4..0359c16fe 100644
--- a/packages/opencode/src/installation/index.ts
+++ b/packages/opencode/src/installation/index.ts
@@ -1,9 +1,12 @@
+import { BusEvent } from "@/bus/bus-event"
+import { Bus } from "@/bus"
import path from "path"
import { $ } from "bun"
import z from "zod"
import { NamedError } from "@opencode-ai/util/error"
-import { Bus } from "../bus"
import { Log } from "../util/log"
+import { iife } from "@/util/iife"
+import { Flag } from "../flag/flag"
declare global {
const OPENCODE_VERSION: string
@@ -16,13 +19,13 @@ export namespace Installation {
export type Method = Awaited>
export const Event = {
- Updated: Bus.event(
+ Updated: BusEvent.define(
"installation.updated",
z.object({
version: z.string(),
}),
),
- UpdateAvailable: Bus.event(
+ UpdateAvailable: BusEvent.define(
"installation.update-available",
z.object({
version: z.string(),
@@ -160,12 +163,31 @@ export namespace Installation {
export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local"
export const CHANNEL = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local"
- export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}`
+ export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}/${Flag.OPENCODE_CLIENT}`
- export async function latest() {
+ export async function latest(installMethod?: Method) {
+ const detectedMethod = installMethod || (await method())
+ if (detectedMethod === "brew") {
+ const formula = await getBrewFormula()
+ if (formula === "opencode") {
+ return fetch("https://formulae.brew.sh/api/formula/opencode.json")
+ .then((res) => {
+ if (!res.ok) throw new Error(res.statusText)
+ return res.json()
+ })
+ .then((data: any) => data.versions.stable)
+ }
+ }
+
+ const registry = await iife(async () => {
+ const r = (await $`npm config get registry`.quiet().nothrow().text()).trim()
+ const reg = r || "https://registry.npmjs.org"
+ return reg.endsWith("/") ? reg.slice(0, -1) : reg
+ })
const [major] = VERSION.split(".").map((x) => Number(x))
- const channel = CHANNEL === "latest" ? `latest-${major}` : CHANNEL
- return fetch(`https://registry.npmjs.org/opencode-ai/${channel}`)
+ // const channel = CHANNEL === "latest" ? `latest-${major}` : CHANNEL
+ const channel = CHANNEL
+ return fetch(`${registry}/opencode-ai/${channel}`)
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts
index f8b7db7ae..b66bb9933 100644
--- a/packages/opencode/src/lsp/client.ts
+++ b/packages/opencode/src/lsp/client.ts
@@ -1,14 +1,19 @@
+import { BusEvent } from "@/bus/bus-event"
+import { Bus } from "@/bus"
import path from "path"
+import { pathToFileURL, fileURLToPath } from "url"
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
import { Log } from "../util/log"
import { LANGUAGE_EXTENSIONS } from "./language"
-import { Bus } from "../bus"
import z from "zod"
import type { LSPServer } from "./server"
import { NamedError } from "@opencode-ai/util/error"
import { withTimeout } from "../util/timeout"
import { Instance } from "../project/instance"
+import { Filesystem } from "../util/filesystem"
+
+const DIAGNOSTICS_DEBOUNCE_MS = 150
export namespace LSPClient {
const log = Log.create({ service: "lsp.client" })
@@ -25,7 +30,7 @@ export namespace LSPClient {
)
export const Event = {
- Diagnostics: Bus.event(
+ Diagnostics: BusEvent.define(
"lsp.client.diagnostics",
z.object({
serverID: z.string(),
@@ -45,14 +50,15 @@ export namespace LSPClient {
const diagnostics = new Map()
connection.onNotification("textDocument/publishDiagnostics", (params) => {
- const path = new URL(params.uri).pathname
+ const filePath = Filesystem.normalizePath(fileURLToPath(params.uri))
l.info("textDocument/publishDiagnostics", {
- path,
+ path: filePath,
+ count: params.diagnostics.length,
})
- const exists = diagnostics.has(path)
- diagnostics.set(path, params.diagnostics)
+ const exists = diagnostics.has(filePath)
+ diagnostics.set(filePath, params.diagnostics)
if (!exists && input.serverID === "typescript") return
- Bus.publish(Event.Diagnostics, { path, serverID: input.serverID })
+ Bus.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID })
})
connection.onRequest("window/workDoneProgress/create", (params) => {
l.info("window/workDoneProgress/create", params)
@@ -67,7 +73,7 @@ export namespace LSPClient {
connection.onRequest("workspace/workspaceFolders", async () => [
{
name: "workspace",
- uri: "file://" + input.root,
+ uri: pathToFileURL(input.root).href,
},
])
connection.listen()
@@ -75,12 +81,12 @@ export namespace LSPClient {
l.info("sending initialize")
await withTimeout(
connection.sendRequest("initialize", {
- rootUri: "file://" + input.root,
+ rootUri: pathToFileURL(input.root).href,
processId: input.server.process.pid,
workspaceFolders: [
{
name: "workspace",
- uri: "file://" + input.root,
+ uri: pathToFileURL(input.root).href,
},
],
initializationOptions: {
@@ -104,7 +110,7 @@ export namespace LSPClient {
},
},
}),
- 5_000,
+ 45_000,
).catch((err) => {
l.error("initialize error", { error: err })
throw new InitializeError(
@@ -153,7 +159,7 @@ export namespace LSPClient {
})
await connection.sendNotification("textDocument/didChange", {
textDocument: {
- uri: `file://` + input.path,
+ uri: pathToFileURL(input.path).href,
version: next,
},
contentChanges: [{ text }],
@@ -165,7 +171,7 @@ export namespace LSPClient {
diagnostics.delete(input.path)
await connection.sendNotification("textDocument/didOpen", {
textDocument: {
- uri: `file://` + input.path,
+ uri: pathToFileURL(input.path).href,
languageId,
version: 0,
text,
@@ -179,16 +185,23 @@ export namespace LSPClient {
return diagnostics
},
async waitForDiagnostics(input: { path: string }) {
- input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
- log.info("waiting for diagnostics", input)
+ const normalizedPath = Filesystem.normalizePath(
+ path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path),
+ )
+ log.info("waiting for diagnostics", { path: normalizedPath })
let unsub: () => void
+ let debounceTimer: ReturnType | undefined
return await withTimeout(
new Promise((resolve) => {
unsub = Bus.subscribe(Event.Diagnostics, (event) => {
- if (event.properties.path === input.path && event.properties.serverID === result.serverID) {
- log.info("got diagnostics", input)
- unsub?.()
- resolve()
+ if (event.properties.path === normalizedPath && event.properties.serverID === result.serverID) {
+ // Debounce to allow LSP to send follow-up diagnostics (e.g., semantic after syntax)
+ if (debounceTimer) clearTimeout(debounceTimer)
+ debounceTimer = setTimeout(() => {
+ log.info("got diagnostics", { path: normalizedPath })
+ unsub?.()
+ resolve()
+ }, DIAGNOSTICS_DEBOUNCE_MS)
}
})
}),
@@ -196,6 +209,7 @@ export namespace LSPClient {
)
.catch(() => {})
.finally(() => {
+ if (debounceTimer) clearTimeout(debounceTimer)
unsub?.()
})
},
diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts
index 6c082d0d7..764c91fcc 100644
--- a/packages/opencode/src/lsp/index.ts
+++ b/packages/opencode/src/lsp/index.ts
@@ -1,18 +1,20 @@
+import { BusEvent } from "@/bus/bus-event"
+import { Bus } from "@/bus"
import { Log } from "../util/log"
import { LSPClient } from "./client"
import path from "path"
+import { pathToFileURL } from "url"
import { LSPServer } from "./server"
import z from "zod"
import { Config } from "../config/config"
import { spawn } from "child_process"
import { Instance } from "../project/instance"
-import { Bus } from "../bus"
export namespace LSP {
const log = Log.create({ service: "lsp" })
export const Event = {
- Updated: Bus.event("lsp.updated", z.object({})),
+ Updated: BusEvent.define("lsp.updated", z.object({})),
}
export const Range = z
@@ -269,7 +271,7 @@ export namespace LSP {
return run((client) => {
return client.connection.sendRequest("textDocument/hover", {
textDocument: {
- uri: `file://${input.file}`,
+ uri: pathToFileURL(input.file).href,
},
position: {
line: input.line,
diff --git a/packages/opencode/src/lsp/language.ts b/packages/opencode/src/lsp/language.ts
index 7980f05e8..5261873f6 100644
--- a/packages/opencode/src/lsp/language.ts
+++ b/packages/opencode/src/lsp/language.ts
@@ -34,6 +34,7 @@ export const LANGUAGE_EXTENSIONS: Record = {
".gitrebase": "git-rebase",
".go": "go",
".groovy": "groovy",
+ ".gleam": "gleam",
".hbs": "handlebars",
".handlebars": "handlebars",
".hs": "haskell",
@@ -103,4 +104,9 @@ export const LANGUAGE_EXTENSIONS: Record = {
".zig": "zig",
".zon": "zig",
".astro": "astro",
+ ".ml": "ocaml",
+ ".mli": "ocaml",
+ ".tf": "terraform",
+ ".tfvars": "terraform-vars",
+ ".hcl": "hcl",
} as const
diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts
index ce2fbfa69..939a31a2d 100644
--- a/packages/opencode/src/lsp/server.ts
+++ b/packages/opencode/src/lsp/server.ts
@@ -9,6 +9,7 @@ import fs from "fs/promises"
import { Filesystem } from "../util/filesystem"
import { Instance } from "../project/instance"
import { Flag } from "../flag/flag"
+import { Archive } from "../util/archive"
export namespace LSPServer {
const log = Log.create({ service: "lsp.server" })
@@ -176,7 +177,13 @@ export namespace LSPServer {
const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip")
await Bun.file(zipPath).write(response)
- await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
+ const ok = await Archive.extractZip(zipPath, Global.Path.bin)
+ .then(() => true)
+ .catch((error) => {
+ log.error("Failed to extract vscode-eslint archive", { error })
+ return false
+ })
+ if (!ok) return
await fs.rm(zipPath, { force: true })
const extractedPath = path.join(Global.Path.bin, "vscode-eslint-main")
@@ -209,6 +216,68 @@ export namespace LSPServer {
},
}
+ export const Biome: Info = {
+ id: "biome",
+ root: NearestRoot([
+ "biome.json",
+ "biome.jsonc",
+ "package-lock.json",
+ "bun.lockb",
+ "bun.lock",
+ "pnpm-lock.yaml",
+ "yarn.lock",
+ ]),
+ extensions: [
+ ".ts",
+ ".tsx",
+ ".js",
+ ".jsx",
+ ".mjs",
+ ".cjs",
+ ".mts",
+ ".cts",
+ ".json",
+ ".jsonc",
+ ".vue",
+ ".astro",
+ ".svelte",
+ ".css",
+ ".graphql",
+ ".gql",
+ ".html",
+ ],
+ async spawn(root) {
+ const localBin = path.join(root, "node_modules", ".bin", "biome")
+ let bin: string | undefined
+ if (await Bun.file(localBin).exists()) bin = localBin
+ if (!bin) {
+ const found = Bun.which("biome")
+ if (found) bin = found
+ }
+
+ let args = ["lsp-proxy", "--stdio"]
+
+ if (!bin) {
+ const resolved = await Bun.resolve("biome", root).catch(() => undefined)
+ if (!resolved) return
+ bin = BunProc.which()
+ args = ["x", "biome", "lsp-proxy", "--stdio"]
+ }
+
+ const proc = spawn(bin, args, {
+ cwd: root,
+ env: {
+ ...process.env,
+ BUN_BE_BUN: "1",
+ },
+ })
+
+ return {
+ process: proc,
+ }
+ },
+ }
+
export const Gopls: Info = {
id: "gopls",
root: async (file) => {
@@ -219,7 +288,7 @@ export namespace LSPServer {
extensions: [".go"],
async spawn(root) {
let bin = Bun.which("gopls", {
- PATH: process.env["PATH"] + ":" + Global.Path.bin,
+ PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
})
if (!bin) {
if (!Bun.which("go")) return
@@ -257,7 +326,7 @@ export namespace LSPServer {
extensions: [".rb", ".rake", ".gemspec", ".ru"],
async spawn(root) {
let bin = Bun.which("rubocop", {
- PATH: process.env["PATH"] + ":" + Global.Path.bin,
+ PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
})
if (!bin) {
const ruby = Bun.which("ruby")
@@ -358,7 +427,7 @@ export namespace LSPServer {
Global.Path.bin,
"elixir-ls-master",
"release",
- process.platform === "win32" ? "language_server.bar" : "language_server.sh",
+ process.platform === "win32" ? "language_server.bat" : "language_server.sh",
)
if (!(await Bun.file(binary).exists())) {
@@ -376,7 +445,13 @@ export namespace LSPServer {
const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
await Bun.file(zipPath).write(response)
- await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
+ const ok = await Archive.extractZip(zipPath, Global.Path.bin)
+ .then(() => true)
+ .catch((error) => {
+ log.error("Failed to extract elixir-ls archive", { error })
+ return false
+ })
+ if (!ok) return
await fs.rm(zipPath, {
force: true,
@@ -408,7 +483,7 @@ export namespace LSPServer {
root: NearestRoot(["build.zig"]),
async spawn(root) {
let bin = Bun.which("zls", {
- PATH: process.env["PATH"] + ":" + Global.Path.bin,
+ PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
})
if (!bin) {
@@ -479,7 +554,13 @@ export namespace LSPServer {
await Bun.file(tempPath).write(downloadResponse)
if (ext === "zip") {
- await $`unzip -o -q ${tempPath}`.quiet().cwd(Global.Path.bin).nothrow()
+ const ok = await Archive.extractZip(tempPath, Global.Path.bin)
+ .then(() => true)
+ .catch((error) => {
+ log.error("Failed to extract zls archive", { error })
+ return false
+ })
+ if (!ok) return
} else {
await $`tar -xf ${tempPath}`.cwd(Global.Path.bin).nothrow()
}
@@ -514,7 +595,7 @@ export namespace LSPServer {
extensions: [".cs"],
async spawn(root) {
let bin = Bun.which("csharp-ls", {
- PATH: process.env["PATH"] + ":" + Global.Path.bin,
+ PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
})
if (!bin) {
if (!Bun.which("dotnet")) {
@@ -548,6 +629,46 @@ export namespace LSPServer {
},
}
+ export const FSharp: Info = {
+ id: "fsharp",
+ root: NearestRoot([".sln", ".fsproj", "global.json"]),
+ extensions: [".fs", ".fsi", ".fsx", ".fsscript"],
+ async spawn(root) {
+ let bin = Bun.which("fsautocomplete", {
+ PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
+ })
+ if (!bin) {
+ if (!Bun.which("dotnet")) {
+ log.error(".NET SDK is required to install fsautocomplete")
+ return
+ }
+
+ if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+ log.info("installing fsautocomplete via dotnet tool")
+ const proc = Bun.spawn({
+ cmd: ["dotnet", "tool", "install", "fsautocomplete", "--tool-path", Global.Path.bin],
+ stdout: "pipe",
+ stderr: "pipe",
+ stdin: "pipe",
+ })
+ const exit = await proc.exited
+ if (exit !== 0) {
+ log.error("Failed to install fsautocomplete")
+ return
+ }
+
+ bin = path.join(Global.Path.bin, "fsautocomplete" + (process.platform === "win32" ? ".exe" : ""))
+ log.info(`installed fsautocomplete`, { bin })
+ }
+
+ return {
+ process: spawn(bin, {
+ cwd: root,
+ }),
+ }
+ },
+ }
+
export const SourceKit: Info = {
id: "sourcekit-lsp",
extensions: [".swift", ".objc", "objcpp"],
@@ -738,7 +859,13 @@ export namespace LSPServer {
}
if (zip) {
- await $`unzip -o -q ${archive}`.quiet().cwd(Global.Path.bin).nothrow()
+ const ok = await Archive.extractZip(archive, Global.Path.bin)
+ .then(() => true)
+ .catch((error) => {
+ log.error("Failed to extract clangd archive", { error })
+ return false
+ })
+ if (!ok) return
}
if (tar) {
await $`tar -xf ${archive}`.cwd(Global.Path.bin).nothrow()
@@ -1008,7 +1135,7 @@ export namespace LSPServer {
extensions: [".lua"],
async spawn(root) {
let bin = Bun.which("lua-language-server", {
- PATH: process.env["PATH"] + ":" + Global.Path.bin,
+ PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
})
if (!bin) {
@@ -1086,14 +1213,21 @@ export namespace LSPServer {
await fs.mkdir(installDir, { recursive: true })
if (ext === "zip") {
- const ok = await $`unzip -o -q ${tempPath} -d ${installDir}`.quiet().catch((error) => {
- log.error("Failed to extract lua-language-server archive", { error })
- })
+ const ok = await Archive.extractZip(tempPath, installDir)
+ .then(() => true)
+ .catch((error) => {
+ log.error("Failed to extract lua-language-server archive", { error })
+ return false
+ })
if (!ok) return
} else {
- const ok = await $`tar -xzf ${tempPath} -C ${installDir}`.quiet().catch((error) => {
- log.error("Failed to extract lua-language-server archive", { error })
- })
+ const ok = await $`tar -xzf ${tempPath} -C ${installDir}`
+ .quiet()
+ .then(() => true)
+ .catch((error) => {
+ log.error("Failed to extract lua-language-server archive", { error })
+ return false
+ })
if (!ok) return
}
@@ -1184,4 +1318,297 @@ export namespace LSPServer {
}
},
}
+
+ export const Ocaml: Info = {
+ id: "ocaml-lsp",
+ extensions: [".ml", ".mli"],
+ root: NearestRoot(["dune-project", "dune-workspace", ".merlin", "opam"]),
+ async spawn(root) {
+ const bin = Bun.which("ocamllsp")
+ if (!bin) {
+ log.info("ocamllsp not found, please install ocaml-lsp-server")
+ return
+ }
+ return {
+ process: spawn(bin, {
+ cwd: root,
+ }),
+ }
+ },
+ }
+ export const BashLS: Info = {
+ id: "bash",
+ extensions: [".sh", ".bash", ".zsh", ".ksh"],
+ root: async () => Instance.directory,
+ async spawn(root) {
+ let binary = Bun.which("bash-language-server")
+ const args: string[] = []
+ if (!binary) {
+ const js = path.join(Global.Path.bin, "node_modules", "bash-language-server", "out", "cli.js")
+ if (!(await Bun.file(js).exists())) {
+ if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+ await Bun.spawn([BunProc.which(), "install", "bash-language-server"], {
+ cwd: Global.Path.bin,
+ env: {
+ ...process.env,
+ BUN_BE_BUN: "1",
+ },
+ stdout: "pipe",
+ stderr: "pipe",
+ stdin: "pipe",
+ }).exited
+ }
+ binary = BunProc.which()
+ args.push("run", js)
+ }
+ args.push("start")
+ const proc = spawn(binary, args, {
+ cwd: root,
+ env: {
+ ...process.env,
+ BUN_BE_BUN: "1",
+ },
+ })
+ return {
+ process: proc,
+ }
+ },
+ }
+
+ export const TerraformLS: Info = {
+ id: "terraform",
+ extensions: [".tf", ".tfvars"],
+ root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]),
+ async spawn(root) {
+ let bin = Bun.which("terraform-ls", {
+ PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
+ })
+
+ if (!bin) {
+ if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+ log.info("downloading terraform-ls from GitHub releases")
+
+ const releaseResponse = await fetch("https://api.github.com/repos/hashicorp/terraform-ls/releases/latest")
+ if (!releaseResponse.ok) {
+ log.error("Failed to fetch terraform-ls release info")
+ return
+ }
+
+ const release = (await releaseResponse.json()) as {
+ tag_name?: string
+ assets?: { name?: string; browser_download_url?: string }[]
+ }
+ const version = release.tag_name?.replace("v", "")
+ if (!version) {
+ log.error("terraform-ls release did not include a version tag")
+ return
+ }
+
+ const platform = process.platform
+ const arch = process.arch
+
+ const tfArch = arch === "arm64" ? "arm64" : "amd64"
+ const tfPlatform = platform === "win32" ? "windows" : platform
+
+ const assetName = `terraform-ls_${version}_${tfPlatform}_${tfArch}.zip`
+
+ const assets = release.assets ?? []
+ const asset = assets.find((a) => a.name === assetName)
+ if (!asset?.browser_download_url) {
+ log.error(`Could not find asset ${assetName} in terraform-ls release`)
+ return
+ }
+
+ const downloadResponse = await fetch(asset.browser_download_url)
+ if (!downloadResponse.ok) {
+ log.error("Failed to download terraform-ls")
+ return
+ }
+
+ const tempPath = path.join(Global.Path.bin, assetName)
+ await Bun.file(tempPath).write(downloadResponse)
+
+ const ok = await Archive.extractZip(tempPath, Global.Path.bin)
+ .then(() => true)
+ .catch((error) => {
+ log.error("Failed to extract terraform-ls archive", { error })
+ return false
+ })
+ if (!ok) return
+ await fs.rm(tempPath, { force: true })
+
+ bin = path.join(Global.Path.bin, "terraform-ls" + (platform === "win32" ? ".exe" : ""))
+
+ if (!(await Bun.file(bin).exists())) {
+ log.error("Failed to extract terraform-ls binary")
+ return
+ }
+
+ if (platform !== "win32") {
+ await $`chmod +x ${bin}`.nothrow()
+ }
+
+ log.info(`installed terraform-ls`, { bin })
+ }
+
+ return {
+ process: spawn(bin, ["serve"], {
+ cwd: root,
+ }),
+ initialization: {
+ experimentalFeatures: {
+ prefillRequiredFields: true,
+ validateOnSave: true,
+ },
+ },
+ }
+ },
+ }
+
+ export const TexLab: Info = {
+ id: "texlab",
+ extensions: [".tex", ".bib"],
+ root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
+ async spawn(root) {
+ let bin = Bun.which("texlab", {
+ PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
+ })
+
+ if (!bin) {
+ if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+ log.info("downloading texlab from GitHub releases")
+
+ const response = await fetch("https://api.github.com/repos/latex-lsp/texlab/releases/latest")
+ if (!response.ok) {
+ log.error("Failed to fetch texlab release info")
+ return
+ }
+
+ const release = (await response.json()) as {
+ tag_name?: string
+ assets?: { name?: string; browser_download_url?: string }[]
+ }
+ const version = release.tag_name?.replace("v", "")
+ if (!version) {
+ log.error("texlab release did not include a version tag")
+ return
+ }
+
+ const platform = process.platform
+ const arch = process.arch
+
+ const texArch = arch === "arm64" ? "aarch64" : "x86_64"
+ const texPlatform = platform === "darwin" ? "macos" : platform === "win32" ? "windows" : "linux"
+ const ext = platform === "win32" ? "zip" : "tar.gz"
+ const assetName = `texlab-${texArch}-${texPlatform}.${ext}`
+
+ const assets = release.assets ?? []
+ const asset = assets.find((a) => a.name === assetName)
+ if (!asset?.browser_download_url) {
+ log.error(`Could not find asset ${assetName} in texlab release`)
+ return
+ }
+
+ const downloadResponse = await fetch(asset.browser_download_url)
+ if (!downloadResponse.ok) {
+ log.error("Failed to download texlab")
+ return
+ }
+
+ const tempPath = path.join(Global.Path.bin, assetName)
+ await Bun.file(tempPath).write(downloadResponse)
+
+ if (ext === "zip") {
+ const ok = await Archive.extractZip(tempPath, Global.Path.bin)
+ .then(() => true)
+ .catch((error) => {
+ log.error("Failed to extract texlab archive", { error })
+ return false
+ })
+ if (!ok) return
+ }
+ if (ext === "tar.gz") {
+ await $`tar -xzf ${tempPath}`.cwd(Global.Path.bin).nothrow()
+ }
+
+ await fs.rm(tempPath, { force: true })
+
+ bin = path.join(Global.Path.bin, "texlab" + (platform === "win32" ? ".exe" : ""))
+
+ if (!(await Bun.file(bin).exists())) {
+ log.error("Failed to extract texlab binary")
+ return
+ }
+
+ if (platform !== "win32") {
+ await $`chmod +x ${bin}`.nothrow()
+ }
+
+ log.info("installed texlab", { bin })
+ }
+
+ return {
+ process: spawn(bin, {
+ cwd: root,
+ }),
+ }
+ },
+ }
+
+ export const DockerfileLS: Info = {
+ id: "dockerfile",
+ extensions: [".dockerfile", "Dockerfile"],
+ root: async () => Instance.directory,
+ async spawn(root) {
+ let binary = Bun.which("docker-langserver")
+ const args: string[] = []
+ if (!binary) {
+ const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")
+ if (!(await Bun.file(js).exists())) {
+ if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+ await Bun.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], {
+ cwd: Global.Path.bin,
+ env: {
+ ...process.env,
+ BUN_BE_BUN: "1",
+ },
+ stdout: "pipe",
+ stderr: "pipe",
+ stdin: "pipe",
+ }).exited
+ }
+ binary = BunProc.which()
+ args.push("run", js)
+ }
+ args.push("--stdio")
+ const proc = spawn(binary, args, {
+ cwd: root,
+ env: {
+ ...process.env,
+ BUN_BE_BUN: "1",
+ },
+ })
+ return {
+ process: proc,
+ }
+ },
+ }
+
+ export const Gleam: Info = {
+ id: "gleam",
+ extensions: [".gleam"],
+ root: NearestRoot(["gleam.toml"]),
+ async spawn(root) {
+ const gleam = Bun.which("gleam")
+ if (!gleam) {
+ log.info("gleam not found, please install gleam first")
+ return
+ }
+ return {
+ process: spawn(gleam, ["lsp"], {
+ cwd: root,
+ }),
+ }
+ },
+ }
}
diff --git a/packages/opencode/src/mcp/auth.ts b/packages/opencode/src/mcp/auth.ts
new file mode 100644
index 000000000..385cb3c73
--- /dev/null
+++ b/packages/opencode/src/mcp/auth.ts
@@ -0,0 +1,82 @@
+import path from "path"
+import fs from "fs/promises"
+import z from "zod"
+import { Global } from "../global"
+
+export namespace McpAuth {
+ export const Tokens = z.object({
+ accessToken: z.string(),
+ refreshToken: z.string().optional(),
+ expiresAt: z.number().optional(),
+ scope: z.string().optional(),
+ })
+ export type Tokens = z.infer
+
+ export const ClientInfo = z.object({
+ clientId: z.string(),
+ clientSecret: z.string().optional(),
+ clientIdIssuedAt: z.number().optional(),
+ clientSecretExpiresAt: z.number().optional(),
+ })
+ export type ClientInfo = z.infer
+
+ export const Entry = z.object({
+ tokens: Tokens.optional(),
+ clientInfo: ClientInfo.optional(),
+ codeVerifier: z.string().optional(),
+ })
+ export type Entry = z.infer
+
+ const filepath = path.join(Global.Path.data, "mcp-auth.json")
+
+ export async function get(mcpName: string): Promise {
+ const data = await all()
+ return data[mcpName]
+ }
+
+ export async function all(): Promise> {
+ const file = Bun.file(filepath)
+ return file.json().catch(() => ({}))
+ }
+
+ export async function set(mcpName: string, entry: Entry): Promise {
+ const file = Bun.file(filepath)
+ const data = await all()
+ await Bun.write(file, JSON.stringify({ ...data, [mcpName]: entry }, null, 2))
+ await fs.chmod(file.name!, 0o600)
+ }
+
+ export async function remove(mcpName: string): Promise {
+ const file = Bun.file(filepath)
+ const data = await all()
+ delete data[mcpName]
+ await Bun.write(file, JSON.stringify(data, null, 2))
+ await fs.chmod(file.name!, 0o600)
+ }
+
+ export async function updateTokens(mcpName: string, tokens: Tokens): Promise {
+ const entry = (await get(mcpName)) ?? {}
+ entry.tokens = tokens
+ await set(mcpName, entry)
+ }
+
+ export async function updateClientInfo(mcpName: string, clientInfo: ClientInfo): Promise {
+ const entry = (await get(mcpName)) ?? {}
+ entry.clientInfo = clientInfo
+ await set(mcpName, entry)
+ }
+
+ export async function updateCodeVerifier(mcpName: string, codeVerifier: string): Promise {
+ const entry = (await get(mcpName)) ?? {}
+ entry.codeVerifier = codeVerifier
+ await set(mcpName, entry)
+ }
+
+ export async function clearCodeVerifier(mcpName: string): Promise {
+ const entry = await get(mcpName)
+ if (entry) {
+ delete entry.codeVerifier
+ await set(mcpName, entry)
+ }
+ }
+}
diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts
index a68a1716f..7b5f81650 100644
--- a/packages/opencode/src/mcp/index.ts
+++ b/packages/opencode/src/mcp/index.ts
@@ -3,12 +3,17 @@ import { experimental_createMCPClient } from "@ai-sdk/mcp"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
+import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
import { Config } from "../config/config"
import { Log } from "../util/log"
import { NamedError } from "@opencode-ai/util/error"
import z from "zod/v4"
import { Instance } from "../project/instance"
import { withTimeout } from "@/util/timeout"
+import { McpOAuthProvider } from "./oauth-provider"
+import { McpOAuthCallback } from "./oauth-callback"
+import { McpAuth } from "./auth"
+import open from "open"
export namespace MCP {
const log = Log.create({ service: "mcp" })
@@ -46,6 +51,21 @@ export namespace MCP {
.meta({
ref: "MCPStatusFailed",
}),
+ z
+ .object({
+ status: z.literal("needs_auth"),
+ })
+ .meta({
+ ref: "MCPStatusNeedsAuth",
+ }),
+ z
+ .object({
+ status: z.literal("needs_client_registration"),
+ error: z.string(),
+ })
+ .meta({
+ ref: "MCPStatusNeedsClientRegistration",
+ }),
])
.meta({
ref: "MCPStatus",
@@ -53,6 +73,10 @@ export namespace MCP {
export type Status = z.infer
type MCPClient = Awaited>
+ // Store transports for OAuth servers to allow finishing auth
+ type TransportWithAuth = StreamableHTTPClientTransport | SSEClientTransport
+ const pendingOAuthTransports = new Map()
+
const state = Instance.state(
async () => {
const cfg = await Config.get()
@@ -62,6 +86,12 @@ export namespace MCP {
await Promise.all(
Object.entries(config).map(async ([key, mcp]) => {
+ // If disabled by config, mark as disabled without trying to connect
+ if (mcp.enabled === false) {
+ status[key] = { status: "disabled" }
+ return
+ }
+
const result = await create(key, mcp).catch(() => undefined)
if (!result) return
@@ -87,6 +117,7 @@ export namespace MCP {
}),
),
)
+ pendingOAuthTransports.clear()
},
)
@@ -120,58 +151,98 @@ export namespace MCP {
async function create(key: string, mcp: Config.Mcp) {
if (mcp.enabled === false) {
log.info("mcp server disabled", { key })
- return
+ return {
+ mcpClient: undefined,
+ status: { status: "disabled" as const },
+ }
}
log.info("found", { key, type: mcp.type })
let mcpClient: MCPClient | undefined
let status: Status | undefined = undefined
if (mcp.type === "remote") {
- const transports = [
+ // OAuth is enabled by default for remote servers unless explicitly disabled with oauth: false
+ const oauthDisabled = mcp.oauth === false
+ const oauthConfig = typeof mcp.oauth === "object" ? mcp.oauth : undefined
+ let authProvider: McpOAuthProvider | undefined
+
+ if (!oauthDisabled) {
+ authProvider = new McpOAuthProvider(
+ key,
+ mcp.url,
+ {
+ clientId: oauthConfig?.clientId,
+ clientSecret: oauthConfig?.clientSecret,
+ scope: oauthConfig?.scope,
+ },
+ {
+ onRedirect: async (url) => {
+ log.info("oauth redirect requested", { key, url: url.toString() })
+ // Store the URL - actual browser opening is handled by startAuth
+ },
+ },
+ )
+ }
+
+ const transports: Array<{ name: string; transport: TransportWithAuth }> = [
{
name: "StreamableHTTP",
transport: new StreamableHTTPClientTransport(new URL(mcp.url), {
- requestInit: {
- headers: mcp.headers,
- },
+ authProvider,
+ requestInit: mcp.headers ? { headers: mcp.headers } : undefined,
}),
},
{
name: "SSE",
transport: new SSEClientTransport(new URL(mcp.url), {
- requestInit: {
- headers: mcp.headers,
- },
+ authProvider,
+ requestInit: mcp.headers ? { headers: mcp.headers } : undefined,
}),
},
]
+
let lastError: Error | undefined
for (const { name, transport } of transports) {
- const result = await experimental_createMCPClient({
- name: "opencode",
- transport,
- })
- .then((client) => {
- log.info("connected", { key, transport: name })
- mcpClient = client
- status = { status: "connected" }
- return true
+ try {
+ mcpClient = await experimental_createMCPClient({
+ name: "opencode",
+ transport,
})
- .catch((error) => {
- lastError = error instanceof Error ? error : new Error(String(error))
- log.debug("transport connection failed", {
- key,
- transport: name,
- url: mcp.url,
- error: lastError.message,
- })
- status = {
- status: "failed" as const,
- error: lastError.message,
+ log.info("connected", { key, transport: name })
+ status = { status: "connected" }
+ break
+ } catch (error) {
+ lastError = error instanceof Error ? error : new Error(String(error))
+
+ // Handle OAuth-specific errors
+ if (error instanceof UnauthorizedError) {
+ log.info("mcp server requires authentication", { key, transport: name })
+
+ // Check if this is a "needs registration" error
+ if (lastError.message.includes("registration") || lastError.message.includes("client_id")) {
+ status = {
+ status: "needs_client_registration" as const,
+ error: "Server does not support dynamic client registration. Please provide clientId in config.",
+ }
+ } else {
+ // Store transport for later finishAuth call
+ pendingOAuthTransports.set(key, transport)
+ status = { status: "needs_auth" as const }
}
- return false
+ break
+ }
+
+ log.debug("transport connection failed", {
+ key,
+ transport: name,
+ url: mcp.url,
+ error: lastError.message,
})
- if (result) break
+ status = {
+ status: "failed" as const,
+ error: lastError.message,
+ }
+ }
}
}
@@ -254,18 +325,73 @@ export namespace MCP {
}
export async function status() {
- return state().then((state) => state.status)
+ const s = await state()
+ const cfg = await Config.get()
+ const config = cfg.mcp ?? {}
+ const result: Record = {}
+
+ // Include all MCPs from config, not just connected ones
+ for (const key of Object.keys(config)) {
+ result[key] = s.status[key] ?? { status: "disabled" }
+ }
+
+ return result
}
export async function clients() {
return state().then((state) => state.clients)
}
+ export async function connect(name: string) {
+ const cfg = await Config.get()
+ const config = cfg.mcp ?? {}
+ const mcp = config[name]
+ if (!mcp) {
+ log.error("MCP config not found", { name })
+ return
+ }
+
+ const result = await create(name, { ...mcp, enabled: true })
+
+ if (!result) {
+ const s = await state()
+ s.status[name] = {
+ status: "failed",
+ error: "Unknown error during connection",
+ }
+ return
+ }
+
+ const s = await state()
+ s.status[name] = result.status
+ if (result.mcpClient) {
+ s.clients[name] = result.mcpClient
+ }
+ }
+
+ export async function disconnect(name: string) {
+ const s = await state()
+ const client = s.clients[name]
+ if (client) {
+ await client.close().catch((error) => {
+ log.error("Failed to close MCP client", { name, error })
+ })
+ delete s.clients[name]
+ }
+ s.status[name] = { status: "disabled" }
+ }
+
export async function tools() {
const result: Record = {}
const s = await state()
const clientsSnapshot = await clients()
+
for (const [clientName, client] of Object.entries(clientsSnapshot)) {
+ // Only include tools from connected MCPs (skip disabled ones)
+ if (s.status[clientName]?.status !== "connected") {
+ continue
+ }
+
const tools = await client.tools().catch((e) => {
log.error("failed to get tools", { clientName, error: e.message })
const failedStatus = {
@@ -286,4 +412,165 @@ export namespace MCP {
}
return result
}
+
+ /**
+ * Start OAuth authentication flow for an MCP server.
+ * Returns the authorization URL that should be opened in a browser.
+ */
+ export async function startAuth(mcpName: string): Promise<{ authorizationUrl: string }> {
+ const cfg = await Config.get()
+ const mcpConfig = cfg.mcp?.[mcpName]
+
+ if (!mcpConfig) {
+ throw new Error(`MCP server not found: ${mcpName}`)
+ }
+
+ if (mcpConfig.type !== "remote") {
+ throw new Error(`MCP server ${mcpName} is not a remote server`)
+ }
+
+ if (mcpConfig.oauth === false) {
+ throw new Error(`MCP server ${mcpName} has OAuth explicitly disabled`)
+ }
+
+ // Start the callback server
+ await McpOAuthCallback.ensureRunning()
+
+ // Create a new auth provider for this flow
+ // OAuth config is optional - if not provided, we'll use auto-discovery
+ const oauthConfig = typeof mcpConfig.oauth === "object" ? mcpConfig.oauth : undefined
+ let capturedUrl: URL | undefined
+ const authProvider = new McpOAuthProvider(
+ mcpName,
+ mcpConfig.url,
+ {
+ clientId: oauthConfig?.clientId,
+ clientSecret: oauthConfig?.clientSecret,
+ scope: oauthConfig?.scope,
+ },
+ {
+ onRedirect: async (url) => {
+ capturedUrl = url
+ },
+ },
+ )
+
+ // Create transport with auth provider
+ const transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url), {
+ authProvider,
+ })
+
+ // Try to connect - this will trigger the OAuth flow
+ try {
+ await experimental_createMCPClient({
+ name: "opencode",
+ transport,
+ })
+ // If we get here, we're already authenticated
+ return { authorizationUrl: "" }
+ } catch (error) {
+ if (error instanceof UnauthorizedError && capturedUrl) {
+ // Store transport for finishAuth
+ pendingOAuthTransports.set(mcpName, transport)
+ return { authorizationUrl: capturedUrl.toString() }
+ }
+ throw error
+ }
+ }
+
+ /**
+ * Complete OAuth authentication after user authorizes in browser.
+ * Opens the browser and waits for callback.
+ */
+ export async function authenticate(mcpName: string): Promise {
+ const { authorizationUrl } = await startAuth(mcpName)
+
+ if (!authorizationUrl) {
+ // Already authenticated
+ const s = await state()
+ return s.status[mcpName] ?? { status: "connected" }
+ }
+
+ // Extract state from authorization URL to use as callback key
+ // If no state parameter, use mcpName as fallback
+ const authUrl = new URL(authorizationUrl)
+ const oauthState = authUrl.searchParams.get("state") ?? mcpName
+
+ // Open browser
+ log.info("opening browser for oauth", { mcpName, url: authorizationUrl, state: oauthState })
+ await open(authorizationUrl)
+
+ // Wait for callback using the OAuth state parameter (or mcpName as fallback)
+ const code = await McpOAuthCallback.waitForCallback(oauthState)
+
+ // Finish auth
+ return finishAuth(mcpName, code)
+ }
+
+ /**
+ * Complete OAuth authentication with the authorization code.
+ */
+ export async function finishAuth(mcpName: string, authorizationCode: string): Promise {
+ const transport = pendingOAuthTransports.get(mcpName)
+
+ if (!transport) {
+ throw new Error(`No pending OAuth flow for MCP server: ${mcpName}`)
+ }
+
+ try {
+ // Call finishAuth on the transport
+ await transport.finishAuth(authorizationCode)
+
+ // Clear the code verifier after successful auth
+ await McpAuth.clearCodeVerifier(mcpName)
+
+ // Now try to reconnect
+ const cfg = await Config.get()
+ const mcpConfig = cfg.mcp?.[mcpName]
+
+ if (!mcpConfig) {
+ throw new Error(`MCP server not found: ${mcpName}`)
+ }
+
+ // Re-add the MCP server to establish connection
+ pendingOAuthTransports.delete(mcpName)
+ const result = await add(mcpName, mcpConfig)
+
+ const statusRecord = result.status as Record
+ return statusRecord[mcpName] ?? { status: "failed", error: "Unknown error after auth" }
+ } catch (error) {
+ log.error("failed to finish oauth", { mcpName, error })
+ return {
+ status: "failed",
+ error: error instanceof Error ? error.message : String(error),
+ }
+ }
+ }
+
+ /**
+ * Remove OAuth credentials for an MCP server.
+ */
+ export async function removeAuth(mcpName: string): Promise {
+ await McpAuth.remove(mcpName)
+ McpOAuthCallback.cancelPending(mcpName)
+ pendingOAuthTransports.delete(mcpName)
+ log.info("removed oauth credentials", { mcpName })
+ }
+
+ /**
+ * Check if an MCP server supports OAuth (remote servers support OAuth by default unless explicitly disabled).
+ */
+ export async function supportsOAuth(mcpName: string): Promise {
+ const cfg = await Config.get()
+ const mcpConfig = cfg.mcp?.[mcpName]
+ return mcpConfig?.type === "remote" && mcpConfig.oauth !== false
+ }
+
+ /**
+ * Check if an MCP server has stored OAuth tokens.
+ */
+ export async function hasStoredTokens(mcpName: string): Promise {
+ const entry = await McpAuth.get(mcpName)
+ return !!entry?.tokens
+ }
}
diff --git a/packages/opencode/src/mcp/oauth-callback.ts b/packages/opencode/src/mcp/oauth-callback.ts
new file mode 100644
index 000000000..67bb51684
--- /dev/null
+++ b/packages/opencode/src/mcp/oauth-callback.ts
@@ -0,0 +1,203 @@
+import { Log } from "../util/log"
+import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider"
+
+const log = Log.create({ service: "mcp.oauth-callback" })
+
+const HTML_SUCCESS = `
+
+
+ OpenCode - Authorization Successful
+
+
+
+
+
Authorization Successful
+
You can close this window and return to OpenCode.
+
+
+
+`
+
+const HTML_ERROR = (error: string) => `
+
+
+ OpenCode - Authorization Failed
+
+
+
+
+
Authorization Failed
+
An error occurred during authorization.
+
${error}
+
+
+`
+
+interface PendingAuth {
+ resolve: (code: string) => void
+ reject: (error: Error) => void
+ timeout: ReturnType
+}
+
+export namespace McpOAuthCallback {
+ let server: ReturnType | undefined
+ const pendingAuths = new Map()
+
+ const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
+
+ export async function ensureRunning(): Promise {
+ if (server) return
+
+ const running = await isPortInUse()
+ if (running) {
+ log.info("oauth callback server already running on another instance", { port: OAUTH_CALLBACK_PORT })
+ return
+ }
+
+ server = Bun.serve({
+ port: OAUTH_CALLBACK_PORT,
+ fetch(req) {
+ const url = new URL(req.url)
+
+ if (url.pathname !== OAUTH_CALLBACK_PATH) {
+ return new Response("Not found", { status: 404 })
+ }
+
+ const code = url.searchParams.get("code")
+ const state = url.searchParams.get("state")
+ const error = url.searchParams.get("error")
+ const errorDescription = url.searchParams.get("error_description")
+
+ log.info("received oauth callback", { hasCode: !!code, state, error })
+
+ if (error) {
+ const errorMsg = errorDescription || error
+ if (state && pendingAuths.has(state)) {
+ const pending = pendingAuths.get(state)!
+ clearTimeout(pending.timeout)
+ pendingAuths.delete(state)
+ pending.reject(new Error(errorMsg))
+ }
+ return new Response(HTML_ERROR(errorMsg), {
+ headers: { "Content-Type": "text/html" },
+ })
+ }
+
+ if (!code) {
+ return new Response(HTML_ERROR("No authorization code provided"), {
+ status: 400,
+ headers: { "Content-Type": "text/html" },
+ })
+ }
+
+ // Try to find the pending auth by state parameter, or if no state, use the single pending auth
+ let pending: PendingAuth | undefined
+ let pendingKey: string | undefined
+
+ if (state && pendingAuths.has(state)) {
+ pending = pendingAuths.get(state)!
+ pendingKey = state
+ } else if (!state && pendingAuths.size === 1) {
+ // No state parameter but only one pending auth - use it
+ const [key, value] = pendingAuths.entries().next().value as [string, PendingAuth]
+ pending = value
+ pendingKey = key
+ log.info("no state parameter, using single pending auth", { key })
+ }
+
+ if (!pending || !pendingKey) {
+ const errorMsg = !state
+ ? "No state parameter provided and multiple pending authorizations"
+ : "Unknown or expired authorization request"
+ return new Response(HTML_ERROR(errorMsg), {
+ status: 400,
+ headers: { "Content-Type": "text/html" },
+ })
+ }
+
+ clearTimeout(pending.timeout)
+ pendingAuths.delete(pendingKey)
+ pending.resolve(code)
+
+ return new Response(HTML_SUCCESS, {
+ headers: { "Content-Type": "text/html" },
+ })
+ },
+ })
+
+ log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
+ }
+
+ export function waitForCallback(mcpName: string): Promise {
+ return new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ if (pendingAuths.has(mcpName)) {
+ pendingAuths.delete(mcpName)
+ reject(new Error("OAuth callback timeout - authorization took too long"))
+ }
+ }, CALLBACK_TIMEOUT_MS)
+
+ pendingAuths.set(mcpName, { resolve, reject, timeout })
+ })
+ }
+
+ export function cancelPending(mcpName: string): void {
+ const pending = pendingAuths.get(mcpName)
+ if (pending) {
+ clearTimeout(pending.timeout)
+ pendingAuths.delete(mcpName)
+ pending.reject(new Error("Authorization cancelled"))
+ }
+ }
+
+ export async function isPortInUse(): Promise {
+ return new Promise((resolve) => {
+ Bun.connect({
+ hostname: "127.0.0.1",
+ port: OAUTH_CALLBACK_PORT,
+ socket: {
+ open(socket) {
+ socket.end()
+ resolve(true)
+ },
+ error() {
+ resolve(false)
+ },
+ data() {},
+ close() {},
+ },
+ }).catch(() => {
+ resolve(false)
+ })
+ })
+ }
+
+ export async function stop(): Promise {
+ if (server) {
+ server.stop()
+ server = undefined
+ log.info("oauth callback server stopped")
+ }
+
+ for (const [name, pending] of pendingAuths) {
+ clearTimeout(pending.timeout)
+ pending.reject(new Error("OAuth callback server stopped"))
+ }
+ pendingAuths.clear()
+ }
+
+ export function isRunning(): boolean {
+ return server !== undefined
+ }
+}
diff --git a/packages/opencode/src/mcp/oauth-provider.ts b/packages/opencode/src/mcp/oauth-provider.ts
new file mode 100644
index 000000000..584eca8e8
--- /dev/null
+++ b/packages/opencode/src/mcp/oauth-provider.ts
@@ -0,0 +1,132 @@
+import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
+import type {
+ OAuthClientMetadata,
+ OAuthTokens,
+ OAuthClientInformation,
+ OAuthClientInformationFull,
+} from "@modelcontextprotocol/sdk/shared/auth.js"
+import { McpAuth } from "./auth"
+import { Log } from "../util/log"
+
+const log = Log.create({ service: "mcp.oauth" })
+
+const OAUTH_CALLBACK_PORT = 19876
+const OAUTH_CALLBACK_PATH = "/mcp/oauth/callback"
+
+export interface McpOAuthConfig {
+ clientId?: string
+ clientSecret?: string
+ scope?: string
+}
+
+export interface McpOAuthCallbacks {
+ onRedirect: (url: URL) => void | Promise
+}
+
+export class McpOAuthProvider implements OAuthClientProvider {
+ constructor(
+ private mcpName: string,
+ private serverUrl: string,
+ private config: McpOAuthConfig,
+ private callbacks: McpOAuthCallbacks,
+ ) {}
+
+ get redirectUrl(): string {
+ return `http://127.0.0.1:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`
+ }
+
+ get clientMetadata(): OAuthClientMetadata {
+ return {
+ redirect_uris: [this.redirectUrl],
+ client_name: "OpenCode",
+ client_uri: "https://opencode.ai",
+ grant_types: ["authorization_code", "refresh_token"],
+ response_types: ["code"],
+ token_endpoint_auth_method: this.config.clientSecret ? "client_secret_post" : "none",
+ }
+ }
+
+ async clientInformation(): Promise {
+ // Check config first (pre-registered client)
+ if (this.config.clientId) {
+ return {
+ client_id: this.config.clientId,
+ client_secret: this.config.clientSecret,
+ }
+ }
+
+ // Check stored client info (from dynamic registration)
+ const entry = await McpAuth.get(this.mcpName)
+ if (entry?.clientInfo) {
+ // Check if client secret has expired
+ if (entry.clientInfo.clientSecretExpiresAt && entry.clientInfo.clientSecretExpiresAt < Date.now() / 1000) {
+ log.info("client secret expired, need to re-register", { mcpName: this.mcpName })
+ return undefined
+ }
+ return {
+ client_id: entry.clientInfo.clientId,
+ client_secret: entry.clientInfo.clientSecret,
+ }
+ }
+
+ // No client info - will trigger dynamic registration
+ return undefined
+ }
+
+ async saveClientInformation(info: OAuthClientInformationFull): Promise {
+ await McpAuth.updateClientInfo(this.mcpName, {
+ clientId: info.client_id,
+ clientSecret: info.client_secret,
+ clientIdIssuedAt: info.client_id_issued_at,
+ clientSecretExpiresAt: info.client_secret_expires_at,
+ })
+ log.info("saved dynamically registered client", {
+ mcpName: this.mcpName,
+ clientId: info.client_id,
+ })
+ }
+
+ async tokens(): Promise {
+ const entry = await McpAuth.get(this.mcpName)
+ if (!entry?.tokens) return undefined
+
+ return {
+ access_token: entry.tokens.accessToken,
+ token_type: "Bearer",
+ refresh_token: entry.tokens.refreshToken,
+ expires_in: entry.tokens.expiresAt
+ ? Math.max(0, Math.floor(entry.tokens.expiresAt - Date.now() / 1000))
+ : undefined,
+ scope: entry.tokens.scope,
+ }
+ }
+
+ async saveTokens(tokens: OAuthTokens): Promise {
+ await McpAuth.updateTokens(this.mcpName, {
+ accessToken: tokens.access_token,
+ refreshToken: tokens.refresh_token,
+ expiresAt: tokens.expires_in ? Date.now() / 1000 + tokens.expires_in : undefined,
+ scope: tokens.scope,
+ })
+ log.info("saved oauth tokens", { mcpName: this.mcpName })
+ }
+
+ async redirectToAuthorization(authorizationUrl: URL): Promise {
+ log.info("redirecting to authorization", { mcpName: this.mcpName, url: authorizationUrl.toString() })
+ await this.callbacks.onRedirect(authorizationUrl)
+ }
+
+ async saveCodeVerifier(codeVerifier: string): Promise {
+ await McpAuth.updateCodeVerifier(this.mcpName, codeVerifier)
+ }
+
+ async codeVerifier(): Promise {
+ const entry = await McpAuth.get(this.mcpName)
+ if (!entry?.codeVerifier) {
+ throw new Error(`No code verifier saved for MCP server: ${this.mcpName}`)
+ }
+ return entry.codeVerifier
+ }
+}
+
+export { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH }
diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts
index 32dbd5a03..f3a8852ae 100644
--- a/packages/opencode/src/permission/index.ts
+++ b/packages/opencode/src/permission/index.ts
@@ -1,5 +1,6 @@
+import { BusEvent } from "@/bus/bus-event"
+import { Bus } from "@/bus"
import z from "zod"
-import { Bus } from "../bus"
import { Log } from "../util/log"
import { Identifier } from "../id/id"
import { Plugin } from "../plugin"
@@ -38,8 +39,8 @@ export namespace Permission {
export type Info = z.infer
export const Event = {
- Updated: Bus.event("permission.updated", Info),
- Replied: Bus.event(
+ Updated: BusEvent.define("permission.updated", Info),
+ Replied: BusEvent.define(
"permission.replied",
z.object({
sessionID: z.string(),
diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts
index 7d1f50ec8..b492c7179 100644
--- a/packages/opencode/src/plugin/index.ts
+++ b/packages/opencode/src/plugin/index.ts
@@ -28,8 +28,8 @@ export namespace Plugin {
}
const plugins = [...(config.plugin ?? [])]
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
- plugins.push("opencode-copilot-auth@0.0.7")
- plugins.push("opencode-anthropic-auth@0.0.2")
+ plugins.push("opencode-copilot-auth@0.0.9")
+ plugins.push("opencode-anthropic-auth@0.0.5")
}
for (let plugin of plugins) {
log.info("loading plugin", { path: plugin })
diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts
index 4defefa51..5291995a3 100644
--- a/packages/opencode/src/project/instance.ts
+++ b/packages/opencode/src/project/instance.ts
@@ -3,6 +3,7 @@ import { Context } from "../util/context"
import { Project } from "./project"
import { State } from "./state"
import { iife } from "@/util/iife"
+import { GlobalBus } from "@/bus/global"
interface Context {
directory: string
@@ -52,6 +53,15 @@ export const Instance = {
Log.Default.info("disposing instance", { directory: Instance.directory })
await State.dispose(Instance.directory)
cache.delete(Instance.directory)
+ GlobalBus.emit("event", {
+ directory: Instance.directory,
+ payload: {
+ type: "server.instance.disposed",
+ properties: {
+ directory: Instance.directory,
+ },
+ },
+ })
},
async disposeAll() {
Log.Default.info("disposing all instances")
diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts
index 74e969145..80c712605 100644
--- a/packages/opencode/src/project/project.ts
+++ b/packages/opencode/src/project/project.ts
@@ -5,6 +5,12 @@ import { $ } from "bun"
import { Storage } from "../storage/storage"
import { Log } from "../util/log"
import { Flag } from "@/flag/flag"
+import { Session } from "../session"
+import { work } from "../util/queue"
+import { fn } from "@opencode-ai/util/fn"
+import { BusEvent } from "@/bus/bus-event"
+import { iife } from "@/util/iife"
+import { GlobalBus } from "@/bus/global"
export namespace Project {
const log = Log.create({ service: "project" })
@@ -12,10 +18,17 @@ export namespace Project {
.object({
id: z.string(),
worktree: z.string(),
- vcsDir: z.string().optional(),
vcs: z.literal("git").optional(),
+ name: z.string().optional(),
+ icon: z
+ .object({
+ url: z.string().optional(),
+ color: z.string().optional(),
+ })
+ .optional(),
time: z.object({
created: z.number(),
+ updated: z.number(),
initialized: z.number().optional(),
}),
})
@@ -24,80 +37,147 @@ export namespace Project {
})
export type Info = z.infer
+ export const Event = {
+ Updated: BusEvent.define("project.updated", Info),
+ }
+
export async function fromDirectory(directory: string) {
log.info("fromDirectory", { directory })
- const matches = Filesystem.up({ targets: [".git"], start: directory })
- const git = await matches.next().then((x) => x.value)
- await matches.return()
- if (!git) {
- const project: Info = {
+
+ const { id, worktree, vcs } = await iife(async () => {
+ const matches = Filesystem.up({ targets: [".git"], start: directory })
+ const git = await matches.next().then((x) => x.value)
+ await matches.return()
+ if (git) {
+ let worktree = path.dirname(git)
+ let id = await Bun.file(path.join(git, "opencode"))
+ .text()
+ .then((x) => x.trim())
+ .catch(() => {})
+ if (!id) {
+ const roots = await $`git rev-list --max-parents=0 --all`
+ .quiet()
+ .nothrow()
+ .cwd(worktree)
+ .text()
+ .then((x) =>
+ x
+ .split("\n")
+ .filter(Boolean)
+ .map((x) => x.trim())
+ .toSorted(),
+ )
+ id = roots[0]
+ if (id) Bun.file(path.join(git, "opencode")).write(id)
+ }
+ if (!id)
+ return {
+ id: "global",
+ worktree,
+ vcs: "git",
+ }
+ worktree = await $`git rev-parse --show-toplevel`
+ .quiet()
+ .nothrow()
+ .cwd(worktree)
+ .text()
+ .then((x) => path.resolve(worktree, x.trim()))
+ return { id, worktree, vcs: "git" }
+ }
+
+ return {
id: "global",
worktree: "/",
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
+ }
+ })
+
+ let existing = await Storage.read(["project", id]).catch(() => undefined)
+ if (!existing) {
+ existing = {
+ id,
+ worktree,
+ vcs: vcs as Info["vcs"],
time: {
created: Date.now(),
+ updated: Date.now(),
},
}
- await Storage.write(["project", "global"], project)
- return project
- }
- let worktree = path.dirname(git)
- const timer = log.time("git.rev-parse")
- let id = await Bun.file(path.join(git, "opencode"))
- .text()
- .then((x) => x.trim())
- .catch(() => {})
- if (!id) {
- const roots = await $`git rev-list --max-parents=0 --all`
- .quiet()
- .nothrow()
- .cwd(worktree)
- .text()
- .then((x) =>
- x
- .split("\n")
- .filter(Boolean)
- .map((x) => x.trim())
- .toSorted(),
- )
- id = roots[0]
- if (id) Bun.file(path.join(git, "opencode")).write(id)
- }
- timer.stop()
- if (!id) {
- const project: Info = {
- id: "global",
- worktree: "/",
- time: {
- created: Date.now(),
- },
+ if (id !== "global") {
+ await migrateFromGlobal(id, worktree)
}
- await Storage.write(["project", "global"], project)
- return project
}
- worktree = await $`git rev-parse --path-format=absolute --show-toplevel`
- .quiet()
- .nothrow()
- .cwd(worktree)
- .text()
- .then((x) => x.trim())
- const vcsDir = await $`git rev-parse --path-format=absolute --git-dir`
- .quiet()
- .nothrow()
- .cwd(worktree)
- .text()
- .then((x) => x.trim())
- const project: Info = {
- id,
+ if (Flag.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY) discover(existing)
+ const result: Info = {
+ ...existing,
worktree,
- vcsDir,
- vcs: "git",
+ vcs: vcs as Info["vcs"],
time: {
- created: Date.now(),
+ ...existing.time,
+ updated: Date.now(),
},
}
- await Storage.write(["project", id], project)
- return project
+ await Storage.write(["project", id], result)
+ GlobalBus.emit("event", {
+ payload: {
+ type: Event.Updated.type,
+ properties: result,
+ },
+ })
+ return result
+ }
+
+ export async function discover(input: Info) {
+ if (input.vcs !== "git") return
+ if (input.icon?.url) return
+ const glob = new Bun.Glob("**/{favicon}.{ico,png,svg,jpg,jpeg,webp}")
+ const matches = await Array.fromAsync(
+ glob.scan({
+ cwd: input.worktree,
+ absolute: true,
+ onlyFiles: true,
+ followSymlinks: false,
+ dot: false,
+ }),
+ )
+ const shortest = matches.sort((a, b) => a.length - b.length)[0]
+ if (!shortest) return
+ const file = Bun.file(shortest)
+ const buffer = await file.arrayBuffer()
+ const base64 = Buffer.from(buffer).toString("base64")
+ const mime = file.type || "image/png"
+ const url = `data:${mime};base64,${base64}`
+ await update({
+ projectID: input.id,
+ icon: {
+ url,
+ },
+ })
+ return
+ }
+
+ async function migrateFromGlobal(newProjectID: string, worktree: string) {
+ const globalProject = await Storage.read(["project", "global"]).catch(() => undefined)
+ if (!globalProject) return
+
+ const globalSessions = await Storage.list(["session", "global"]).catch(() => [])
+ if (globalSessions.length === 0) return
+
+ log.info("migrating sessions from global", { newProjectID, worktree, count: globalSessions.length })
+
+ await work(10, globalSessions, async (key) => {
+ const sessionID = key[key.length - 1]
+ const session = await Storage.read(key).catch(() => undefined)
+ if (!session) return
+ if (session.directory && session.directory !== worktree) return
+
+ session.projectID = newProjectID
+ log.info("migrating session", { sessionID, from: "global", to: newProjectID })
+ await Storage.write(["session", newProjectID, sessionID], session)
+ await Storage.remove(key)
+ }).catch((error) => {
+ log.error("failed to migrate sessions from global to project", { error, projectId: newProjectID })
+ })
}
export async function setInitialized(projectID: string) {
@@ -110,4 +190,32 @@ export namespace Project {
const keys = await Storage.list(["project"])
return await Promise.all(keys.map((x) => Storage.read(x)))
}
+
+ export const update = fn(
+ z.object({
+ projectID: z.string(),
+ name: z.string().optional(),
+ icon: Info.shape.icon.optional(),
+ }),
+ async (input) => {
+ const result = await Storage.update(["project", input.projectID], (draft) => {
+ if (input.name !== undefined) draft.name = input.name
+ if (input.icon !== undefined) {
+ draft.icon = {
+ ...draft.icon,
+ }
+ if (input.icon.url !== undefined) draft.icon.url = input.icon.url
+ if (input.icon.color !== undefined) draft.icon.color = input.icon.color
+ }
+ draft.time.updated = Date.now()
+ })
+ GlobalBus.emit("event", {
+ payload: {
+ type: Event.Updated.type,
+ properties: result,
+ },
+ })
+ return result
+ },
+ )
}
diff --git a/packages/opencode/src/project/vcs.ts b/packages/opencode/src/project/vcs.ts
index a8d5e91b3..e434b5f8c 100644
--- a/packages/opencode/src/project/vcs.ts
+++ b/packages/opencode/src/project/vcs.ts
@@ -1,8 +1,9 @@
+import { BusEvent } from "@/bus/bus-event"
+import { Bus } from "@/bus"
import { $ } from "bun"
import path from "path"
import z from "zod"
import { Log } from "@/util/log"
-import { Bus } from "@/bus"
import { Instance } from "./instance"
import { FileWatcher } from "@/file/watcher"
@@ -10,7 +11,7 @@ const log = Log.create({ service: "vcs" })
export namespace Vcs {
export const Event = {
- BranchUpdated: Bus.event(
+ BranchUpdated: BusEvent.define(
"vcs.branch.updated",
z.object({
branch: z.string().optional(),
@@ -39,16 +40,14 @@ export namespace Vcs {
const state = Instance.state(
async () => {
- const vcsDir = Instance.project.vcsDir
- if (Instance.project.vcs !== "git" || !vcsDir) {
+ if (Instance.project.vcs !== "git") {
return { branch: async () => undefined, unsubscribe: undefined }
}
let current = await currentBranch()
log.info("initialized", { branch: current })
- const head = path.join(vcsDir, "HEAD")
const unsubscribe = Bus.subscribe(FileWatcher.Event.Updated, async (evt) => {
- if (evt.properties.file !== head) return
+ if (evt.properties.file.endsWith("HEAD")) return
const next = await currentBranch()
if (next !== current) {
log.info("branch changed", { from: current, to: next })
diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts
index 676837e15..c58638d28 100644
--- a/packages/opencode/src/provider/models.ts
+++ b/packages/opencode/src/provider/models.ts
@@ -4,21 +4,33 @@ import path from "path"
import z from "zod"
import { data } from "./models-macro" with { type: "macro" }
import { Installation } from "../installation"
+import { Flag } from "../flag/flag"
export namespace ModelsDev {
const log = Log.create({ service: "models.dev" })
const filepath = path.join(Global.Path.cache, "models.json")
- export const Model = z
- .object({
- id: z.string(),
- name: z.string(),
- release_date: z.string(),
- attachment: z.boolean(),
- reasoning: z.boolean(),
- temperature: z.boolean(),
- tool_call: z.boolean(),
- cost: z.object({
+ export const Model = z.object({
+ id: z.string(),
+ name: z.string(),
+ family: z.string().optional(),
+ release_date: z.string(),
+ attachment: z.boolean(),
+ reasoning: z.boolean(),
+ temperature: z.boolean(),
+ tool_call: z.boolean(),
+ interleaved: z
+ .union([
+ z.literal(true),
+ z
+ .object({
+ field: z.enum(["reasoning_content", "reasoning_details"]),
+ })
+ .strict(),
+ ])
+ .optional(),
+ cost: z
+ .object({
input: z.number(),
output: z.number(),
cache_read: z.number().optional(),
@@ -31,40 +43,34 @@ export namespace ModelsDev {
cache_write: z.number().optional(),
})
.optional(),
- }),
- limit: z.object({
- context: z.number(),
- output: z.number(),
- }),
- modalities: z
- .object({
- input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
- output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
- })
- .optional(),
- experimental: z.boolean().optional(),
- status: z.enum(["alpha", "beta", "deprecated"]).optional(),
- options: z.record(z.string(), z.any()),
- headers: z.record(z.string(), z.string()).optional(),
- provider: z.object({ npm: z.string() }).optional(),
- })
- .meta({
- ref: "Model",
- })
+ })
+ .optional(),
+ limit: z.object({
+ context: z.number(),
+ output: z.number(),
+ }),
+ modalities: z
+ .object({
+ input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
+ output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
+ })
+ .optional(),
+ experimental: z.boolean().optional(),
+ status: z.enum(["alpha", "beta", "deprecated"]).optional(),
+ options: z.record(z.string(), z.any()),
+ headers: z.record(z.string(), z.string()).optional(),
+ provider: z.object({ npm: z.string() }).optional(),
+ })
export type Model = z.infer
- export const Provider = z
- .object({
- api: z.string().optional(),
- name: z.string(),
- env: z.array(z.string()),
- id: z.string(),
- npm: z.string().optional(),
- models: z.record(z.string(), Model),
- })
- .meta({
- ref: "Provider",
- })
+ export const Provider = z.object({
+ api: z.string().optional(),
+ name: z.string(),
+ env: z.array(z.string()),
+ id: z.string(),
+ npm: z.string().optional(),
+ models: z.record(z.string(), Model),
+ })
export type Provider = z.infer
@@ -78,6 +84,7 @@ export namespace ModelsDev {
}
export async function refresh() {
+ if (Flag.OPENCODE_DISABLE_MODELS_FETCH) return
const file = Bun.file(filepath)
log.info("refreshing", {
file,
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index ded0b9b19..b8d4dadbd 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -1,14 +1,15 @@
import z from "zod"
import fuzzysort from "fuzzysort"
import { Config } from "../config/config"
-import { mergeDeep, sortBy } from "remeda"
-import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
+import { mapValues, mergeDeep, sortBy } from "remeda"
+import { NoSuchModelError, type Provider as SDK } from "ai"
import { Log } from "../util/log"
import { BunProc } from "../bun"
import { Plugin } from "../plugin"
import { ModelsDev } from "./models"
import { NamedError } from "@opencode-ai/util/error"
import { Auth } from "../auth"
+import { Env } from "../env"
import { Instance } from "../project/instance"
import { Flag } from "../flag/flag"
import { iife } from "@/util/iife"
@@ -22,7 +23,8 @@ import { createVertex } from "@ai-sdk/google-vertex"
import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic"
import { createOpenAI } from "@ai-sdk/openai"
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
-import { createOpenRouter } from "@openrouter/ai-sdk-provider"
+import { createOpenRouter, type LanguageModelV2 } from "@openrouter/ai-sdk-provider"
+import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/openai-compatible/src"
export namespace Provider {
const log = Log.create({ service: "provider" })
@@ -37,16 +39,17 @@ export namespace Provider {
"@ai-sdk/openai": createOpenAI,
"@ai-sdk/openai-compatible": createOpenAICompatible,
"@openrouter/ai-sdk-provider": createOpenRouter,
+ // @ts-ignore (TODO: kill this code so we dont have to maintain it)
+ "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
}
- type CustomLoader = (provider: ModelsDev.Provider) => Promise<{
+ type CustomModelLoader = (sdk: any, modelID: string, options?: Record) => Promise
+ type CustomLoader = (provider: Info) => Promise<{
autoload: boolean
- getModel?: (sdk: any, modelID: string, options?: Record) => Promise
+ getModel?: CustomModelLoader
options?: Record
}>
- type Source = "env" | "config" | "custom" | "api"
-
const CUSTOM_LOADERS: Record = {
async anthropic() {
return {
@@ -61,7 +64,8 @@ export namespace Provider {
},
async opencode(input) {
const hasKey = await (async () => {
- if (input.env.some((item) => process.env[item])) return true
+ const env = Env.all()
+ if (input.env.some((item) => env[item])) return true
if (await Auth.get(input.id)) return true
return false
})()
@@ -87,6 +91,30 @@ export namespace Provider {
options: {},
}
},
+ "github-copilot": async () => {
+ return {
+ autoload: false,
+ async getModel(sdk: any, modelID: string, _options?: Record) {
+ if (modelID.includes("codex")) {
+ return sdk.responses(modelID)
+ }
+ return sdk.chat(modelID)
+ },
+ options: {},
+ }
+ },
+ "github-copilot-enterprise": async () => {
+ return {
+ autoload: false,
+ async getModel(sdk: any, modelID: string, _options?: Record) {
+ if (modelID.includes("codex")) {
+ return sdk.responses(modelID)
+ }
+ return sdk.chat(modelID)
+ },
+ options: {},
+ }
+ },
azure: async () => {
return {
autoload: false,
@@ -101,7 +129,7 @@ export namespace Provider {
}
},
"azure-cognitive-services": async () => {
- const resourceName = process.env["AZURE_COGNITIVE_SERVICES_RESOURCE_NAME"]
+ const resourceName = Env.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME")
return {
autoload: false,
async getModel(sdk: any, modelID: string, options?: Record) {
@@ -117,10 +145,15 @@ export namespace Provider {
}
},
"amazon-bedrock": async () => {
- if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"] && !process.env["AWS_BEARER_TOKEN_BEDROCK"])
- return { autoload: false }
+ const [awsProfile, awsAccessKeyId, awsBearerToken, awsRegion] = await Promise.all([
+ Env.get("AWS_PROFILE"),
+ Env.get("AWS_ACCESS_KEY_ID"),
+ Env.get("AWS_BEARER_TOKEN_BEDROCK"),
+ Env.get("AWS_REGION"),
+ ])
+ if (!awsProfile && !awsAccessKeyId && !awsBearerToken) return { autoload: false }
- const region = process.env["AWS_REGION"] ?? "us-east-1"
+ const region = awsRegion ?? "us-east-1"
const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers"))
return {
@@ -219,8 +252,8 @@ export namespace Provider {
}
},
"google-vertex": async () => {
- const project = process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCP_PROJECT"] ?? process.env["GCLOUD_PROJECT"]
- const location = process.env["GOOGLE_CLOUD_LOCATION"] ?? process.env["VERTEX_LOCATION"] ?? "us-east5"
+ const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT")
+ const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "us-east5"
const autoload = Boolean(project)
if (!autoload) return { autoload: false }
return {
@@ -236,8 +269,8 @@ export namespace Provider {
}
},
"google-vertex-anthropic": async () => {
- const project = process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCP_PROJECT"] ?? process.env["GCLOUD_PROJECT"]
- const location = process.env["GOOGLE_CLOUD_LOCATION"] ?? process.env["VERTEX_LOCATION"] ?? "global"
+ const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT")
+ const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "global"
const autoload = Boolean(project)
if (!autoload) return { autoload: false }
return {
@@ -246,12 +279,34 @@ export namespace Provider {
project,
location,
},
- async getModel(sdk: any, modelID: string) {
+ async getModel(sdk: any, modelID) {
const id = String(modelID).trim()
return sdk.languageModel(id)
},
}
},
+ "sap-ai-core": async () => {
+ const auth = await Auth.get("sap-ai-core")
+ const envServiceKey = iife(() => {
+ const envAICoreServiceKey = Env.get("AICORE_SERVICE_KEY")
+ if (envAICoreServiceKey) return envAICoreServiceKey
+ if (auth?.type === "api") {
+ Env.set("AICORE_SERVICE_KEY", auth.key)
+ return auth.key
+ }
+ return undefined
+ })
+ const deploymentId = Env.get("AICORE_DEPLOYMENT_ID")
+ const resourceGroup = Env.get("AICORE_RESOURCE_GROUP")
+
+ return {
+ autoload: !!envServiceKey,
+ options: envServiceKey ? { deploymentId, resourceGroup } : {},
+ async getModel(sdk: any, modelID: string) {
+ return sdk(modelID)
+ },
+ }
+ },
zenmux: async () => {
return {
autoload: false,
@@ -263,12 +318,179 @@ export namespace Provider {
},
}
},
+ cerebras: async () => {
+ return {
+ autoload: false,
+ options: {
+ headers: {
+ "X-Cerebras-3rd-Party-Integration": "opencode",
+ },
+ },
+ }
+ },
+ }
+
+ export const Model = z
+ .object({
+ id: z.string(),
+ providerID: z.string(),
+ api: z.object({
+ id: z.string(),
+ url: z.string(),
+ npm: z.string(),
+ }),
+ name: z.string(),
+ family: z.string().optional(),
+ capabilities: z.object({
+ temperature: z.boolean(),
+ reasoning: z.boolean(),
+ attachment: z.boolean(),
+ toolcall: z.boolean(),
+ input: z.object({
+ text: z.boolean(),
+ audio: z.boolean(),
+ image: z.boolean(),
+ video: z.boolean(),
+ pdf: z.boolean(),
+ }),
+ output: z.object({
+ text: z.boolean(),
+ audio: z.boolean(),
+ image: z.boolean(),
+ video: z.boolean(),
+ pdf: z.boolean(),
+ }),
+ interleaved: z.union([
+ z.boolean(),
+ z.object({
+ field: z.enum(["reasoning_content", "reasoning_details"]),
+ }),
+ ]),
+ }),
+ cost: z.object({
+ input: z.number(),
+ output: z.number(),
+ cache: z.object({
+ read: z.number(),
+ write: z.number(),
+ }),
+ experimentalOver200K: z
+ .object({
+ input: z.number(),
+ output: z.number(),
+ cache: z.object({
+ read: z.number(),
+ write: z.number(),
+ }),
+ })
+ .optional(),
+ }),
+ limit: z.object({
+ context: z.number(),
+ output: z.number(),
+ }),
+ status: z.enum(["alpha", "beta", "deprecated", "active"]),
+ options: z.record(z.string(), z.any()),
+ headers: z.record(z.string(), z.string()),
+ release_date: z.string(),
+ })
+ .meta({
+ ref: "Model",
+ })
+ export type Model = z.infer
+
+ export const Info = z
+ .object({
+ id: z.string(),
+ name: z.string(),
+ source: z.enum(["env", "config", "custom", "api"]),
+ env: z.string().array(),
+ key: z.string().optional(),
+ options: z.record(z.string(), z.any()),
+ models: z.record(z.string(), Model),
+ })
+ .meta({
+ ref: "Provider",
+ })
+ export type Info = z.infer
+
+ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
+ return {
+ id: model.id,
+ providerID: provider.id,
+ name: model.name,
+ family: model.family,
+ api: {
+ id: model.id,
+ url: provider.api!,
+ npm: model.provider?.npm ?? provider.npm ?? provider.id,
+ },
+ status: model.status ?? "active",
+ headers: model.headers ?? {},
+ options: model.options ?? {},
+ cost: {
+ input: model.cost?.input ?? 0,
+ output: model.cost?.output ?? 0,
+ cache: {
+ read: model.cost?.cache_read ?? 0,
+ write: model.cost?.cache_write ?? 0,
+ },
+ experimentalOver200K: model.cost?.context_over_200k
+ ? {
+ cache: {
+ read: model.cost.context_over_200k.cache_read ?? 0,
+ write: model.cost.context_over_200k.cache_write ?? 0,
+ },
+ input: model.cost.context_over_200k.input,
+ output: model.cost.context_over_200k.output,
+ }
+ : undefined,
+ },
+ limit: {
+ context: model.limit.context,
+ output: model.limit.output,
+ },
+ capabilities: {
+ temperature: model.temperature,
+ reasoning: model.reasoning,
+ attachment: model.attachment,
+ toolcall: model.tool_call,
+ input: {
+ text: model.modalities?.input?.includes("text") ?? false,
+ audio: model.modalities?.input?.includes("audio") ?? false,
+ image: model.modalities?.input?.includes("image") ?? false,
+ video: model.modalities?.input?.includes("video") ?? false,
+ pdf: model.modalities?.input?.includes("pdf") ?? false,
+ },
+ output: {
+ text: model.modalities?.output?.includes("text") ?? false,
+ audio: model.modalities?.output?.includes("audio") ?? false,
+ image: model.modalities?.output?.includes("image") ?? false,
+ video: model.modalities?.output?.includes("video") ?? false,
+ pdf: model.modalities?.output?.includes("pdf") ?? false,
+ },
+ interleaved: model.interleaved ?? false,
+ },
+ release_date: model.release_date,
+ }
+ }
+
+ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
+ return {
+ id: provider.id,
+ source: "custom",
+ name: provider.name,
+ env: provider.env ?? [],
+ options: {},
+ models: mapValues(provider.models, (model) => fromModelsDevModel(provider, model)),
+ }
}
const state = Instance.state(async () => {
using _ = log.time("state")
const config = await Config.get()
- const database = await ModelsDev.get()
+ const modelsDev = await ModelsDev.get()
+ const database = mapValues(modelsDev, fromModelsDevProvider)
const disabled = new Set(config.disabled_providers ?? [])
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null
@@ -279,54 +501,15 @@ export namespace Provider {
return true
}
- const providers: {
- [providerID: string]: {
- source: Source
- info: ModelsDev.Provider
- getModel?: (sdk: any, modelID: string, options?: Record) => Promise
- options: Record
- }
+ const providers: { [providerID: string]: Info } = {}
+ const languages = new Map()
+ const modelLoaders: {
+ [providerID: string]: CustomModelLoader
} = {}
- const models = new Map<
- string,
- {
- providerID: string
- modelID: string
- info: ModelsDev.Model
- language: LanguageModel
- npm?: string
- }
- >()
const sdk = new Map