Compare commits

..

48 Commits

Author SHA1 Message Date
opencode-agent[bot] 6106cb64c7 chore: generate 2026-08-17 03:35:05 +00:00
opencode-agent[bot] c786ab92d3 test(cli): ignore logs before standalone readiness (#42968)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-16 22:33:55 -05:00
Luke Parker c400746dd5 fix(app): render code mode executions (#42949) 2026-08-17 13:03:06 +10:00
opencode-agent[bot] e3ce37899d fix(core): run HTTP hooks for session generate (#42965)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-16 21:44:22 -05:00
opencode-agent[bot] c5e58b38a8 fix(app): omit shell prompts when copying commands (#42958)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-17 12:33:43 +10:00
Aiden Cline 1f86a6d3f0 fix(core): restore OpenAI request headers (#42959) 2026-08-16 21:25:48 -05:00
Luke Parker be8f975a99 fix(app): reduce session spinner CPU usage (#42952) 2026-08-17 11:04:02 +10:00
opencode-agent[bot] c7abb340f1 chore: generate 2026-08-16 23:37:10 +00:00
opencode-agent[bot] 45771a7e39 chore(util): log spawned processes (#42948)
Co-authored-by: Dax Raad <d@ironbay.co>
2026-08-16 19:36:02 -04:00
Dax 39810da936 docs: reorganize v2 documentation (#42947) 2026-08-16 19:35:48 -04:00
Luke Parker 01c8bf20f9 fix(app): correct background subagent status (#42944) 2026-08-17 09:20:51 +10:00
opencode-agent[bot] 9241a79dc9 chore: generate 2026-08-16 22:50:26 +00:00
Luke Parker e73fa8f4b7 fix(app): clarify skill timeline presentation (#42945) 2026-08-17 08:49:18 +10:00
opencode-agent[bot] fd2699b4d7 fix(tui): clarify saved permission copy (#41144)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
2026-08-16 17:14:19 -05:00
Aiden Cline ec10d71f22 fix(core): keep plan mode reminders in sync with session agent (#42652) 2026-08-16 16:01:02 -05:00
opencode-agent[bot] ca589273c7 fix(core): preserve tool-result images for AI SDK providers (#42849)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-08-16 15:20:37 -05:00
opencode-agent[bot] 75a979ec5c fix(ai): pass media through protocol lowering (#42921)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-16 15:18:43 -05:00
Dax Raad d4f10fa9be fix(release): stop advancing next npm tags 2026-08-16 11:24:58 -04:00
opencode-agent[bot] 08dd3f51ed fix(app): space no git status (#42868)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-16 08:14:58 +00:00
Dax Raad 0e99cb987a fix(desktop): use built CLI during beta build 2026-08-16 03:20:15 -04:00
opencode-agent[bot] 7731d1235d fix(cli): clarify debug config output (#42860)
Co-authored-by: Dax Raad <d@ironbay.co>
2026-08-16 02:39:47 -04:00
opencode-agent[bot] 174c0a742c chore: generate 2026-08-16 06:39:30 +00:00
Dax b080f216a5 feat(cli): add native CPU profiling (#42862) 2026-08-16 02:38:23 -04:00
Simon Klee 0d7fe6e074 fix(cli): omit undefined compile executable path 2026-08-16 08:03:11 +02:00
opencode-agent[bot] 01b7b53eeb chore: generate 2026-08-16 05:32:10 +00:00
Dax Raad 613c570a3b feat(cli): add heap snapshot signal 2026-08-16 01:30:44 -04:00
Dax Raad a48d44955e fix(cli): suppress compiled runtime warnings 2026-08-16 01:03:25 -04:00
Dax Raad 8251934007 fix(core): batch initial streamed delta 2026-08-15 19:26:16 -04:00
Dax Raad 42e345e1bc fix(cli): resolve Bun canary compile assets 2026-08-15 18:56:03 -04:00
Dax 6a7d6c5adc refactor(core): use numeric event timestamps (#42828) 2026-08-15 18:48:58 -04:00
Dax Raad 9e22c40fff feat: align V2 beta release channels 2026-08-15 18:40:23 -04:00
opencode-agent[bot] 467722c2f9 fix(app): use tree directory picker everywhere (#42820)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-16 08:19:44 +10:00
Luke Parker d9dfceddf8 fix(app): release virtualized timeline elements (#42825) 2026-08-16 08:19:06 +10:00
Dax 4fee4d7d86 fix(core): batch streamed session deltas 2026-08-15 18:16:01 -04:00
Luke Parker 61d7f942b5 fix(app): show new session header immediately (#42822) 2026-08-16 08:04:28 +10:00
Dax Raad d8e1753330 chore(cli): build binaries with Bun 1.4 2026-08-15 17:51:09 -04:00
opencode-agent[bot] d94d520f45 fix(tui): make running subagents clickable (#42797)
Co-authored-by: Dax Raad <mail@thdxr.com>
2026-08-15 19:54:40 +00:00
Dax Raad fcc1e9c42f docs(core): add MCP setup guidance 2026-08-15 15:13:28 -04:00
opencode-agent[bot] 64b9ba339e docs(console): prohibit abusive multi-account use (#42813)
Co-authored-by: Dax Raad <mail@thdxr.com>
2026-08-15 15:08:52 -04:00
opencode-agent[bot] 73700065bf chore: generate 2026-08-15 18:35:53 +00:00
Kit Langton 07a82a442e fix(core): keep queued work parked after interrupt (#42682) 2026-08-15 18:34:43 +00:00
Kit Langton 55be895e14 fix(tui): scope session picker by directory (#42804) 2026-08-15 18:30:35 +00:00
Kit Langton 58a36a1560 fix(tui): preserve path suffix in prompt footer (#42800) 2026-08-15 18:21:07 +00:00
Dax 3d782efee4 docs(core): fix opencode skill source link (#42792) 2026-08-15 16:37:34 +00:00
opencode-agent[bot] c054bb183e chore: generate 2026-08-15 16:30:19 +00:00
Dax 0d7904c91f refactor(core): model provider activation explicitly (#42791)
Co-authored-by: Michael Hart <mhart@cloudflare.com>
2026-08-15 16:29:09 +00:00
opencode-agent[bot] 75ec0b454c chore: generate 2026-08-15 12:13:32 +00:00
Luke Parker f4baba2824 fix(app): use fixed message page size (#42743) 2026-08-15 22:12:20 +10:00
202 changed files with 3845 additions and 3342 deletions
+10 -3
View File
@@ -1,6 +1,10 @@
name: "Setup Bun"
description: "Setup Bun with caching and install dependencies"
inputs:
bun-version:
description: "Bun version to install instead of the root packageManager version"
required: false
default: ""
install-flags:
description: "Additional flags to pass to 'bun install'"
required: false
@@ -20,19 +24,22 @@ runs:
shell: bash
run: |
if [ "$RUNNER_ARCH" = "X64" ]; then
V=$(node -p "require('./package.json').packageManager.split('@')[1]")
V="${{ inputs.bun-version }}"
if [ -z "$V" ]; then V=$(node -p "require('./package.json').packageManager.split('@')[1]"); fi
TAG=$([ "$V" = "canary" ] && echo "canary" || echo "bun-v${V}")
case "$RUNNER_OS" in
macOS) OS=darwin ;;
Linux) OS=linux ;;
Windows) OS=windows ;;
esac
echo "url=https://github.com/oven-sh/bun/releases/download/bun-v${V}/bun-${OS}-x64-baseline.zip" >> "$GITHUB_OUTPUT"
echo "url=https://github.com/oven-sh/bun/releases/download/${TAG}/bun-${OS}-x64-baseline.zip" >> "$GITHUB_OUTPUT"
fi
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: ${{ !steps.bun-url.outputs.url && 'package.json' || '' }}
bun-version: ${{ !steps.bun-url.outputs.url && inputs.bun-version || '' }}
bun-version-file: ${{ !steps.bun-url.outputs.url && !inputs.bun-version && 'package.json' || '' }}
bun-download-url: ${{ steps.bun-url.outputs.url }}
- name: Get cache directory
-37
View File
@@ -1,37 +0,0 @@
name: beta
on:
workflow_dispatch:
schedule:
- cron: "0 * * * *"
jobs:
sync:
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Setup Git Committer
id: setup-git-committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Install OpenCode
run: bun i -g opencode-ai
- name: Sync beta branch
env:
GH_TOKEN: ${{ steps.setup-git-committer.outputs.token }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
run: bun script/beta.ts
+23 -5
View File
@@ -7,6 +7,7 @@ on:
- ci
- dev
- beta
- v2
- fix/npm-native-binary-install
- snapshot-*
workflow_dispatch:
@@ -32,7 +33,7 @@ permissions:
packages: write
env:
OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && 'next') || '' }}
OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && 'dev') || '' }}
jobs:
version:
@@ -45,6 +46,13 @@ jobs:
- uses: ./.github/actions/setup-bun
- name: Deploy update service
if: github.ref_name == 'v2' || github.ref_name == 'beta'
working-directory: packages/updates
run: bun run deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
@@ -74,13 +82,15 @@ jobs:
build-cli:
needs: version
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta'
if: github.repository == 'anomalyco/opencode'
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
with:
fetch-tags: true
- uses: ./.github/actions/setup-bun
with:
bun-version: canary # Bun 1.4 until its stable release is published
- name: Setup git committer
id: committer
@@ -102,6 +112,7 @@ jobs:
id: build
run: ./packages/cli/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }}
env:
BUN_COMPILE_RELEASE: canary
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
GH_REPO: ${{ needs.version.outputs.repo }}
@@ -185,7 +196,7 @@ jobs:
build-node-cli:
needs: version
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta'
if: github.repository == 'anomalyco/opencode'
strategy:
fail-fast: false
matrix:
@@ -335,6 +346,7 @@ jobs:
build-electron:
needs:
- version
- sign-cli-macos
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2'
continue-on-error: false
env:
@@ -373,6 +385,12 @@ jobs:
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: github.ref_name == 'beta'
with:
name: opencode-preview-cli
path: packages/cli/dist
- uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2.0.0
if: runner.os == 'macOS'
with:
@@ -431,6 +449,7 @@ jobs:
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
OPENCODE_CLI_TARGET: ${{ matrix.settings.target }}
OPENCODE_CLI_DIST: ${{ (github.ref_name == 'beta' && format('{0}/packages/cli/dist', github.workspace)) || '' }}
- name: Build
run: bun run build
@@ -447,6 +466,7 @@ jobs:
VITE_SENTRY_ENVIRONMENT: ${{ (github.ref_name == 'beta' && 'beta') || 'production' }}
VITE_SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }}
OPENCODE_CLI_TARGET: ${{ matrix.settings.target }}
OPENCODE_CLI_DIST: ${{ github.workspace }}/packages/cli/dist
- name: Package
if: needs.version.outputs.release
@@ -569,13 +589,11 @@ jobs:
path: packages/opencode/dist
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: github.ref_name != 'beta'
with:
name: opencode-preview-cli
path: packages/cli/dist
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: github.ref_name != 'beta'
with:
pattern: opencode-node-cli-*
path: packages/cli/dist/node
+1 -1
View File
@@ -9,7 +9,7 @@
- Run `bun run dev:live` from a development worktree to test its TUI against the currently elected `opencode2` background server and live sessions.
- Pass a directory after the script when needed, for example `bun run dev:live /path/to/project`.
- The script discovers the server with `opencode2 service status`, injects its private local credential from `opencode2 service get password`, and uses the `next` TUI storage channel so tabs and other client-local state match the installed client.
- The script discovers the server with `opencode2 service status`, injects its private local credential from `opencode2 service get password`, and uses the `dev` TUI storage channel so tabs and other client-local state match the installed client.
- Prefer `dev:live` over plain `bun run dev` for this workflow. An implicit managed-service connection may replace the live server when the worktree client version differs; explicit `--server` warns and continues without replacing it.
## V2 TUI Stories
+218 -62
View File
@@ -1028,7 +1028,7 @@
"packages/www": {
"name": "@opencode-ai/www",
"dependencies": {
"blume": "1.1.4",
"blume": "1.5.1",
},
"devDependencies": {
"@astrojs/cloudflare": "14.1.4",
@@ -1289,6 +1289,12 @@
"@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.4", "", { "dependencies": { "yaml": "^2.8.3" } }, "sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A=="],
"@asyncapi/converter": ["@asyncapi/converter@2.0.2", "", { "dependencies": { "@asyncapi/parser": "^3.6.2", "js-yaml": "^3.14.1", "path": "^0.12.7" } }, "sha512-tFvT2ijEriTe9CPCMP32fh2VPzdQiUupK63MyW7UBDCtENANPZJ0aboZuVPhgBhmqo2TxcHWbZANpmXc5sEFiw=="],
"@asyncapi/parser": ["@asyncapi/parser@3.6.3", "", { "dependencies": { "@asyncapi/specs": "^6.11.1", "@openapi-contrib/openapi-schema-to-json-schema": "~3.2.0", "@stoplight/json": "3.21.0", "@stoplight/json-ref-readers": "^1.2.2", "@stoplight/json-ref-resolver": "^3.1.5", "@stoplight/spectral-core": "^1.18.3", "@stoplight/spectral-functions": "^1.7.2", "@stoplight/spectral-parsers": "^1.0.2", "@stoplight/spectral-ref-resolver": "^1.0.3", "@stoplight/types": "^13.12.0", "@types/json-schema": "^7.0.11", "@types/urijs": "^1.19.19", "ajv": "^8.18.0", "ajv-errors": "^3.0.0", "ajv-formats": "^2.1.1", "avsc": "^5.7.5", "js-yaml": "^4.3.1", "jsonpath-plus": "^10.0.7", "node-fetch": "2.6.7" } }, "sha512-MUC8xIUMcS2qNvqrqyx/ie0txu3d/OdIsrXs7UCzawdyR6P07gh35DpOqPz/z57s1UA3vERVpcheZYl3h8cVtw=="],
"@asyncapi/specs": ["@asyncapi/specs@6.11.1", "", { "dependencies": { "@types/json-schema": "^7.0.11" } }, "sha512-A3WBLqAKGoJ2+6FWFtpjBlCQ1oFCcs4GxF7zsIGvNqp/klGUHjlA3aAcZ9XMMpLGE8zPeYDz2x9FmO6DSuKraQ=="],
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
"@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="],
@@ -1799,6 +1805,12 @@
"@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="],
"@jsep-plugin/assignment": ["@jsep-plugin/assignment@1.3.0", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ=="],
"@jsep-plugin/regex": ["@jsep-plugin/regex@1.0.4", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg=="],
"@jsep-plugin/ternary": ["@jsep-plugin/ternary@1.1.4", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg=="],
"@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="],
"@jsx-email/body": ["@jsx-email/body@1.0.2", "", { "peerDependencies": { "react": "^18.2.0" } }, "sha512-NjR2tgLH4XGfGkm+O8kcVwi9MBqZsXZCLlmk3HlMux3/n/+a5zB+yhJqXWZBJl2i+6cSF+E2O6hK11ekyK9WWQ=="],
@@ -1985,6 +1997,8 @@
"@one-ini/wasm": ["@one-ini/wasm@0.1.1", "", {}, "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="],
"@openapi-contrib/openapi-schema-to-json-schema": ["@openapi-contrib/openapi-schema-to-json-schema@3.2.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" } }, "sha512-Gj6C0JwCr8arj0sYuslWXUBSP/KnUlEGnPW4qxlXvAl543oaNQgMgIgkQUA6vs5BCCvwTEiL8m/wdWzfl4UvSw=="],
"@openauthjs/openauth": ["@openauthjs/openauth@0.0.0-20250322224806", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-p5IWSRXvABcwocH2dNI0w8c1QJelIOFulwhKk+aLLFfUbs8u1pr7kQbYe8yCSM2+bcLHiwbogpUQc2ovrGwCuw=="],
"@opencode-ai/ai": ["@opencode-ai/ai@workspace:packages/ai"],
@@ -2585,6 +2599,8 @@
"@scalar/validation": ["@scalar/validation@0.6.2", "", {}, "sha512-Sc1TkcwGV6aVCO51AyKeaGiP8gpwAHxEtO5d3tZzPV+KsnlC/YokQxFxwBrbIXw73k9hmcExnJyGu3k5i6n6VA=="],
"@scarf/scarf": ["@scarf/scarf@1.4.0", "", {}, "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="],
"@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="],
"@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.36.0", "", { "dependencies": { "@sentry/core": "10.36.0" } }, "sha512-WILVR8HQBWOxbqLRuTxjzRCMIACGsDTo6jXvzA8rz6ezElElLmIrn3CFAswrESLqEEUa4CQHl5bLgSVJCRNweA=="],
@@ -2807,6 +2823,36 @@
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@stoplight/better-ajv-errors": ["@stoplight/better-ajv-errors@1.0.3", "", { "dependencies": { "jsonpointer": "^5.0.0", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA=="],
"@stoplight/json": ["@stoplight/json@3.21.0", "", { "dependencies": { "@stoplight/ordered-object-literal": "^1.0.3", "@stoplight/path": "^1.3.2", "@stoplight/types": "^13.6.0", "jsonc-parser": "~2.2.1", "lodash": "^4.17.21", "safe-stable-stringify": "^1.1" } }, "sha512-5O0apqJ/t4sIevXCO3SBN9AHCEKKR/Zb4gaj7wYe5863jme9g02Q0n/GhM7ZCALkL+vGPTe4ZzTETP8TFtsw3g=="],
"@stoplight/json-ref-readers": ["@stoplight/json-ref-readers@1.2.2", "", { "dependencies": { "node-fetch": "^2.6.0", "tslib": "^1.14.1" } }, "sha512-nty0tHUq2f1IKuFYsLM4CXLZGHdMn+X/IwEUIpeSOXt0QjMUbL0Em57iJUDzz+2MkWG83smIigNZ3fauGjqgdQ=="],
"@stoplight/json-ref-resolver": ["@stoplight/json-ref-resolver@3.1.6", "", { "dependencies": { "@stoplight/json": "^3.21.0", "@stoplight/path": "^1.3.2", "@stoplight/types": "^12.3.0 || ^13.0.0", "@types/urijs": "^1.19.19", "dependency-graph": "~0.11.0", "fast-memoize": "^2.5.2", "immer": "^9.0.6", "lodash": "^4.17.21", "tslib": "^2.6.0", "urijs": "^1.19.11" } }, "sha512-YNcWv3R3n3U6iQYBsFOiWSuRGE5su1tJSiX6pAPRVk7dP0L7lqCteXGzuVRQ0gMZqUl8v1P0+fAKxF6PLo9B5A=="],
"@stoplight/ordered-object-literal": ["@stoplight/ordered-object-literal@1.0.5", "", {}, "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg=="],
"@stoplight/path": ["@stoplight/path@1.3.2", "", {}, "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ=="],
"@stoplight/spectral-core": ["@stoplight/spectral-core@1.23.1", "", { "dependencies": { "@scarf/scarf": "^1.4.0", "@stoplight/better-ajv-errors": "1.0.3", "@stoplight/json": "~3.21.0", "@stoplight/path": "1.3.2", "@stoplight/spectral-parsers": "^1.0.0", "@stoplight/spectral-ref-resolver": "^1.0.4", "@stoplight/spectral-runtime": "^1.1.2", "@stoplight/types": "~13.6.0", "@types/es-aggregate-error": "^1.0.2", "@types/json-schema": "^7.0.11", "ajv": "^8.18.0", "ajv-errors": "~3.0.0", "ajv-formats": "~2.1.1", "es-aggregate-error": "^1.0.7", "expr-eval-fork": "^3.0.1", "jsonpath-plus": "^10.3.0", "lodash": "^4.18.1", "lodash.topath": "^4.5.2", "minimatch": "^3.1.4", "nimma": "0.2.3", "pony-cause": "^1.1.1", "tslib": "^2.8.1" } }, "sha512-VLC8OhpO/pMJKb6IHhurxJjXO1qB56Ng1unIb8b+hNxdw0+SEcASvmR+RpjfHYX/jv/DfSaA1x8QhFBJBmqBOQ=="],
"@stoplight/spectral-formats": ["@stoplight/spectral-formats@1.8.5", "", { "dependencies": { "@scarf/scarf": "^1.4.0", "@stoplight/json": "^3.17.0", "@stoplight/spectral-core": "^1.23.0", "@types/json-schema": "^7.0.7", "tslib": "^2.8.1" } }, "sha512-xaC0rCH0p7/bzNJsz+JgLSj+Cp6uwYGWpePQxdLkF2G6a8Zyp3OyS7umkGYNiimEwKrOjvCNNTFJpeuiENZSBA=="],
"@stoplight/spectral-functions": ["@stoplight/spectral-functions@1.10.5", "", { "dependencies": { "@scarf/scarf": "^1.4.0", "@stoplight/better-ajv-errors": "1.0.3", "@stoplight/json": "^3.17.1", "@stoplight/spectral-core": "^1.23.0", "@stoplight/spectral-formats": "^1.8.1", "@stoplight/spectral-runtime": "^1.1.2", "ajv": "^8.18.0", "ajv-draft-04": "~1.0.0", "ajv-errors": "~3.0.0", "ajv-formats": "~2.1.1", "lodash": "^4.18.1", "tslib": "^2.8.1" } }, "sha512-vDCd0NJ93715bcUpZZ5vNHiyxd4cgHF6tuXsDiXOXKAByg+I1fR5/dMijEo6Ce1Lz95a+RZ22JKYhF1YuzVvuA=="],
"@stoplight/spectral-parsers": ["@stoplight/spectral-parsers@1.0.5", "", { "dependencies": { "@stoplight/json": "~3.21.0", "@stoplight/types": "^14.1.1", "@stoplight/yaml": "~4.3.0", "tslib": "^2.8.1" } }, "sha512-ANDTp2IHWGvsQDAY85/jQi9ZrF4mRrA5bciNHX+PUxPr4DwS6iv4h+FVWJMVwcEYdpyoIdyL+SRmHdJfQEPmwQ=="],
"@stoplight/spectral-ref-resolver": ["@stoplight/spectral-ref-resolver@1.0.5", "", { "dependencies": { "@stoplight/json-ref-readers": "1.2.2", "@stoplight/json-ref-resolver": "~3.1.6", "@stoplight/spectral-runtime": "^1.1.2", "dependency-graph": "0.11.0", "tslib": "^2.8.1" } }, "sha512-gj3TieX5a9zMW29z3mBlAtDOCgN3GEc1VgZnCVlr5irmR4Qi5LuECuFItAq4pTn5Zu+sW5bqutsCH7D4PkpyAA=="],
"@stoplight/spectral-runtime": ["@stoplight/spectral-runtime@1.1.6", "", { "dependencies": { "@stoplight/json": "^3.20.1", "@stoplight/path": "^1.3.2", "@stoplight/types": "^13.6.0", "lodash": "^4.18.1", "node-fetch": "^2.7.0", "tslib": "^2.8.1" } }, "sha512-Y8rEDyMN4bSMJCrDs2shdcVHYyCnH3FvXRP4dBhha4Z8iJv+JPp7KqOV/hwVB/hWFC209upiwj2oDmLfR0qCDg=="],
"@stoplight/types": ["@stoplight/types@13.20.0", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA=="],
"@stoplight/yaml": ["@stoplight/yaml@4.3.0", "", { "dependencies": { "@stoplight/ordered-object-literal": "^1.0.5", "@stoplight/types": "^14.1.1", "@stoplight/yaml-ast-parser": "0.0.50", "tslib": "^2.2.0" } }, "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w=="],
"@stoplight/yaml-ast-parser": ["@stoplight/yaml-ast-parser@0.0.50", "", {}, "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ=="],
"@storybook/addon-a11y": ["@storybook/addon-a11y@10.5.7", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.5.7" } }, "sha512-I30rsNz6aA3xg3811MEry40uJDHP3l5SOkfqtmNkp7y4NdqTDdKhmbhhDAZQX1WWEk6GeMBGr3AJ3TCz7r7JmQ=="],
"@storybook/addon-docs": ["@storybook/addon-docs@10.5.7", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.5.7", "@storybook/icons": "^2.0.2", "@storybook/react-dom-shim": "10.5.7", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.5.7" }, "optionalPeers": ["@types/react"] }, "sha512-KNARJfjICaizinsR3INMEiipZm1ObYo+xw+E26gteu50Bcy2dIZUtk5uHY5XdtardU3AXX6yRXoBZ2HCY3lbHA=="],
@@ -3023,6 +3069,8 @@
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/es-aggregate-error": ["@types/es-aggregate-error@1.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg=="],
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
@@ -3127,6 +3175,8 @@
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@types/urijs": ["@types/urijs@1.19.26", "", {}, "sha512-wkXrVzX5yoqLnndOwFsieJA7oKM8cNkOKJtf/3vVGSUFkWDKZvFHpIl9Pvqb/T9UsawBBFMTTD8xu7sK5MWuvg=="],
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
"@types/which": ["@types/which@3.0.4", "", {}, "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w=="],
@@ -3213,6 +3263,8 @@
"@webgpu/types": ["@webgpu/types@0.1.54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="],
"@workflow/serde": ["@workflow/serde@4.1.0", "", {}, "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ=="],
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="],
"@yuuang/ffi-rs-android-arm64": ["@yuuang/ffi-rs-android-arm64@1.3.7", "", { "os": "android", "cpu": "arm64" }, "sha512-t6Wx3Xll6c07Nuk0k3xnZsxKFxlshm92i0U/BiTHc6kQbvu+fMJF+gKsj4yEj886jH51CM3EqZT9Xdhq9CdUVw=="],
@@ -3265,6 +3317,8 @@
"ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
"ajv-errors": ["ajv-errors@3.0.0", "", { "peerDependencies": { "ajv": "^8.0.1" } }, "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"ajv-i18n": ["ajv-i18n@4.2.0", "", { "peerDependencies": { "ajv": "^8.0.0-beta.0" } }, "sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg=="],
@@ -3343,6 +3397,8 @@
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"avsc": ["avsc@5.7.9", "", {}, "sha512-yOA4wFeI7ET3v32Di/sUybQ+ttP20JHSW3mxLuNGeO0uD6PPcvLrIQXSvy/rhJOWU5JrYh7U4OHplWMmtAtjMg=="],
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
"aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="],
@@ -3409,7 +3465,7 @@
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
"blume": ["blume@1.1.4", "", { "dependencies": { "@astrojs/check": "^0.9.0", "@astrojs/markdown-satteri": "^0.3.2", "@astrojs/mdx": "^7.0.0", "@astrojs/node": "^11.0.0", "@astrojs/react": "^6.0.0", "@astrojs/vercel": "^11.0.0", "@clack/prompts": "^1.7.0", "@iconify-json/lucide": "^1.2.115", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.3", "@modelcontextprotocol/sdk": "^1.29.0", "@orama/orama": "^3.1.18", "@pierre/diffs": "^1.2.11", "@scalar/astro": "^0.4.5", "@scalar/openapi-parser": "^0.28.8", "@scalar/openapi-types": "^0.9.1", "@shikijs/transformers": "^4.2.0", "@shikijs/twoslash": "^4.2.0", "@tailwindcss/typography": "^0.5.20", "@tailwindcss/vite": "^4", "@vercel/analytics": "^2.0.1", "ai": "^5.0.0", "astro": "^7.0.2", "babel-plugin-react-compiler": "^1.0.0", "citty": "^0.1.6", "consola": "^3.4.0", "dompurify": "^3.4.11", "epub-gen-memory": "^1.1.2", "github-slugger": "^2.0.0", "gray-matter": "^4.0.3", "jiti": "^2.4.0", "js-yaml": "^4.1.0", "katex": "^0.17.0", "marked": "^18.0.5", "mermaid": "^11.15.0", "node-html-parser": "^9.0.0", "pagefind": "^1.3.0", "pathe": "^2.0.0", "react": "^19.0.0", "react-dom": "^19.0.0", "satteri": "^0.9.5", "shiki": "^4.2.0", "simple-icons": "^13.0.0", "tailwindcss": "^4", "takumi-js": "^2.2.1", "tinyglobby": "^0.2.10", "twoslash": "^0.3.9", "typescript": "^6.0.3", "undici": "^8.6.0", "zod": "^3.24.0" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^1.0.41", "@astrojs/cloudflare": "^14.0.0", "@astrojs/netlify": "^8.0.0", "@astrojs/svelte": "^9.0.0", "@astrojs/vue": "^7.0.0", "@mixedbread/sdk": "^0.76.0", "@notionhq/client": "^2.2.15", "@openrouter/ai-sdk-provider": "^1.5.4", "@oramacloud/client": "^2.1.0", "@sanity/client": "^6.21.0", "algoliasearch": "^5.55.0", "flexsearch": "^0.8.0", "typesense": "^3.0.0" }, "optionalPeers": ["@ai-sdk/openai-compatible", "@astrojs/cloudflare", "@astrojs/netlify", "@astrojs/svelte", "@astrojs/vue", "@mixedbread/sdk", "@notionhq/client", "@openrouter/ai-sdk-provider", "@oramacloud/client", "@sanity/client", "algoliasearch", "flexsearch", "typesense"], "bin": { "blume": "bin/blume.mjs" } }, "sha512-boCWAMfuyc2788hjGJPrmxu4mNUojG+XkkPypjq+z8KErq1yV4XrHgET8bzVJtmOdpWVGLIjfzmbLniTePBU8Q=="],
"blume": ["blume@1.5.1", "", { "dependencies": { "@astrojs/check": "^0.9.0", "@astrojs/markdown-satteri": "^0.3.2", "@astrojs/mdx": "^7.0.0", "@astrojs/node": "^11.0.0", "@astrojs/react": "^6.0.0", "@astrojs/vercel": "^11.0.3", "@asyncapi/converter": "^2.0.2", "@clack/prompts": "^1.7.0", "@iconify-json/lucide": "^1.2.115", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.3", "@modelcontextprotocol/sdk": "^1.29.0", "@orama/orama": "^3.1.18", "@pierre/diffs": "^1.2.11", "@scalar/astro": "^0.4.5", "@scalar/openapi-parser": "^0.28.8", "@scalar/openapi-types": "^0.9.1", "@shikijs/transformers": "^4.2.0", "@shikijs/twoslash": "^4.2.0", "@tailwindcss/typography": "^0.5.20", "@tailwindcss/vite": "^4", "@types/mdast": "^4.0.4", "@vercel/analytics": "^2.0.1", "ai": "^7.0.42", "astro": "^7.1.0", "babel-plugin-react-compiler": "^1.0.0", "chokidar": "^5.0.0", "citty": "^0.1.6", "consola": "^3.4.0", "cross-spawn": "^7.0.6", "dompurify": "^3.4.13", "dotenv": "^17.4.2", "epub-gen-memory": "^1.1.2", "fast-xml-parser": "^5.10.1", "get-tsconfig": "^4.14.1", "github-slugger": "^2.0.0", "gray-matter": "^4.0.3", "html-escaper": "^3.0.3", "image-size": "^2.0.2", "jiti": "^2.4.0", "js-yaml": "^4.3.1", "katex": "^0.18.1", "markdown-table": "^3.0.4", "marked": "^18.0.5", "mdast-util-from-markdown": "^2.0.3", "mdast-util-gfm": "^3.1.0", "mdast-util-to-string": "^4.0.0", "medium-zoom": "^1.1.0", "mermaid": "^11.16.1", "micromark-extension-gfm": "^3.0.0", "nanotar": "^0.3.0", "node-html-parser": "^9.0.0", "openapi-sampler": "^1.7.4", "p-limit": "^7.3.1", "p-map": "^7.0.6", "p-retry": "^8.0.0", "package-manager-detector": "^1.8.0", "pagefind": "^1.3.0", "pathe": "^2.0.0", "perfect-debounce": "^2.1.0", "picomatch": "^4.0.5", "react": "^19.0.0", "react-dom": "^19.0.0", "robots-parser": "^3.0.1", "satteri": "^0.9.5", "semver": "^7.8.5", "sharp": "^0.35.3", "shiki": "^4.2.0", "simple-icons": "^13.0.0", "string-width": "^8.1.0", "tailwindcss": "^4.3.3", "takumi-js": "^2.2.1", "tinyglobby": "^0.2.10", "twoslash": "^0.3.9", "typescript": "^6.0.3", "ufo": "^1.6.4", "undici": "^8.9.0", "write-file-atomic": "^8.0.0", "zod": "^4.3.6" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^3.0.0", "@astrojs/cloudflare": "^14.0.0", "@astrojs/netlify": "^8.0.0", "@astrojs/svelte": "^9.0.0", "@astrojs/vue": "^7.0.0", "@mixedbread/sdk": "^0.76.0", "@notionhq/client": "^2.2.15", "@openrouter/ai-sdk-provider": "^3.0.0", "@oramacloud/client": "^2.1.0", "@sanity/client": "^6.21.0 || ^7.0.0", "algoliasearch": "^5.55.0", "flexsearch": "^0.8.0", "typesense": "^3.0.0" }, "optionalPeers": ["@ai-sdk/openai-compatible", "@astrojs/cloudflare", "@astrojs/netlify", "@astrojs/svelte", "@astrojs/vue", "@mixedbread/sdk", "@notionhq/client", "@openrouter/ai-sdk-provider", "@oramacloud/client", "@sanity/client", "algoliasearch", "flexsearch", "typesense"], "bin": { "blume": "bin/blume.mjs" } }, "sha512-RnzoY+OHvchUJkXjF7s45SUW5rhuL81URWATEZoEl3psWeajsUu6JRyqDuy8bpYZjoth7P6gpS4IijZspv8Yrw=="],
"body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="],
@@ -3737,6 +3793,8 @@
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"dependency-graph": ["dependency-graph@0.11.0", "", {}, "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg=="],
"deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
@@ -3791,7 +3849,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.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="],
@@ -3877,6 +3935,8 @@
"es-abstract-get": ["es-abstract-get@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "es-object-atoms": "^1.1.2", "is-callable": "^1.2.7", "object-inspect": "^1.13.4" } }, "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg=="],
"es-aggregate-error": ["es-aggregate-error@1.0.14", "", { "dependencies": { "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "globalthis": "^1.0.4", "has-property-descriptors": "^1.0.2", "set-function-name": "^2.0.2" } }, "sha512-3YxX6rVb07B5TV11AV5wsL7nQCHXNwoHPsQC8S4AmBiqYhyNCJ5BRKXkXyDJvs8QzXN20NgRtxe3dEEQD9NLHA=="],
"es-array-method-boxes-properly": ["es-array-method-boxes-properly@1.0.0", "", {}, "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
@@ -3951,6 +4011,8 @@
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
"expr-eval-fork": ["expr-eval-fork@3.0.3", "", {}, "sha512-BhC+hbc5lIVjygr840n5DEkW3MQq7H9o+mc1/N7Z5uIiCFVyESLL5DIE7LNq4CYUNxy+XjA+3jRrL/h0Kt2xcg=="],
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"express-rate-limit": ["express-rate-limit@8.6.2", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A=="],
@@ -3977,6 +4039,8 @@
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-memoize": ["fast-memoize@2.5.2", "", {}, "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw=="],
"fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
"fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
@@ -3987,7 +4051,7 @@
"fast-xml-builder": ["fast-xml-builder@1.3.0", "", { "dependencies": { "path-expression-matcher": "^1.6.2", "xml-naming": "^0.3.0" } }, "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ=="],
"fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="],
"fast-xml-parser": ["fast-xml-parser@5.10.1", "", { "dependencies": { "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", "is-unsafe": "^2.0.0", "path-expression-matcher": "^1.6.2", "strnum": "^2.4.1", "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw=="],
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
@@ -4027,6 +4091,8 @@
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"foreach": ["foreach@2.0.6", "", {}, "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg=="],
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="],
@@ -4243,6 +4309,8 @@
"ignore-walk": ["ignore-walk@8.0.0", "", { "dependencies": { "minimatch": "^10.0.3" } }, "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A=="],
"image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="],
"immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
"immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
@@ -4329,6 +4397,8 @@
"is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
"is-network-error": ["is-network-error@1.3.2", "", {}, "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
@@ -4401,6 +4471,8 @@
"jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="],
"jsep": ["jsep@1.4.0", "", {}, "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
@@ -4409,6 +4481,8 @@
"json-parse-even-better-errors": ["json-parse-even-better-errors@5.0.0", "", {}, "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ=="],
"json-pointer": ["json-pointer@0.6.2", "", { "dependencies": { "foreach": "^2.0.4" } }, "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw=="],
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
@@ -4431,6 +4505,8 @@
"jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="],
"jsonpath-plus": ["jsonpath-plus@10.4.0", "", { "dependencies": { "@jsep-plugin/assignment": "^1.3.0", "@jsep-plugin/regex": "^1.0.4", "jsep": "^1.4.0" }, "bin": { "jsonpath": "bin/jsonpath-cli.js", "jsonpath-plus": "bin/jsonpath-cli.js" } }, "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA=="],
"jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="],
"jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="],
@@ -4529,6 +4605,8 @@
"lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
"lodash.topath": ["lodash.topath@4.5.2", "", {}, "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg=="],
"loglevelnext": ["loglevelnext@6.0.0", "", {}, "sha512-FDl1AI2sJGjHHG3XKJd6sG3/6ncgiGCQ0YkW46nxe7SfqQq6hujd9CvFXIXtkGBUN83KPZ2KSOJK8q5P0bSSRQ=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
@@ -4613,6 +4691,8 @@
"media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="],
"medium-zoom": ["medium-zoom@1.1.0", "", {}, "sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ=="],
"merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
@@ -4767,12 +4847,16 @@
"nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
"nanotar": ["nanotar@0.3.0", "", {}, "sha512-Kv2JYYiCzt16Kt5QwAc9BFG89xfPNBx+oQL4GQXD9nLqPkZBiNaqaCWtwnbk/q7UVsTYevvM1b0UF8zmEI4pCg=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="],
"nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="],
"nimma": ["nimma@0.2.3", "", { "dependencies": { "@jsep-plugin/regex": "^1.0.1", "@jsep-plugin/ternary": "^1.0.2", "astring": "^1.8.1", "jsep": "^1.2.0" }, "optionalDependencies": { "jsonpath-plus": "^6.0.1 || ^10.1.0", "lodash.topath": "^4.5.2" } }, "sha512-1ZOI8J+1PKKGceo/5CT5GfQOG6H8I2BencSK06YarZ2wXwH37BSSUWldqJmMJYA5JfqDqffxDXynt6f11AyKcA=="],
"nitro": ["nitro@3.0.1-alpha.1", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.1", "db0": "^0.3.4", "h3": "2.0.1-rc.5", "jiti": "^2.6.1", "nf3": "^0.1.10", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.96.0", "oxc-transform": "^0.96.0", "srvx": "^0.9.5", "undici": "^7.16.0", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.4" }, "peerDependencies": { "rolldown": "*", "rollup": "^4", "vite": "^7", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-U4AxIsXxdkxzkFrK0XAw0e5Qbojk8jQ50MjjRBtBakC4HurTtQoiZvF+lSe382jhuQZCfAyywGWOFa9QzXLFaw=="],
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
@@ -4861,6 +4945,8 @@
"openai": ["openai@6.49.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-aYCc0C6L864eR6WSYIwQGyXriw/nIyZx0ObvhzOEVuk0zoBDpynjSbrionWI7q65B5H8jJX0DXR9snEzM6bfPg=="],
"openapi-sampler": ["openapi-sampler@1.7.4", "", { "dependencies": { "@types/json-schema": "^7.0.7", "fast-xml-parser": "^5.5.1", "json-pointer": "0.6.2" } }, "sha512-CKS/rd5ucPCuEDbJnjGDXZTsuGWcmv53aCmQx7soZlPEONUGN4af0/dY5+THRFZraSEjeA78nlfzdFswC/N5SA=="],
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
"openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="],
@@ -4899,7 +4985,7 @@
"p-queue": ["p-queue@8.1.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="],
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
"p-retry": ["p-retry@8.0.0", "", { "dependencies": { "is-network-error": "^1.3.0" } }, "sha512-kFVqH1HxOHp8LupNsOys7bSV09VYTRLxarH/mokO4Rqhk6wGi70E0jh4VzvVGXfEVNggHoHLAMWsQqHyU1Ey9A=="],
"p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="],
@@ -4933,6 +5019,8 @@
"pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="],
"path": ["path@0.12.7", "", { "dependencies": { "process": "^0.11.1", "util": "^0.10.3" } }, "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
@@ -4965,6 +5053,8 @@
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
"perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="],
"piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
@@ -4993,6 +5083,8 @@
"points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
"pony-cause": ["pony-cause@1.1.1", "", {}, "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="],
@@ -5221,6 +5313,8 @@
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
"robots-parser": ["robots-parser@3.0.1", "", {}, "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ=="],
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
"rolldown": ["rolldown@1.2.3", "", { "dependencies": { "@oxc-project/types": "=0.143.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.3", "@rolldown/binding-darwin-arm64": "1.2.3", "@rolldown/binding-darwin-x64": "1.2.3", "@rolldown/binding-freebsd-x64": "1.2.3", "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", "@rolldown/binding-linux-arm64-gnu": "1.2.3", "@rolldown/binding-linux-arm64-musl": "1.2.3", "@rolldown/binding-linux-ppc64-gnu": "1.2.3", "@rolldown/binding-linux-s390x-gnu": "1.2.3", "@rolldown/binding-linux-x64-gnu": "1.2.3", "@rolldown/binding-linux-x64-musl": "1.2.3", "@rolldown/binding-openharmony-arm64": "1.2.3", "@rolldown/binding-win32-arm64-msvc": "1.2.3", "@rolldown/binding-win32-x64-msvc": "1.2.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A=="],
@@ -5249,6 +5343,8 @@
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
"safe-stable-stringify": ["safe-stable-stringify@1.1.1", "", {}, "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"sanitize-filename": ["sanitize-filename@1.6.4", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg=="],
@@ -5459,7 +5555,7 @@
"stripe": ["stripe@18.0.0", "", { "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.11.0" } }, "sha512-3Fs33IzKUby//9kCkCa1uRpinAoTvj6rJgQ2jrBEysoxEvfsclvXdna1amyEYbA2EKkjynuB4+L/kleCCaWTpA=="],
"strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
"strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="],
"stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="],
@@ -5681,6 +5777,8 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"urijs": ["urijs@1.19.11", "", {}, "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ=="],
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
@@ -5689,8 +5787,12 @@
"utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="],
"util": ["util@0.10.4", "", { "dependencies": { "inherits": "2.0.3" } }, "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="],
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
"uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="],
@@ -5807,7 +5909,7 @@
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"write-file-atomic": ["write-file-atomic@7.0.1", "", { "dependencies": { "signal-exit": "^4.0.1" } }, "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg=="],
"write-file-atomic": ["write-file-atomic@8.0.0", "", { "dependencies": { "signal-exit": "^4.0.1" } }, "sha512-dYwyZredl67GyLLIHJnRM3h2PcOmN5SkcgC7eM5DPDEOEl6dLFqVrMg3F1Ea32usj4VSVZtd2H4MtKTNOf6nPg=="],
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
@@ -5999,6 +6101,12 @@
"@astrojs/vercel/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
"@asyncapi/parser/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"@asyncapi/parser/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
"@asyncapi/parser/node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="],
"@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="],
"@aws-crypto/crc32c/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="],
@@ -6117,8 +6225,6 @@
"@azure/core-http/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
"@azure/core-xml/fast-xml-parser": ["fast-xml-parser@5.10.1", "", { "dependencies": { "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", "is-unsafe": "^2.0.0", "path-expression-matcher": "^1.6.2", "strnum": "^2.4.1", "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw=="],
"@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=="],
@@ -6325,6 +6431,8 @@
"@scalar/types/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@sentry/bundler-plugin-core/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"@sentry/bundler-plugin-core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
"@sentry/bundler-plugin-core/magic-string": ["magic-string@0.30.8", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ=="],
@@ -6365,6 +6473,8 @@
"@slack/web-api/p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="],
"@slack/web-api/p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
"@solidjs/start/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
"@solidjs/start/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
@@ -6373,6 +6483,26 @@
"@solidjs/start/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="],
"@stoplight/better-ajv-errors/leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
"@stoplight/json/jsonc-parser": ["jsonc-parser@2.2.1", "", {}, "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w=="],
"@stoplight/json-ref-readers/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
"@stoplight/json-ref-resolver/immer": ["immer@9.0.21", "", {}, "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA=="],
"@stoplight/spectral-core/@stoplight/types": ["@stoplight/types@13.6.0", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ=="],
"@stoplight/spectral-core/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"@stoplight/spectral-core/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"@stoplight/spectral-functions/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"@stoplight/spectral-parsers/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="],
"@stoplight/yaml/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="],
"@storybook/addon-docs/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
"@storybook/addon-docs/react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
@@ -6435,6 +6565,8 @@
"app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
"app-builder-lib/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"app-builder-lib/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
"app-builder-lib/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
@@ -6473,6 +6605,8 @@
"babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
"bin-links/write-file-atomic": ["write-file-atomic@7.0.1", "", { "dependencies": { "signal-exit": "^4.0.1" } }, "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg=="],
"blume/@astrojs/mdx": ["@astrojs/mdx@7.0.5", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.2", "@astrojs/markdown-remark": "7.2.2", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.16.0", "es-module-lexer": "^2.0.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@astrojs/markdown-satteri": "^0.3.1", "astro": "^7.0.0" }, "optionalPeers": ["@astrojs/markdown-satteri"] }, "sha512-wEM/HH1RiEntyPVagdiF+yArzfcYLKBB0C1RZspVidKZ97rRMbaqP1Nbl/GR0sJs8zwaceqxRymw8aOKKJRdYw=="],
"blume/@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="],
@@ -6481,23 +6615,35 @@
"blume/@shikijs/transformers": ["@shikijs/transformers@4.4.2", "", { "dependencies": { "@shikijs/core": "4.4.2", "@shikijs/types": "4.4.2" } }, "sha512-d81PJ9KkR1tVP95FH/9296HTtDo0mh76wv10u9T1YmsZq/UcXgt0OLdBszfUQ1i+umkRMCjDnFbFZU7/tCODTQ=="],
"blume/ai": ["ai@7.0.64", "", { "dependencies": { "@ai-sdk/gateway": "4.0.51", "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-29Ufm56e53pRzIPueWzZiwJsgxmZStaBFpwEkVM89sqyIIax/pzDm+ez/ksJ/CcY9UCuYzvCQpE2zkFHICvS3w=="],
"blume/astro": ["astro@7.1.3", "", { "dependencies": { "@astrojs/compiler-rs": "^0.3.1", "@astrojs/internal-helpers": "0.10.1", "@astrojs/markdown-satteri": "0.3.4", "@astrojs/telemetry": "3.3.3", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^2.0.1", "devalue": "^5.8.1", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.28.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^1.0.1", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unstorage": "^1.17.5", "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0 || ^0.35.0" }, "peerDependencies": { "@astrojs/markdown-remark": "7.2.1" }, "optionalPeers": ["@astrojs/markdown-remark"], "bin": { "astro": "./bin/astro.mjs" } }, "sha512-4dhPyAAXthf3xLEYnG8SeL7yr/nTPPABfY7e9YF0yuO+vK9Xp+8Q5j4xzsmL3GueukQv4oNwGNTBepLOiDGeJA=="],
"blume/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"blume/dompurify": ["dompurify@3.4.13", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ=="],
"blume/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
"blume/katex": ["katex@0.17.0", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw=="],
"blume/katex": ["katex@0.18.4", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-IMPntbRLOU+eu88XDiFKqQ8Akhr9Tv7jDMXqPhjG9SI1JMA4DIgXk4x9k4skJz2NZJXBRbC+2pYBLj9olqcZow=="],
"blume/node-html-parser": ["node-html-parser@9.0.1", "", { "dependencies": { "css-select": "^5.1.0", "entities": "^8.0.0" } }, "sha512-QrdiYYm1NnLRXsMXThUgVcF/syWfWIgHFmy8hylWMGFbHFtnRuXLBxxGQvIm3xaIJMUDJ0ayOmT/FGJYT3pZIw=="],
"blume/p-limit": ["p-limit@7.3.1", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-0trZaiG7Y7kN/Egy9a8j47t9osC0Tch4PaIWd9yGF6bvmlk7muExRvGNYb8sXBwEKMoNKsbNN9P8EefuQekE4Q=="],
"blume/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
"blume/react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
"blume/sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="],
"blume/string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
"blume/tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
"blume/typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"blume/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"blume/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
@@ -6545,6 +6691,8 @@
"dot-prop/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="],
"dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"duplexer2/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=="],
"editorconfig/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
@@ -6685,8 +6833,6 @@
"p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
@@ -6785,6 +6931,8 @@
"unzipper/fs-extra": ["fs-extra@11.3.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g=="],
"util/inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="],
"venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.42" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-oMh5/bhu2ibsXqzHeLbJ+9Y7UQdOSrfOLPI+UA1vEcqXGYVBQfWxLe5jBUpsaL8GBBj/nmSeYGX6QPan8RfHiQ=="],
"venice-ai-sdk-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="],
@@ -7019,6 +7167,8 @@
"@astrojs/vercel/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
"@asyncapi/parser/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
@@ -7033,6 +7183,8 @@
"@aws-sdk/client-lambda/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="],
"@aws-sdk/client-sts/@aws-sdk/core/fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="],
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6ESVxwCbGm7WZ17kY1fjmxQud43vzJFoLd4bmlR+idQSWdqlzGDYdcfzpjDKTcivdtNrVYmFvcH1JBUwCRAZhw=="],
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/util-stream": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-PjDQeDH/J1S0yWV32wCj2k5liRo0ssXMseCBEkCsD3SqsU8o5cU82b0hMX4sAib/RkglCSZqGO0xMiN0/7ndww=="],
@@ -7091,10 +7243,6 @@
"@aws-sdk/token-providers/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="],
"@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="],
"@azure/core-xml/fast-xml-parser/strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="],
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@bruits/satteri-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
@@ -7413,6 +7561,8 @@
"@slack/web-api/p-queue/p-timeout": ["p-timeout@3.2.0", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="],
"@slack/web-api/p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
"@solidjs/start/shiki/@shikijs/core": ["@shikijs/core@1.29.2", "", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ=="],
"@solidjs/start/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "oniguruma-to-es": "^2.2.0" } }, "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A=="],
@@ -7425,6 +7575,8 @@
"@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
"@stoplight/spectral-core/minimatch/brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="],
"@storybook/addon-docs/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"@storybook/csf-plugin/unplugin/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
@@ -7515,6 +7667,12 @@
"blume/@shikijs/transformers/@shikijs/types": ["@shikijs/types@4.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-PFYitV4vpDr/iPCIhnHp+Q4ftic5N5VeNJ3KQ1O8gn3h2ar8qgwMAXF7tq4m1CWaMS60fV4VqF6vfnWH4F7vqQ=="],
"blume/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.51", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tQxWGUbg/O3A5EgekJo/C2byLVQ1r+vL6QdEVWmxUSnPhdCupcOeDbQO1tv9Um40VrdwYuSWlYzcbWvtlG5bOA=="],
"blume/ai/@ai-sdk/provider": ["@ai-sdk/provider@4.0.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q=="],
"blume/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.27", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EzAn4pdgG5g0xXtH6lE2zyNmfjDQIDjATkfqzuidEI35g++hh4+07vnjzkT/RmGmIClPZiRj/Q2GMPV2V7mkHw=="],
"blume/astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.1", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q=="],
"blume/astro/@astrojs/telemetry": ["@astrojs/telemetry@3.3.3", "", { "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", "package-manager-detector": "^1.6.0" } }, "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ=="],
@@ -7535,12 +7693,8 @@
"blume/astro/neotraverse": ["neotraverse@1.0.1", "", {}, "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w=="],
"blume/astro/p-limit": ["p-limit@7.3.1", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-0trZaiG7Y7kN/Egy9a8j47t9osC0Tch4PaIWd9yGF6bvmlk7muExRvGNYb8sXBwEKMoNKsbNN9P8EefuQekE4Q=="],
"blume/astro/p-queue": ["p-queue@9.3.3", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA=="],
"blume/astro/sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="],
"blume/astro/unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="],
"blume/astro/unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "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 || ^7 || ^8", "@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 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "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", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="],
@@ -7549,7 +7703,7 @@
"blume/astro/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
"blume/astro/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"blume/chokidar/readdirp": ["readdirp@5.1.1", "", {}, "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="],
"blume/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
@@ -7559,6 +7713,42 @@
"blume/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"blume/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.2" }, "os": "darwin", "cpu": "arm64" }, "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg=="],
"blume/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.2" }, "os": "darwin", "cpu": "x64" }, "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w=="],
"blume/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg=="],
"blume/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw=="],
"blume/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ=="],
"blume/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA=="],
"blume/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ=="],
"blume/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w=="],
"blume/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw=="],
"blume/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ=="],
"blume/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.2" }, "os": "linux", "cpu": "arm" }, "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA=="],
"blume/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ=="],
"blume/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.2" }, "os": "linux", "cpu": "s390x" }, "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw=="],
"blume/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA=="],
"blume/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w=="],
"blume/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="],
"blume/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw=="],
"blume/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="],
"builder-util/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -8115,6 +8305,8 @@
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
"@aws-sdk/client-sts/@aws-sdk/core/fast-xml-parser/strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="],
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-5GlJBejo8wqMpSSEKb45WE82YxI2k73YuebjLH/eWDNQeE6VI5Bh9lA1YQ7xNkLLH8hIsb0pSfKVuwh0VEzVrg=="],
@@ -8479,6 +8671,8 @@
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
"@stoplight/spectral-core/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
@@ -8505,6 +8699,8 @@
"blume/@shikijs/transformers/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.4.2", "", { "dependencies": { "@shikijs/types": "4.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-l6fQQKsOMlz72n38fztmSgZ76MO6KSWuw8o+GJ+FhmqrpC9pIOJNQNXGgbb5yX2AwpzlEHwsaLPnk/8o4Fm+rA=="],
"blume/ai/@ai-sdk/provider-utils/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
"blume/astro/@astrojs/telemetry/is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="],
"blume/astro/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
@@ -8561,46 +8757,8 @@
"blume/astro/p-queue/p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="],
"blume/astro/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.2" }, "os": "darwin", "cpu": "arm64" }, "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg=="],
"blume/astro/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.2" }, "os": "darwin", "cpu": "x64" }, "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w=="],
"blume/astro/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg=="],
"blume/astro/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw=="],
"blume/astro/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ=="],
"blume/astro/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA=="],
"blume/astro/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ=="],
"blume/astro/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w=="],
"blume/astro/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw=="],
"blume/astro/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ=="],
"blume/astro/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.2" }, "os": "linux", "cpu": "arm" }, "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA=="],
"blume/astro/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ=="],
"blume/astro/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.2" }, "os": "linux", "cpu": "s390x" }, "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw=="],
"blume/astro/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA=="],
"blume/astro/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w=="],
"blume/astro/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="],
"blume/astro/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw=="],
"blume/astro/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="],
"blume/astro/unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
"blume/astro/unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"blume/astro/unstorage/h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="],
"blume/astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
@@ -9059,8 +9217,6 @@
"babel-plugin-module-resolver/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"blume/astro/unstorage/chokidar/readdirp": ["readdirp@5.1.1", "", {}, "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="],
"blume/astro/unstorage/h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="],
"blume/astro/unstorage/h3/crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
+1 -1
View File
@@ -2,7 +2,7 @@
exact = true
# Only install newly resolved package versions published at least 3 days ago.
minimumReleaseAge = 259200
minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@opencode-ai/sdk", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish"]
minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@opencode-ai/sdk", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish", "blume"]
[test]
root = "./do-not-run-tests-from-root"
+4 -4
View File
@@ -15,13 +15,13 @@ Usage: install.sh [options]
Options:
-h, --help Display this help message
-v, --version <version> Install a specific version (e.g., 0.0.0-next-17236)
-v, --version <version> Install a specific version (e.g., 0.0.0-beta-17236)
-b, --binary <path> Install from a local binary instead of downloading
--no-modify-path Don't modify shell config files (.zshrc, .bashrc, etc.)
Examples:
curl -fsSL https://raw.githubusercontent.com/anomalyco/opencode/v2/install | bash
curl -fsSL https://raw.githubusercontent.com/anomalyco/opencode/v2/install | bash -s -- --version 0.0.0-next-17236
curl -fsSL https://opencode.ai/v2/install | bash
curl -fsSL https://opencode.ai/v2/install | bash -s -- --version 0.0.0-beta-17236
./install --binary /path/to/opencode2
EOF
}
@@ -166,7 +166,7 @@ else
fi
if [ -z "$requested_version" ]; then
metadata=$(curl -fsSL https://registry.npmjs.org/@opencode-ai%2fcli/next || true)
metadata=$(curl -fsSL https://registry.npmjs.org/@opencode-ai%2fcli/beta || true)
specific_version=$(echo "$metadata" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')
if [ -z "$specific_version" ]; then
+1 -1
View File
@@ -8,7 +8,7 @@
"packageManager": "bun@1.3.14",
"scripts": {
"dev": "bun run --cwd packages/cli --conditions=browser src/index.ts",
"dev:live": "OPENCODE_TUI_CHANNEL=next OPENCODE_PASSWORD=\"$(opencode2 service get password)\" bun run dev --server \"$(opencode2 service status)\"",
"dev:live": "OPENCODE_TUI_CHANNEL=dev OPENCODE_PASSWORD=\"$(opencode2 service get password)\" bun run dev --server \"$(opencode2 service status)\"",
"dev:desktop": "bun --cwd packages/desktop dev",
"dev:web": "bun --cwd packages/app dev",
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
@@ -30,7 +30,6 @@ import { ToolSchemaProjection } from "./utils/tool-schema.js"
import { ToolStream } from "./utils/tool-stream.js"
const ADAPTER = "anthropic-messages"
const MEDIA_MIMES = new Set<string>([...ProviderShared.IMAGE_MIMES, ...ProviderShared.PDF_MIMES])
export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1"
export const PATH = "/messages"
@@ -400,7 +399,7 @@ const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult
})
const lowerMedia = Effect.fn("AnthropicMessages.lowerMedia")(function* (part: MediaPart) {
const media = yield* ProviderShared.validateMedia("Anthropic Messages", part, MEDIA_MIMES)
const media = ProviderShared.normalizeMedia(part)
if (media.mime === "application/pdf")
return {
type: "document" as const,
@@ -410,6 +409,8 @@ const lowerMedia = Effect.fn("AnthropicMessages.lowerMedia")(function* (part: Me
data: media.base64,
},
} satisfies AnthropicDocumentBlock
if (!media.mime.startsWith("image/"))
return yield* invalid(`Anthropic Messages does not support media type ${part.mediaType}`)
return {
type: "image" as const,
source: {
+2 -3
View File
@@ -24,7 +24,6 @@ import { Lifecycle } from "./utils/lifecycle.js"
import { ToolSchemaProjection } from "./utils/tool-schema.js"
const ADAPTER = "gemini"
const MEDIA_MIMES = new Set<string>(ProviderShared.MEDIA_MIMES)
// Google documents this sentinel for replaying Gemini 3 function calls after their original signature was lost.
const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator"
export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
@@ -248,7 +247,7 @@ const lowerToolConfig = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
const lowerUserPart = Effect.fn("Gemini.lowerUserPart")(function* (part: TextPart | MediaPart) {
if (part.type === "text") return { text: part.text }
const media = yield* ProviderShared.validateMedia("Gemini", part, MEDIA_MIMES)
const media = ProviderShared.normalizeMedia(part)
return { inlineData: { mimeType: media.mime, data: media.base64 } }
})
@@ -353,7 +352,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
const media: GeminiInlineDataPart[] = []
for (const item of content) {
if (item.type === "text") continue
const value = yield* ProviderShared.validateToolFile("Gemini", item, MEDIA_MIMES)
const value = ProviderShared.normalizeToolFile(item)
media.push({ inlineData: { mimeType: value.mime, data: value.base64 } })
}
parts.push({
+4 -5
View File
@@ -26,7 +26,6 @@ import { ToolStream } from "./utils/tool-stream.js"
const ADAPTER = "open-responses"
const NAME = "Open Responses"
const MEDIA_MIMES = new Set<string>([...ProviderShared.IMAGE_MIMES, ...ProviderShared.PDF_MIMES])
export const PATH = "/responses"
// =============================================================================
@@ -285,7 +284,7 @@ export interface Extension {
readonly name: string
readonly lowerMedia?: (input: {
readonly part: MediaPart
readonly media: ProviderShared.ValidatedMedia
readonly media: ProviderShared.NormalizedMedia
readonly request: LLMRequest
}) => MediaInput | undefined
readonly messagePhase?: (value: unknown) => MessagePhase | null | undefined
@@ -380,13 +379,13 @@ const lowerMedia = Effect.fn("OpenResponses.lowerMedia")(function* (
request: LLMRequest,
extension: Extension,
) {
const media = yield* ProviderShared.validateMedia(extension.name, part, MEDIA_MIMES)
const media = ProviderShared.normalizeMedia(part)
const extended = extension.lowerMedia?.({ part, media, request })
if (extended) return extended
if (media.mime === "application/pdf") {
if (!media.mime.startsWith("image/")) {
return {
type: "input_file" as const,
filename: part.filename ?? "document.pdf",
filename: part.filename ?? (media.mime === "application/pdf" ? "document.pdf" : "file"),
file_data: media.dataUrl,
}
}
+3 -2
View File
@@ -28,7 +28,6 @@ import { ToolSchemaProjection } from "./utils/tool-schema.js"
import { ToolStream } from "./utils/tool-stream.js"
const ADAPTER = "openai-chat"
const IMAGE_MIMES = new Set<string>(ProviderShared.IMAGE_MIMES)
const RESERVED_REASONING_FIELDS = new Set(["role", "content", "tool_calls"])
export const DEFAULT_BASE_URL = "https://api.openai.com/v1"
export const PATH = "/chat/completions"
@@ -284,7 +283,9 @@ const lowerToolCall = (part: ToolCallPart): OpenAIChatAssistantToolCall => ({
})
const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart) {
const media = yield* ProviderShared.validateMedia("OpenAI Chat", part, IMAGE_MIMES)
const media = ProviderShared.normalizeMedia(part)
if (!media.mime.startsWith("image/"))
return yield* ProviderShared.invalidRequest(`OpenAI Chat does not support media type ${part.mediaType}`)
return { type: "image_url" as const, image_url: { url: media.dataUrl } }
})
+9 -44
View File
@@ -155,59 +155,24 @@ export const wrappedSystemUpdate = Effect.fn("ProviderShared.wrappedSystemUpdate
export const parseToolInput = (route: string, name: string, raw: string) =>
parseJson(route, raw || "{}", `Invalid JSON input for ${route} tool call ${name}`)
export const IMAGE_MIMES = ["image/png", "image/jpeg", "image/gif", "image/webp"] as const
export const VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"] as const
export const AUDIO_MIMES = ["audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac"] as const
export const PDF_MIMES = ["application/pdf"] as const
export const MEDIA_MIMES = [...IMAGE_MIMES, ...VIDEO_MIMES, ...AUDIO_MIMES, ...PDF_MIMES] as const
export const MAX_MEDIA_ENCODED_BYTES = 28 * 1024 * 1024
export const MAX_MEDIA_DECODED_BYTES = 20 * 1024 * 1024
const base64Pattern = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
export interface ValidatedMedia {
export interface NormalizedMedia {
readonly mime: string
readonly base64: string
readonly dataUrl: string
readonly bytes: Uint8Array
}
export const validateMedia = Effect.fn("ProviderShared.validateMedia")(function* (
route: string,
part: MediaPart,
supportedMimes: ReadonlySet<string>,
) {
export const normalizeMedia = (part: MediaPart): NormalizedMedia => {
const mime = part.mediaType.toLowerCase()
if (!supportedMimes.has(mime)) return yield* invalidRequest(`${route} does not support media type ${part.mediaType}`)
let base64: string
if (typeof part.data !== "string") {
if (part.data.byteLength > MAX_MEDIA_DECODED_BYTES)
return yield* invalidRequest(`${route} media exceeds the ${MAX_MEDIA_DECODED_BYTES} byte decoded limit`)
base64 = Buffer.from(part.data).toString("base64")
} else if (part.data.startsWith("data:")) {
const match = /^data:([^;,]+);base64,([A-Za-z0-9+/]*={0,2})$/s.exec(part.data)
if (!match) return yield* invalidRequest(`${route} media data URL must contain valid base64`)
if (match[1]!.toLowerCase() !== mime)
return yield* invalidRequest(`${route} media type ${part.mediaType} does not match data URL type ${match[1]}`)
base64 = match[2]!
} else {
base64 = part.data
const base64 = Buffer.from(part.data).toString("base64")
return { mime, base64, dataUrl: `data:${mime};base64,${base64}` }
}
if (!part.data.startsWith("data:")) return { mime, base64: part.data, dataUrl: `data:${mime};base64,${part.data}` }
return { mime, base64: part.data.slice(part.data.indexOf(",") + 1), dataUrl: part.data }
}
if (Buffer.byteLength(base64, "utf8") > MAX_MEDIA_ENCODED_BYTES)
return yield* invalidRequest(`${route} media exceeds the ${MAX_MEDIA_ENCODED_BYTES} byte encoded limit`)
if (!base64 || base64.length % 4 !== 0 || !base64Pattern.test(base64))
return yield* invalidRequest(`${route} media must contain valid base64`)
const bytes = Buffer.from(base64, "base64")
if (bytes.byteLength > MAX_MEDIA_DECODED_BYTES)
return yield* invalidRequest(`${route} media exceeds the ${MAX_MEDIA_DECODED_BYTES} byte decoded limit`)
if (bytes.toString("base64") !== base64) return yield* invalidRequest(`${route} media must contain canonical base64`)
return { mime, base64, dataUrl: `data:${mime};base64,${base64}`, bytes } satisfies ValidatedMedia
})
export const validateToolFile = (route: string, part: Tool.FileContent, supportedMimes: ReadonlySet<string>) =>
validateMedia(route, { type: "media", mediaType: part.mime, data: part.uri, filename: part.name }, supportedMimes)
export const normalizeToolFile = (part: Tool.FileContent) =>
normalizeMedia({ type: "media", mediaType: part.mime, data: part.uri, filename: part.name })
export const trimBaseUrl = (value: string) => value.replace(/\/+$/, "")
@@ -66,11 +66,7 @@ export const lower = Effect.fn("BedrockMedia.lower")(function* (part: MediaPart)
const mime = part.mediaType.toLowerCase()
const imageFormat = IMAGE_FORMATS[mime as keyof typeof IMAGE_FORMATS]
if (imageFormat) {
const media = yield* ProviderShared.validateMedia(
"Bedrock Converse",
part,
new Set<string>(Object.keys(IMAGE_FORMATS)),
)
const media = ProviderShared.normalizeMedia(part)
return { image: { format: imageFormat, source: { bytes: media.base64 } } } satisfies ImageBlock
}
if (mime.startsWith("image/"))
@@ -79,11 +75,7 @@ export const lower = Effect.fn("BedrockMedia.lower")(function* (part: MediaPart)
if (documentFormat) {
if (!part.filename)
return yield* ProviderShared.invalidRequest("Bedrock Converse document media requires a filename")
const media = yield* ProviderShared.validateMedia(
"Bedrock Converse",
part,
new Set<string>(Object.keys(DOCUMENT_FORMATS)),
)
const media = ProviderShared.normalizeMedia(part)
return documentBlock(part.filename, documentFormat, media.base64)
}
return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support media type ${part.mediaType}`)
@@ -399,7 +399,7 @@ describe("Anthropic Messages route", () => {
}),
)
it.effect("rejects unsupported media in tool-result content with a clear error", () =>
it.effect("rejects tool-result media that cannot be lowered", () =>
Effect.gen(function* () {
const error = yield* compileRequest(
LLM.request({
@@ -418,8 +418,7 @@ describe("Anthropic Messages route", () => {
}),
).pipe(Effect.flip)
expect(error.message).toContain("Anthropic Messages")
expect(error.message).toContain("audio/mpeg")
expect(error.message).toContain("Anthropic Messages does not support media type audio/mpeg")
}),
)
+18 -24
View File
@@ -4,7 +4,6 @@ import { LLM, AIError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage
import { Auth, LLMClient } from "../../src/route.js"
import { compileRequest } from "../../src/route/client.js"
import * as Gemini from "../../src/protocols/gemini.js"
import { ProviderShared } from "../../src/protocols/shared.js"
import { it } from "../lib/effect.js"
import { fixedResponse } from "../lib/http.js"
import { sseEvents, sseRaw } from "../lib/sse.js"
@@ -291,35 +290,30 @@ describe("Gemini route", () => {
}),
)
for (const [name, media] of [
["mismatched data URL MIME", { mediaType: "image/png", data: "data:image/jpeg;base64,/9j/" }],
["malformed base64", { mediaType: "image/png", data: "%%%=" }],
["unsupported SVG", { mediaType: "image/svg+xml", data: "PHN2Zz4=" }],
] as const)
it.effect(`rejects ${name}`, () =>
Effect.gen(function* () {
const error = yield* compileRequest(
LLM.request({ model, messages: [Message.user({ type: "media", ...media })] }),
).pipe(Effect.flip)
expect(error.message).toMatch(/does not support|does not match|valid base64/)
}),
)
it.effect("rejects oversized image input", () =>
it.effect("passes encoded media through without local validation", () =>
Effect.gen(function* () {
const error = yield* compileRequest(
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.user({
type: "media",
mediaType: "image/png",
data: "A".repeat(ProviderShared.MAX_MEDIA_ENCODED_BYTES + 4),
}),
Message.user([
{ type: "media", mediaType: "image/png", data: "%%%=" },
{ type: "media", mediaType: "image/png", data: "data:image/jpeg;base64,/9j/" },
{ type: "media", mediaType: "image/svg+xml", data: "PHN2Zz4=" },
]),
],
}),
).pipe(Effect.flip)
expect(error.message).toContain("encoded limit")
)
expect(prepared.body.contents).toEqual([
{
role: "user",
parts: [
{ inlineData: { mimeType: "image/png", data: "%%%=" } },
{ inlineData: { mimeType: "image/png", data: "/9j/" } },
{ inlineData: { mimeType: "image/svg+xml", data: "PHN2Zz4=" } },
],
},
])
}),
)
+29 -22
View File
@@ -527,35 +527,42 @@ describe("OpenAI Chat route", () => {
}),
)
for (const [name, media] of [
["mismatched data URL MIME", { mediaType: "image/png", data: "data:image/jpeg;base64,/9j/" }],
["malformed base64", { mediaType: "image/png", data: "not-base64" }],
["unsupported SVG", { mediaType: "image/svg+xml", data: "PHN2Zz4=" }],
] as const)
it.effect(`rejects ${name}`, () =>
Effect.gen(function* () {
const error = yield* compileRequest(
LLM.request({ model, messages: [Message.user({ type: "media", ...media })] }),
).pipe(Effect.flip)
expect(error.message).toMatch(/does not support|does not match|valid base64/)
}),
)
it.effect("passes encoded image media through without local validation", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.user([
{ type: "media", mediaType: "image/png", data: "not-base64" },
{ type: "media", mediaType: "image/png", data: "data:image/jpeg;base64,/9j/" },
{ type: "media", mediaType: "image/svg+xml", data: "PHN2Zz4=" },
]),
],
}),
)
expect(prepared.body.messages).toEqual([
{
role: "user",
content: [
{ type: "image_url", image_url: { url: "data:image/png;base64,not-base64" } },
{ type: "image_url", image_url: { url: "data:image/jpeg;base64,/9j/" } },
{ type: "image_url", image_url: { url: "data:image/svg+xml;base64,PHN2Zz4=" } },
],
},
])
}),
)
it.effect("rejects oversized image input", () =>
it.effect("rejects non-image media that cannot be lowered", () =>
Effect.gen(function* () {
const error = yield* compileRequest(
LLM.request({
model,
messages: [
Message.user({
type: "media",
mediaType: "image/png",
data: "A".repeat(ProviderShared.MAX_MEDIA_ENCODED_BYTES + 4),
}),
],
messages: [Message.user({ type: "media", mediaType: "audio/mpeg", data: "AAECAw==" })],
}),
).pipe(Effect.flip)
expect(error.message).toContain("encoded limit")
expect(error.message).toContain("OpenAI Chat does not support media type audio/mpeg")
}),
)
@@ -1149,6 +1149,32 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("passes large PDF tool-result content through", () =>
Effect.gen(function* () {
const base64 = "A".repeat(8_125_844)
const dataUrl = `data:application/pdf;base64,${base64}`
const prepared = yield* compileRequest(
LLM.request({
id: "req_tool_result_large_pdf",
model,
messages: [
Message.assistant([ToolCallPart.make({ id: "call_1", name: "read", input: {} })]),
Message.tool({
id: "call_1",
name: "read",
resultType: "content",
result: [{ type: "file", uri: dataUrl, mime: "application/pdf", name: "report.pdf" }],
}),
],
}),
)
expect(expectToolOutput(prepared.body).output).toEqual([
{ type: "input_file", filename: "report.pdf", file_data: dataUrl },
])
}),
)
it.effect("uses xAI inline file encoding for PDF tool results", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
@@ -1184,9 +1210,9 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("rejects unsupported media in tool-result content with a clear error", () =>
it.effect("passes non-image tool-result content through as an input file", () =>
Effect.gen(function* () {
const error = yield* compileRequest(
const prepared = yield* compileRequest(
LLM.request({
id: "req_tool_result_unsupported_media",
model,
@@ -1200,10 +1226,11 @@ describe("OpenAI Responses route", () => {
}),
],
}),
).pipe(Effect.flip)
)
expect(error.message).toContain("OpenAI Responses")
expect(error.message).toContain("audio/mpeg")
expect(expectToolOutput(prepared.body).output).toEqual([
{ type: "input_file", filename: "file", file_data: "data:audio/mpeg;base64,AAECAw==" },
])
}),
)
@@ -2394,17 +2421,28 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("rejects unsupported user media content", () =>
it.effect("passes non-image user media through as an input file", () =>
Effect.gen(function* () {
const error = yield* compileRequest(
const prepared = yield* compileRequest(
LLM.request({
id: "req_media",
model,
messages: [Message.user({ type: "media", mediaType: "application/x-tar", data: "AAECAw==" })],
}),
).pipe(Effect.flip)
)
expect(error.message).toContain("OpenAI Responses does not support media type application/x-tar")
expect(prepared.body.input).toEqual([
{
role: "user",
content: [
{
type: "input_file",
filename: "file",
file_data: "data:application/x-tar;base64,AAECAw==",
},
],
},
])
}),
)
@@ -1,60 +0,0 @@
import { expect, test } from "@playwright/test"
import type { Page } from "@playwright/test"
import { fixture, pageMessages } from "../smoke/session-timeline.fixture"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible } from "../utils/waits"
const NAMES = ["alpha-service", "bravo-web", "charlie-api", "delta-tools", "echo-infra", "foxtrot-docs"]
const worktrees = NAMES.map((name) => `/opencode-demo/${name}`)
// The sixth project sits outside the five-item recent cap, so it is only reachable if the
// dialog hands every recent project to the list filter instead of a pre-truncated slice.
const OUTSIDE_CAP = "foxtrot-docs"
// Dialog rows carry data-directory-path; the sidebar project list does not, so this
// scopes assertions to the picker instead of matching the sidebar entry of the same name.
const rows = (page: Page) => page.locator("[data-directory-path]")
const row = (page: Page, name: string) => page.locator(`[data-directory-path*="${name}"]`)
async function openProjectDialog(page: Page) {
await mockOpenCodeServer(page, {
sessions: fixture.sessions,
provider: fixture.provider,
directory: fixture.directory,
project: fixture.project,
pageMessages,
fileList: () => [],
findFiles: () => [],
})
await page.addInitScript((dirs) => {
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
projects: { local: dirs.map((worktree: string) => ({ worktree, expanded: false })) },
lastProject: {},
}),
)
}, worktrees)
await page.goto("/")
const add = page.getByRole("button", { name: "Add project" }).first()
await expectAppVisible(add)
await add.click()
await expect(rows(page)).toHaveCount(5)
return page.getByRole("textbox").last()
}
test("searches every recent project, not just the five most recent", async ({ page }) => {
const search = await openProjectDialog(page)
await expect(row(page, OUTSIDE_CAP)).toHaveCount(0)
await search.fill("foxtrot")
await expect(row(page, OUTSIDE_CAP)).toHaveCount(1)
})
test("still caps the idle recent list at five projects", async ({ page }) => {
await openProjectDialog(page)
await expect(row(page, NAMES[4])).toHaveCount(1)
await expect(row(page, OUTSIDE_CAP)).toHaveCount(0)
})
@@ -16,9 +16,8 @@ import { mockOpenCodeServer } from "../utils/mock-server"
import { installSseTransport } from "../utils/sse-transport"
import { expectSessionTitle } from "../utils/waits"
const initialPageSize = 20
const historyPageSize = 200
const messages = Array.from({ length: initialPageSize + 1 }, (_, index) => {
const messagePageSize = 200
const messages = Array.from({ length: messagePageSize / 2 + 1 }, (_, index) => {
const id = `msg_${String(index + 1001).padStart(4, "0")}_history_root_user`
return [
userMessage(undefined, { id, created: 1700000000000 + index * 2_000 }),
@@ -26,7 +25,7 @@ const messages = Array.from({ length: initialPageSize + 1 }, (_, index) => {
id: `msg_${String(index + 1001).padStart(4, "0")}_history_root_assistant`,
parentID: id,
created: 1700000001000 + index * 2_000,
completed: index < initialPageSize,
completed: index < messagePageSize / 2,
}),
]
}).flat()
@@ -160,21 +159,18 @@ for (const scenario of scenarios) {
await expect(page.locator(`[data-timeline-part-id="${userPartID}"]`)).toBeVisible()
const viewport = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
await viewport.hover()
const deadline = Date.now() + 10_000
const deadline = Date.now() + 30_000
while (requests.filter((request) => request.phase === "start").length < 2) {
if (Date.now() >= deadline) throw new Error("Timed out scrolling to the history boundary")
await page.mouse.wheel(0, -240)
await page.mouse.wheel(0, -1_200)
await page.waitForTimeout(20)
}
expect(requests.filter((request) => request.phase === "end")).toHaveLength(1)
expect(sequence.slice(0, 3)).toEqual([
"messages:start:latest",
"messages:end:latest",
`messages:start:${messages.at(-initialPageSize)!.info.id}`,
`messages:start:${messages.at(-messagePageSize)!.info.id}`,
])
await expect(page.locator('[data-timeline-part-id*="_history_root_assistant:text:0"]')).toHaveCount(
initialPageSize / 2,
)
await page.evaluate(() => {
;(
window as Window & {
@@ -186,15 +182,12 @@ for (const scenario of scenarios) {
expect(await visibleContentHidden(page)).toBe(false)
const beforeHistory = await probeSamples(page)
history.resolve()
await expect
.poll(() => page.locator('[data-timeline-part-id*="_history_root_assistant:text:0"]').count())
.toBeGreaterThan(initialPageSize / 2)
await expect.poll(() => requests.filter((request) => request.phase === "end").length).toBe(2)
await expect(page.getByRole("button", { name: "Stop" })).toBeVisible()
await waitForProbeSamples(page, beforeHistory)
expect(pages).toEqual([
{ before: undefined, limit: initialPageSize },
{ before: messages.at(-initialPageSize)!.info.id, limit: historyPageSize },
{ before: undefined, limit: messagePageSize },
{ before: messages.at(-messagePageSize)!.info.id, limit: messagePageSize },
])
expect(roots).toEqual([])
@@ -1,10 +1,15 @@
import { expect, test } from "@playwright/test"
import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client/promise"
import { session, sessionID, setupTimeline } from "../performance/timeline-stability/fixture"
import { event, session, sessionID, setupTimeline } from "../performance/timeline-stability/fixture"
const user = { id: "msg_user", type: "user", text: "Run it", time: { created: 1 } } satisfies SessionMessageInfo
const assistant = (completed: boolean, tool = false, childID?: string): SessionMessageAssistant => ({
const assistant = (
completed: boolean,
tool = false,
childID?: string,
background = false,
): SessionMessageAssistant => ({
id: "msg_assistant",
type: "assistant",
agent: "build",
@@ -15,7 +20,11 @@ const assistant = (completed: boolean, tool = false, childID?: string): SessionM
type: "tool",
id: "call_subagent",
name: "subagent",
state: { status: "running", input: {}, metadata: childID ? { sessionID: childID } : {} },
state: {
status: "running",
input: { description: "Inspect code", ...(background ? { background: true } : {}) },
metadata: { status: "running", ...(childID ? { sessionID: childID } : {}) },
},
time: { created: 2 },
},
]
@@ -59,12 +68,17 @@ test("renders current protocol notices in CLI order", async ({ page }) => {
await expect(notices.nth(1)).toContainText("explore finished · Search code")
await expect(notices.nth(2)).toContainText("Continuing after restart")
await expect(notices.nth(3)).toContainText("Skill · Review")
await expect(notices).toHaveClass([/text-text-weak/, /text-text-weak/, /text-text-weak/, /text-text-weak/])
await expect(notices.locator(".text-text-strong")).toHaveCount(0)
expect(ownerWarnings).toEqual([])
})
test("moves blocking work to the background with Ctrl+B", async ({ page }) => {
await setupTimeline(page, { currentMessages: [user, assistant(false, true)] })
await expect(page.locator('[data-component="task-tool-card"]')).toBeVisible()
const card = page.locator('[data-component="task-tool-card"]')
await expect(card).toBeVisible()
await expect(card).toContainText("Inspect code")
await expect(card).not.toContainText("(background)")
await expect(page.getByText("Called `subagent`", { exact: false })).toHaveCount(0)
await expect(page.locator('[data-component="background-tool-control"]')).toHaveCount(0)
await expect(page.locator('[data-action="session-background-toggle"]')).toContainText("Move 1 subagent to background")
@@ -77,6 +91,11 @@ test("moves blocking work to the background with Ctrl+B", async ({ page }) => {
await request
})
test("waits for completion before labeling requested background work", async ({ page }) => {
await setupTimeline(page, { currentMessages: [user, assistant(false, true, undefined, true)] })
await expect(page.locator('[data-component="task-tool-card"]')).not.toContainText("(background)")
})
test("navigates from a running subagent card and hides background controls in the child", async ({ page }) => {
const childID = "ses_running_child"
await setupTimeline(page, {
@@ -105,7 +124,7 @@ test("shows a badge for active background work", async ({ page }) => {
test("separates blocking and already-backgrounded work into two rows", async ({ page }) => {
const backgroundID = "ses_background_existing"
const blockingID = "ses_background_blocking"
await setupTimeline(page, {
const timeline = await setupTimeline(page, {
currentMessages: [
user,
{
@@ -175,9 +194,16 @@ test("separates blocking and already-backgrounded work into two rows", async ({
})
const dock = page.locator('[data-component="session-background-dock"]')
const backgroundCard = page.locator('[data-timeline-part-id="call_backgrounded"]')
await expect(dock).toContainText("Move 1 subagent to background")
await expect(dock.getByText("Running 1 shell and 1 subagent in background", { exact: true })).toBeVisible()
await expect(backgroundCard).toContainText("Background task (background)")
await expect(backgroundCard.locator('[data-component="session-progress-indicator-v2"]')).toBeVisible()
await expect(
page.locator('[data-timeline-part-id="call_shell_backgrounded"] [data-component="text-shimmer"]'),
).toHaveAttribute("data-active", "true")
await timeline.send(event("session.status", { sessionID: backgroundID, status: { type: "idle" } }))
await expect(backgroundCard.locator('[data-component="session-progress-indicator-v2"]')).toHaveCount(0)
await expect(backgroundCard).toContainText("Background task (background)")
})
@@ -115,6 +115,12 @@ test("labels V2 skill tools from IDs and result metadata", async ({ page }) => {
"aria-label",
"OpenCode",
)
for (const id of [pending, completed]) {
const skill = page.locator(`[data-timeline-part-id="${id}"]`)
await expect(skill.locator('[data-slot="skill-tool-label"]')).toHaveText("Skill")
await expect(skill.locator('[data-slot="skill-tool-separator"]')).toHaveText("·")
await expect(skill.locator('use[href="#opencode-icon-post-skill"]')).toBeVisible()
}
})
function questionInput() {
@@ -222,7 +222,7 @@ function turn(index: number): Message[] {
return [user, assistantMessage(targetID, index, user.info.id, parts)]
}
const targetMessages = Array.from({ length: 72 }, (_, index) => turn(index)).flat()
const targetMessages = Array.from({ length: 101 }, (_, index) => turn(index)).flat()
const sourceMessages = Array.from({ length: 12 }, (_, index) => [
userMessage(sourceID, index + 1000, 120),
assistantMessage(sourceID, index + 1000, id("msg_user", index + 1000), [textPart(index + 1000, 0, 240)]),
@@ -727,7 +727,7 @@ function expectCompleteScroll(
).toEqual([])
expect(new Set(expectedPartIDs).size).toBe(expectedPartIDs.length)
expect(new Set(expectedMessageIDs).size).toBe(expectedMessageIDs.length)
expect(expectedPartIDs.length).toBe(331)
expect(expectedPartIDs.length).toBe(465)
}
async function selectHomeProject(page: Page, projectName: string) {
@@ -72,7 +72,12 @@ test("creates a session in a new project and selects its model", async ({ page }
const addProject = page.locator('[data-action="home-add-project-row"]')
await expectAppVisible(addProject)
await addProject.click()
await page.locator("[data-directory-path]").click()
const directoryItem = page.getByRole("treeitem", { name: "NewProject" })
await expect(directoryItem).toBeVisible()
await directoryItem.click()
const selectFolder = page.getByRole("button", { name: "Select folder" })
await expect(selectFolder).toBeEnabled()
await selectFolder.click()
await page.locator('[data-action="home-new-session"]').click()
await expectAppVisible(page.locator('[data-component="prompt-input-v2"]'))
@@ -1,205 +0,0 @@
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { List } from "@opencode-ai/ui/list"
import type { ListRef } from "@opencode-ai/ui/list"
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
import { createMemo, createResource, createSignal } from "solid-js"
import { useLanguage } from "@/context/language"
import { ServerConnection } from "@/context/servers"
import { useGlobal } from "@/context/global"
import { cleanPickerInput, createDirectorySearch, displayPickerPath } from "./directory-picker-domain"
import type { Path } from "@/types"
interface DialogSelectDirectoryProps {
title?: string
multiple?: boolean
onSelect: (result: string | string[] | null) => void
server: ServerConnection.Any
}
const RECENT_PROJECT_LIMIT = 5
type Row = {
absolute: string
search: string
group: "recent" | "folders"
}
function toRow(absolute: string, home: string, group: Row["group"]): Row {
const full = displayPickerPath(absolute, "", "")
const tilde = displayPickerPath(full, "~", home)
const withSlash = (value: string) => {
if (!value) return ""
if (value.endsWith("/")) return value
return value + "/"
}
const search = Array.from(
new Set([full, withSlash(full), tilde, withSlash(tilde), getFilename(full)].filter(Boolean)),
).join("\n")
return { absolute: full, search, group }
}
function uniqueRows(rows: Row[]) {
const seen = new Set<string>()
return rows.filter((row) => {
if (seen.has(row.absolute)) return false
seen.add(row.absolute)
return true
})
}
export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
const global = useGlobal()
const { sync, sdk, ...serverCtx } = global.ensureServerCtx(props.server)
const dialog = useDialog()
const language = useLanguage()
const [filter, setFilter] = createSignal("")
let list: ListRef | undefined
const [fallbackPath] = createResource(
() => (!(sync.data.path.home || sync.data.path.directory) ? true : undefined),
() =>
sdk.api.location
.get()
.then(
(location): Path => ({
state: "",
config: "",
worktree: location.project.directory,
directory: location.directory,
home: "",
}),
)
.catch(() => undefined),
{ initialValue: undefined },
)
const home = createMemo(() => sync.data.path.home || fallbackPath()?.home || "")
const start = createMemo(
() => sync.data.path.home || sync.data.path.directory || fallbackPath()?.home || fallbackPath()?.directory,
)
const directories = createDirectorySearch({
sdk,
home,
base: start,
})
const recentProjects = createMemo(() => {
const projects = serverCtx.projects.list()
const byProject = new Map<string, number>()
for (const project of projects) {
let at = 0
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
for (const directory of dirs) {
const sessions = sync.child(directory, { bootstrap: false })[0].session
for (const session of sessions) {
if (session.time.archived) continue
const updated = session.time.updated ?? session.time.created
if (updated > at) at = updated
}
}
byProject.set(project.worktree, at)
}
return projects
.map((project, index) => ({ project, at: byProject.get(project.worktree) ?? 0, index }))
.sort((a, b) => b.at - a.at || a.index - b.index)
.map(({ project }) => {
const row = toRow(project.worktree, home(), "recent")
const name = project.name || getFilename(project.worktree)
return {
...row,
search: `${row.search}\n${name}`,
}
})
})
const items = async (value: string) => {
const results = await directories(value)
const directoryRows = results.map((absolute) => toRow(absolute, home(), "folders"))
// Cap the idle list only. Once a query narrows the results, every project stays searchable.
const recent = recentProjects()
const visible = value ? recent : recent.slice(0, RECENT_PROJECT_LIMIT)
return uniqueRows([...visible, ...directoryRows])
}
function resolve(absolute: string) {
props.onSelect(props.multiple ? [absolute] : absolute)
dialog.close()
}
return (
<Dialog title={props.title ?? language.t("command.project.open")}>
<List
class="px-3"
search={{ placeholder: language.t("dialog.directory.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.directory.empty")}
loadingMessage={language.t("common.loading")}
items={items}
key={(x) => x.absolute}
filterKeys={["search"]}
groupBy={(item) => item.group}
sortGroupsBy={(a, b) => {
if (a.category === b.category) return 0
return a.category === "recent" ? -1 : 1
}}
groupHeader={(group) =>
group.category === "recent" ? language.t("home.recentProjects") : language.t("command.project.open")
}
ref={(r) => (list = r)}
onFilter={(value) => setFilter(cleanPickerInput(value))}
onKeyEvent={(e, item) => {
if (e.key !== "Tab") return
if (e.shiftKey) return
if (!item) return
e.preventDefault()
e.stopPropagation()
const value = displayPickerPath(item.absolute, filter(), home())
list?.setFilter(value.endsWith("/") ? value : value + "/")
}}
onSelect={(path) => {
if (!path) return
resolve(path.absolute)
}}
>
{(item) => {
const path = displayPickerPath(item.absolute, filter(), home())
if (path === "~") {
return (
<div data-directory-path={item.absolute} class="w-full flex items-center justify-between rounded-md">
<div class="flex items-center gap-x-3 grow min-w-0">
<FileIcon node={{ path: item.absolute, type: "directory" }} class="shrink-0 size-4" />
<div class="flex items-center text-14-regular min-w-0">
<span class="text-text-strong whitespace-nowrap">~</span>
<span class="text-text-weak whitespace-nowrap">/</span>
</div>
</div>
</div>
)
}
return (
<div data-directory-path={item.absolute} class="w-full flex items-center justify-between rounded-md">
<div class="flex items-center gap-x-3 grow min-w-0">
<FileIcon node={{ path: item.absolute, type: "directory" }} class="shrink-0 size-4" />
<div class="flex items-center text-14-regular min-w-0">
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
{getDirectory(path)}
</span>
<span class="text-text-strong whitespace-nowrap">{getFilename(path)}</span>
<span class="text-text-weak whitespace-nowrap">/</span>
</div>
</div>
</div>
)
}}
</List>
</Dialog>
)
}
@@ -1,9 +1,7 @@
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ServerConnection } from "@/context/servers"
import { usePlatform } from "@/context/platform"
import { useSettings } from "@/context/settings"
import { lazy } from "solid-js"
import { DialogSelectDirectory } from "./dialog-select-directory"
import { directoryPickerKind } from "./directory-picker-policy"
const DialogSelectDirectoryV2 = lazy(() =>
@@ -19,7 +17,6 @@ type DirectoryPickerInput = {
export function useDirectoryPicker() {
const platform = usePlatform()
const settings = useSettings()
const dialog = useDialog()
return (input: DirectoryPickerInput) => {
@@ -36,10 +33,6 @@ export function useDirectoryPicker() {
const cancel = () => {
if (!selected) input.onSelect(null)
}
if (platform.platform === "desktop" && settings.general.newLayoutDesigns()) {
dialog.show(() => <DialogSelectDirectoryV2 {...input} onSelect={onSelect} />, cancel)
return
}
dialog.show(() => <DialogSelectDirectory {...input} onSelect={onSelect} />, cancel)
dialog.show(() => <DialogSelectDirectoryV2 {...input} onSelect={onSelect} />, cancel)
}
}
@@ -618,7 +618,7 @@ describe("server session", () => {
await ctx.store.sync("root")
expect(ctx.get).toEqual([{ sessionID: "root" }])
expect(ctx.messages).toEqual([{ sessionID: "root", limit: 20, order: "desc" }])
expect(ctx.messages).toEqual([{ sessionID: "root", limit: 200, order: "desc" }])
expect(ctx.store.data.message.root).toEqual([])
})
@@ -629,8 +629,32 @@ describe("server session", () => {
ctx.store.invalidate()
await ctx.store.sync("root")
expect(ctx.store.data.message.root).toEqual([])
expect(ctx.get).toHaveLength(2)
expect(ctx.messages).toHaveLength(2)
expect(ctx.messages).toEqual([
{ sessionID: "root", limit: 200, order: "desc" },
{ sessionID: "root", limit: 200, order: "desc" },
])
})
test("keeps a fixed page size after the local message cache exceeds the API limit", async () => {
const client = messageClient(response(), response())
const store = createServerSession(client)
await store.sync("child")
Array.from({ length: 428 }, (_, index) =>
store.apply({
type: "message.updated",
properties: { info: userMessage(`message-${index}`, { time: { created: index } }) },
}),
)
expect(store.data.message.child).toHaveLength(428)
await store.sync("child", { force: true })
expect(client.requests).toEqual([
{ sessionID: "child", limit: 200, order: "desc" },
{ sessionID: "child", limit: 200, order: "desc" },
])
})
test("loads current session content through the current message API", async () => {
@@ -655,7 +679,7 @@ describe("server session", () => {
await store.sync("root")
expect(requests).toEqual([{ sessionID: "root", limit: 20, order: "desc" }])
expect(requests).toEqual([{ sessionID: "root", limit: 200, order: "desc" }])
expect(store.data.session_message.root.map((message) => message.id)).toEqual([user.id, assistant.id])
expect(store.data.message.root.map((message) => message.id)).toEqual([user.id, assistant.id])
})
@@ -731,8 +755,8 @@ describe("server session", () => {
await store.sync("root")
expect(requests).toEqual([
{ sessionID: "root", limit: 20, order: "desc" },
{ sessionID: "root", limit: 20, cursor: "older" },
{ sessionID: "root", limit: 200, order: "desc" },
{ sessionID: "root", limit: 200, cursor: "older" },
])
expect(store.data.message.root.map((message) => message.id)).toEqual([
user.id,
@@ -741,6 +765,26 @@ describe("server session", () => {
expect(assistants.map((item) => store.data.part[item.id]?.[0]?.type)).toEqual(["text", "text", "text"])
})
test("loads older messages by cursor with the fixed page size", async () => {
const older = userMessage("message-1")
const latest = userMessage("message-2", { time: { created: 2 } })
const client = messageClient(
response([{ info: latest, parts: [] }], "older"),
response([{ info: older, parts: [] }]),
)
const store = createServerSession(client)
await store.sync("child")
await store.history.loadMore("child")
expect(client.requests).toEqual([
{ sessionID: "child", limit: 200, order: "desc" },
{ sessionID: "child", limit: 200, cursor: "older" },
])
expect(store.data.message.child).toEqual([older, latest])
expect(store.history.more("child")).toBe(false)
})
// V2 messages are ordered projections and do not expose V1 assistant parent IDs.
describe.skip("V1 assistant parent projections", () => {
test("backfills an assistant-only initial page through its user root", async () => {
@@ -754,7 +798,7 @@ describe("server session", () => {
await store.sync("child")
expect(client.requests).toEqual([{ sessionID: "child", limit: 20, order: "desc" }])
expect(client.requests).toEqual([{ sessionID: "child", limit: 200, order: "desc" }])
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: user.id }])
expect(store.data.message.child).toEqual([user, ...assistants])
expect(store.history.more("child")).toBe(false)
+18 -22
View File
@@ -30,8 +30,7 @@ type MessageApi = ServerApi["message"]
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
const initialMessagePageSize = 20
const historyMessagePageSize = 200
const messagePageSize = 200
const sessionInfoLimit = 2_048
const emptyIDs: ReadonlySet<string> = new Set()
@@ -242,7 +241,6 @@ export function createServerSession(
return created
}
const [meta, setMeta] = createStore({
limit: {} as Record<string, number | undefined>,
cursor: {} as Record<string, string | undefined>,
complete: {} as Record<string, boolean | undefined>,
loading: {} as Record<string, boolean | undefined>,
@@ -423,7 +421,6 @@ export function createServerSession(
setMeta(
produce((draft) => {
for (const sessionID of sessionIDs) {
delete draft.limit[sessionID]
delete draft.cursor[sessionID]
delete draft.complete[sessionID]
delete draft.loading[sessionID]
@@ -457,11 +454,13 @@ export function createServerSession(
pickSessionCacheEvictions({ seen, keep: sessionID, limit: SESSION_CACHE_LIMIT, preserve: protectedSessions() }),
)
const fetchMessages = async (sessionID: string, limit: number, before?: string, onAttempt?: () => void) => {
const fetchMessages = async (sessionID: string, before?: string, onAttempt?: () => void) => {
const request = (cursor?: string) =>
(options?.retry ?? retry)(() => {
onAttempt?.()
return messageApi.list(cursor ? { sessionID, limit, cursor } : { sessionID, limit, order: "desc" })
return messageApi.list(
cursor ? { sessionID, limit: messagePageSize, cursor } : { sessionID, limit: messagePageSize, order: "desc" },
)
})
const first = await request(before)
const pages = [first]
@@ -632,14 +631,13 @@ export function createServerSession(
}
orphanParts.delete(sessionID)
}
setMeta("limit", sessionID, messages.length)
setMeta("cursor", sessionID, merged.cursor)
setMeta("complete", sessionID, merged.complete)
setMeta("at", sessionID, Date.now())
})
}
const loadMessages = async (sessionID: string, limit: number, before?: string, mode?: "replace" | "prepend") => {
const loadMessages = async (sessionID: string, before?: string, mode?: "replace" | "prepend") => {
if (meta.loading[sessionID]) return
const active = generation(sessionID)
const load: MessageLoadState = {
@@ -658,7 +656,7 @@ export function createServerSession(
setMeta("loading", sessionID, true)
let applied = false
try {
const page = await fetchMessages(sessionID, limit, before, () => resetMessageLoad(sessionID, load))
const page = await fetchMessages(sessionID, before, () => resetMessageLoad(sessionID, load))
const first = page.session.reduce<Message | undefined>(
(oldest, message) => (!oldest || compareMessages(message, oldest) < 0 ? message : oldest),
undefined,
@@ -737,32 +735,30 @@ export function createServerSession(
}
}
const sync = (sessionID: string, options?: { force?: boolean; messageLimit?: number }) => {
const sync = (sessionID: string, options?: { force?: boolean }) => {
touch(sessionID)
return runInflight(inflight, sessionID, async () => {
const cached = data.message[sessionID] !== undefined && meta.limit[sessionID] !== undefined
const cached = data.message[sessionID] !== undefined && meta.complete[sessionID] !== undefined
const invalid = invalidated.has(sessionID)
const revision = invalidationRevision
if (cached && data.info[sessionID] && !invalid && !options?.force) return
await Promise.all([
resolve(sessionID, invalid ? { ...options, force: true } : options),
cached && !invalid && !options?.force
? Promise.resolve()
: loadMessages(sessionID, options?.messageLimit ?? meta.limit[sessionID] ?? initialMessagePageSize),
cached && !invalid && !options?.force ? Promise.resolve() : loadMessages(sessionID),
])
if (invalid && invalidationRevision === revision) invalidated.delete(sessionID)
})
}
const prefetch = async (sessionID: string, limit: number) => {
const prefetch = async (sessionID: string, messageCount: number) => {
touch(sessionID)
await inflight.get(sessionID)
if (
Date.now() - (meta.at[sessionID] ?? 0) <= 15_000 &&
(meta.complete[sessionID] || (data.message[sessionID]?.length ?? 0) >= limit)
(meta.complete[sessionID] || (data.message[sessionID]?.length ?? 0) >= messageCount)
)
return
await runInflight(inflight, sessionID, () => loadMessages(sessionID, limit))
await runInflight(inflight, sessionID, () => loadMessages(sessionID))
}
const eventSessionID = (event: { type: string; properties?: unknown }) => {
@@ -1354,11 +1350,11 @@ export function createServerSession(
setMeta("at", {})
},
prefetch,
shouldPrefetch(sessionID: string, limit: number) {
shouldPrefetch(sessionID: string, messageCount: number) {
if (data.message[sessionID] === undefined) return true
if (Date.now() - (meta.at[sessionID] ?? 0) > 15_000) return true
if (meta.complete[sessionID]) return false
return (meta.limit[sessionID] ?? 0) <= limit
return (data.message[sessionID]?.length ?? 0) <= messageCount
},
fresh(sessionID: string, ttl: number) {
return Date.now() - (meta.at[sessionID] ?? 0) <= ttl
@@ -1440,14 +1436,14 @@ export function createServerSession(
history: {
more: (sessionID: string) =>
data.message[sessionID] !== undefined &&
meta.limit[sessionID] !== undefined &&
meta.complete[sessionID] !== undefined &&
!meta.complete[sessionID] &&
!!meta.cursor[sessionID],
loading: (sessionID: string) => meta.loading[sessionID] ?? false,
async loadMore(sessionID: string, count = historyMessagePageSize) {
async loadMore(sessionID: string) {
touch(sessionID)
if (meta.loading[sessionID] || meta.complete[sessionID] || !meta.cursor[sessionID]) return
await loadMessages(sessionID, count, meta.cursor[sessionID], "prepend")
await loadMessages(sessionID, meta.cursor[sessionID], "prepend")
},
},
evict(sessionID: string) {
@@ -60,7 +60,11 @@ export function NewSessionView(props: {
<Show
when={props.workspace.bar.visible()}
fallback={
<PromptGitStatus branch={props.workspace.bar.branch()} noGit={!props.workspace.project.git()} />
<PromptGitStatus
branch={props.workspace.bar.branch()}
noGit={!props.workspace.project.git()}
class="ms-1"
/>
}
>
<PromptWorkspaceSelector
@@ -94,7 +94,7 @@ export function createTimelineController(input: {
fallback: language.t("command.session.new"),
})
})
const showHeader = createMemo(() => !!(titleValue() || input.session.data.parentID()))
const showHeader = createMemo(() => !!input.session.identity.sessionID())
const projection = createTimelineProjection({
messages: input.session.history.messages,
userMessages: input.userMessages,
@@ -1057,9 +1057,12 @@ function MessageTimelineView(
<TimelineRowFrame row={noticeRow()}>
<Show when={content()}>
{(content) => (
<div data-slot="session-timeline-notice" class={`w-full pt-3 pb-1 text-13-regular ${turnPadding()}`}>
<span class="text-13-medium text-text-strong">{content().label}</span>
<Show when={content().data}>{(data) => <span class="text-text-weak"> · {data()}</span>}</Show>
<div
data-slot="session-timeline-notice"
class={`w-full pt-3 pb-1 text-13-regular text-text-weak ${turnPadding()}`}
>
<span class="text-13-medium">{content().label}</span>
<Show when={content().data}>{(data) => <span> · {data()}</span>}</Show>
</div>
)}
</Show>
@@ -1203,6 +1206,8 @@ function MessageTimelineView(
onCleanup(() => {
if (contentMeasureFrame !== undefined) cancelAnimationFrame(contentMeasureFrame)
// Solid runs cleanup before it disconnects the row, so defer TanStack's null-ref cleanup.
queueMicrotask(() => virtualizer.measureElement(null))
})
return (
+2 -1
View File
@@ -5,7 +5,8 @@ const fs = require("fs")
const path = require("path")
const os = require("os")
const forwardedSignals = ["SIGINT", "SIGTERM", "SIGHUP"]
const forwardedSignals =
process.platform === "win32" ? ["SIGINT", "SIGTERM", "SIGHUP"] : ["SIGINT", "SIGTERM", "SIGHUP", "SIGUSR1"]
function run(target) {
const child = childProcess.spawn(target, process.argv.slice(2), { stdio: "inherit" })
+71 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { rm } from "fs/promises"
import { mkdir, rm } from "fs/promises"
import path from "path"
import { Script } from "@opencode-ai/script"
import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin"
@@ -27,6 +27,7 @@ const requestedTarget = process.argv.find((arg) => arg.startsWith("--target="))?
const skipInstall = process.argv.includes("--skip-install")
const skipWebUi = process.argv.includes("--skip-web-ui")
const solidPlugin = createSolidTransformPlugin()
const releaseAssets = new Map<string, Promise<Map<string, string>>>()
const allTargets: {
os: string
@@ -99,6 +100,7 @@ for (const item of targets) {
}
const target = targetName(item)
const name = target.replace(binary, "cli")
const executablePath = await compileExecutable(item)
console.log(`building ${name}`)
const result = await Bun.build({
entrypoints: ["./src/index.ts"],
@@ -115,8 +117,9 @@ for (const item of targets) {
autoloadTsconfig: true,
autoloadPackageJson: true,
target: target.replace(binary, "bun") as Bun.Build.CompileTarget,
...(executablePath ? { executablePath } : {}),
outfile: path.join(outdir, name, "bin", binary),
execArgv: [`--user-agent=${binary}/${Script.version}`, "--use-system-ca", "--"],
execArgv: [`--user-agent=${binary}/${Script.version}`, "--use-system-ca", "--no-warnings", "--"],
windows: {},
},
define: {
@@ -154,6 +157,72 @@ for (const item of targets) {
await verifyArtifact(path.join(outdir, name))
}
async function compileExecutable(item: (typeof allTargets)[number]) {
const release = process.env.BUN_COMPILE_RELEASE
if (!release) return
const platform = item.os === "win32" ? "windows" : item.os
const name = [
"bun",
platform,
item.arch === "arm64" ? "aarch64" : item.arch,
item.abi,
item.avx2 === false ? "baseline" : undefined,
]
.filter(Boolean)
.join("-")
const cache = path.join(outdir, ".bun", release)
const executable = path.join(cache, name, item.os === "win32" ? "bun.exe" : "bun")
if (await Bun.file(executable).exists()) return executable
await mkdir(cache, { recursive: true })
const archive = path.join(cache, `${name}.zip`)
const assets = await compileReleaseAssets(release)
const url = assets.get(`${name}.zip`)
if (!url) throw new Error(`Bun release ${release} does not include ${name}.zip`)
const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN
const response = await fetch(url, {
headers: { Accept: "application/octet-stream", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
})
if (!response.ok) throw new Error(`Failed to download ${name} from Bun release ${release}: ${response.status}`)
await Bun.write(archive, response)
await $`unzip -oq ${archive} -d ${cache}`
await rm(archive)
return executable
}
function compileReleaseAssets(release: string) {
const existing = releaseAssets.get(release)
if (existing) return existing
const pending = fetch(`https://api.github.com/repos/oven-sh/bun/releases/tags/${release}?cache=${Date.now()}`)
.then(async (response) => {
if (!response.ok) throw new Error(`Failed to resolve Bun release ${release}: ${response.status}`)
const data: unknown = await response.json()
if (typeof data !== "object" || data === null || !("assets" in data) || !Array.isArray(data.assets)) {
throw new Error(`Bun release ${release} returned invalid metadata`)
}
return new Map(
data.assets
.filter(
(asset): asset is { name: string; url: string } =>
typeof asset === "object" &&
asset !== null &&
"name" in asset &&
typeof asset.name === "string" &&
"url" in asset &&
typeof asset.url === "string",
)
.map((asset) => [asset.name, asset.url]),
)
})
.catch((error) => {
releaseAssets.delete(release)
throw error
})
releaseAssets.set(release, pending)
return pending
}
function targetName(item: (typeof allTargets)[number]) {
return [
binary,
+6 -3
View File
@@ -14,9 +14,12 @@ async function published(name: string, version: string) {
async function publish(dir: string, name: string, version: string) {
if (process.platform !== "win32") await $`chmod -R 755 .`.cwd(dir)
if (await published(name, version)) return console.log(`already published ${name}@${version}`)
await $`bun pm pack`.cwd(dir)
await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir)
const exists = await published(name, version)
if (exists) console.log(`already published ${name}@${version}`)
if (!exists) {
await $`bun pm pack`.cwd(dir)
await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir)
}
}
async function publishDistribution(input: { root: string; name: string; binary: string; packagePrefix: string }) {
+6 -3
View File
@@ -1,5 +1,6 @@
import { Argument, Flag } from "effect/unstable/cli"
import { Argument, Command, Flag } from "effect/unstable/cli"
import { Spec } from "../framework/spec"
import { GlobalFlags } from "./global-flags"
declare const OPENCODE_CLI_NAME: string | undefined
@@ -26,7 +27,7 @@ const PermissionParams = {
),
}
export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME : "opencode", {
const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME : "opencode", {
description: "OpenCode 2.0 preview command line interface",
params: {
...ServerParams,
@@ -70,7 +71,7 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
description: "Debugging and troubleshooting tools",
commands: [
Spec.make("agents", { description: "List all agents" }),
Spec.make("config", { description: "Show resolved configuration" }),
Spec.make("config", { description: "List configuration sources" }),
],
}),
Spec.make("console", {
@@ -277,3 +278,5 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
}),
],
})
export const Commands = { ...Root, spec: Root.spec.pipe(Command.withGlobalFlags(GlobalFlags.all)) }
+12
View File
@@ -0,0 +1,12 @@
export * as GlobalFlags from "./global-flags"
import { Flag, GlobalFlag } from "effect/unstable/cli"
export const CpuProfile = GlobalFlag.setting("cpu-profile")({
flag: Flag.string("cpu-profile").pipe(
Flag.withDescription("Write a CPU profile to this path when the process stops"),
Flag.optional,
),
})
export const all = [CpuProfile] as const
+49 -27
View File
@@ -5,7 +5,7 @@ import { TuiKeybind } from "@opencode-ai/tui/config/v1/keybind"
import { Definitions } from "@opencode-ai/tui/config/keybind"
import { Effect, FileSystem, Option, Schema } from "effect"
import { randomUUID } from "crypto"
import { createScanner, parse, parseTree, type Node, type ParseError } from "jsonc-parser"
import { applyEdits, createScanner, modify, parse, parseTree, type Node, type ParseError } from "jsonc-parser"
import path from "path"
import { Info } from "./schema"
@@ -41,33 +41,55 @@ export const run = Effect.fn("cli.config.migrate")(function* (input: {
if (errors.length) return
const config = Option.getOrUndefined(decodeRecord(value))
if (config === undefined) return
const terminal = Option.getOrUndefined(decodeRecord(config.terminal))
const legacyCopy = terminal?.copy_on_select
const copy =
typeof legacyCopy === "boolean" && terminal?.copy === undefined ? (legacyCopy ? "select" : "manual") : undefined
const renamed =
typeof legacyCopy !== "boolean"
? text
: [
...(copy === undefined ? [] : [{ path: ["terminal", "copy"], value: copy }]),
{ path: ["terminal", "copy_on_select"], value: undefined },
].reduce(
(text, edit) =>
applyEdits(
text,
modify(text, edit.path, edit.value, { formattingOptions: { tabSize: 2, insertSpaces: true } }),
),
text,
)
const keybinds = Option.getOrUndefined(decodeRecord(config.keybinds))
if (keybinds === undefined) return
const deduped = findKeybindObjects(text)
.slice(0, -1)
.reduce((text) => {
const property = findKeybindObjects(text)[0]
return property === undefined ? text : removeProperty(text, property)
}, text)
const updated = Object.keys(keybinds).reduce((text, name) => {
const target =
TuiKeybind.CommandMap[name as keyof typeof TuiKeybind.CommandMap] ??
(name in Definitions || LegacyKeybindTargets.has(name) ? name : undefined)
if (target === undefined) return text
const properties = findKeybindProperties(text, name)
if (!properties.length) return text
const remove = !(target in Definitions) || (target !== name && target in keybinds)
// The parser gives the final duplicate precedence, so remove earlier properties before renaming it.
const updated = properties.slice(0, remove ? properties.length : -1).reduce((text) => {
const property = findKeybindProperties(text, name)[0]
return property === undefined ? text : removeProperty(text, property)
}, text)
if (remove) return updated
if (target === name) return updated
const key = findKeybindProperties(updated, name)[0]?.children?.[0]
if (key === undefined) return text
return updated.slice(0, key.offset) + JSON.stringify(target) + updated.slice(key.offset + key.length)
}, deduped)
const updated =
keybinds === undefined
? renamed
: Object.keys(keybinds).reduce(
(text, name) => {
const target =
TuiKeybind.CommandMap[name as keyof typeof TuiKeybind.CommandMap] ??
(name in Definitions || LegacyKeybindTargets.has(name) ? name : undefined)
if (target === undefined) return text
const properties = findKeybindProperties(text, name)
if (!properties.length) return text
const remove = !(target in Definitions) || (target !== name && target in keybinds)
// The parser gives the final duplicate precedence, so remove earlier properties before renaming it.
const cleaned = properties.slice(0, remove ? properties.length : -1).reduce((text) => {
const property = findKeybindProperties(text, name)[0]
return property === undefined ? text : removeProperty(text, property)
}, text)
if (remove) return cleaned
if (target === name) return cleaned
const key = findKeybindProperties(cleaned, name)[0]?.children?.[0]
if (key === undefined) return text
return cleaned.slice(0, key.offset) + JSON.stringify(target) + cleaned.slice(key.offset + key.length)
},
findKeybindObjects(renamed)
.slice(0, -1)
.reduce((text) => {
const property = findKeybindObjects(text)[0]
return property === undefined ? text : removeProperty(text, property)
}, renamed),
)
if (updated === text) return
const updatedErrors: ParseError[] = []
const info = Option.getOrUndefined(decodeInfo(parse(updated, updatedErrors, { allowTrailingComma: true })))
+45
View File
@@ -0,0 +1,45 @@
export * as CpuProfile from "./cpu-profile"
import { Effect, FileSystem } from "effect"
import { Session } from "node:inspector"
import path from "node:path"
export function run<A, E, R>(file: string, effect: Effect.Effect<A, E, R>) {
const target = path.resolve(file)
return Effect.acquireUseRelease(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
yield* fs.makeDirectory(path.dirname(target), { recursive: true })
const session = new Session()
session.connect()
yield* command(session, "Profiler.enable")
yield* command(session, "Profiler.start")
yield* Effect.logInfo("CPU profile started", { path: target })
return session
}),
() => effect,
(session) =>
Effect.tryPromise(
() =>
new Promise<void>((resolve, reject) => {
session.post("Profiler.stop", (error, result) => {
session.disconnect()
if (error) return reject(error)
Bun.write(target, JSON.stringify(result.profile)).then(() => resolve(), reject)
})
}),
).pipe(
Effect.andThen(Effect.logInfo("CPU profile written", { path: target })),
Effect.catchCause((cause) => Effect.logError("Failed to write CPU profile", { path: target, cause })),
),
)
}
function command(session: Session, method: "Profiler.enable" | "Profiler.start") {
return Effect.tryPromise(
() =>
new Promise<void>((resolve, reject) => {
session.post(method, (error) => (error ? reject(error) : resolve()))
}),
)
}
+20 -2
View File
@@ -1,10 +1,13 @@
import { Effect, FileSystem, Scope } from "effect"
import { Effect, FileSystem, Option, Scope } from "effect"
import { Command } from "effect/unstable/cli"
import { Spec } from "./spec"
import { Global } from "@opencode-ai/util/global"
import { Updater } from "../services/updater"
import { Config } from "../config"
import { Npm } from "@opencode-ai/util/npm"
import { GlobalFlags } from "../commands/global-flags"
import { CpuProfile } from "../cpu-profile"
import path from "node:path"
export type Input<Value> =
Value extends Spec.Node<infer _Name, infer Command, infer _Commands>
@@ -86,7 +89,22 @@ function provide(node: Spec.Any, handlers: ReadonlyArray<LazyHandler>): Provided
? node.spec.pipe(
Command.withHandler((input) =>
Effect.gen(function* () {
yield* Effect.flatMap(Effect.promise(handler.load), (module) => module.default(input))
const module = yield* Effect.promise(handler.load)
const cpuProfile = Option.getOrUndefined(yield* GlobalFlags.CpuProfile)
if (!cpuProfile) return yield* module.default(input)
const target = path.resolve(cpuProfile)
const previous = process.env.OPENCODE_CPU_PROFILE
process.env.OPENCODE_CPU_PROFILE = target
return yield* (
node.name === "serve" ? CpuProfile.run(target, module.default(input)) : module.default(input)
).pipe(
Effect.ensuring(
Effect.sync(() => {
if (previous === undefined) delete process.env.OPENCODE_CPU_PROFILE
else process.env.OPENCODE_CPU_PROFILE = previous
}),
),
)
}),
),
)
+37
View File
@@ -0,0 +1,37 @@
import { Global } from "@opencode-ai/util/global"
import { Effect, Queue } from "effect"
import path from "node:path"
export const listen = Effect.gen(function* () {
const global = yield* Global.Service
if (process.platform === "win32") return
const signals = yield* Queue.dropping<void>(1)
yield* Effect.acquireRelease(
Effect.sync(() => {
const handler = () => Queue.offerUnsafe(signals, undefined)
process.on("SIGUSR1", handler)
return handler
}),
(handler) => Effect.sync(() => process.off("SIGUSR1", handler)),
)
yield* Queue.take(signals).pipe(
Effect.andThen(
Effect.suspend(() => {
const file = path.join(
global.log,
`heap-${process.pid}-${new Date().toISOString().replace(/[:.]/g, "")}.heapsnapshot`,
)
return Effect.gen(function* () {
yield* Effect.logInfo("writing heap snapshot", { path: file })
const { writeHeapSnapshot } = yield* Effect.tryPromise(() => import("node:v8"))
yield* Effect.try(() => writeHeapSnapshot(file))
yield* Effect.logInfo("heap snapshot written", { path: file })
}).pipe(Effect.catchCause((cause) => Effect.logError("failed to write heap snapshot", { path: file, cause })))
}),
),
Effect.forever,
Effect.forkScoped({ startImmediately: true }),
)
})
export * as Heap from "./heap"
+10 -6
View File
@@ -12,6 +12,7 @@ import { Global } from "@opencode-ai/util/global"
import { AppProcess } from "@opencode-ai/util/process"
import { Config } from "./config"
import { Npm } from "@opencode-ai/util/npm"
import { Heap } from "./heap"
const Handlers = Runtime.handlers(Commands, {
$: () => import("./commands/handlers/default"),
@@ -54,13 +55,16 @@ const Handlers = Runtime.handlers(Commands, {
serve: () => import("./commands/handlers/serve"),
})
Effect.logInfo("cli starting", {
version: OPENCODE_VERSION,
channel: OPENCODE_CHANNEL,
local: OPENCODE_LOCAL,
args: process.argv.slice(2),
Effect.gen(function* () {
yield* Heap.listen
yield* Effect.logInfo("cli starting", {
version: OPENCODE_VERSION,
channel: OPENCODE_CHANNEL,
local: OPENCODE_LOCAL,
args: process.argv.slice(2),
})
return yield* Runtime.run(Commands, Handlers, { version: OPENCODE_VERSION })
}).pipe(
Effect.flatMap(() => Runtime.run(Commands, Handlers, { version: OPENCODE_VERSION })),
Effect.annotateLogs({ role: "cli" }),
Effect.provide(Config.layer),
Effect.provide(Updater.layer),
+1 -1
View File
@@ -84,7 +84,7 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
database: {
path:
process.env.OPENCODE_DB ??
(["latest", "beta", "next", "prod"].includes(OPENCODE_CHANNEL) ||
(["latest", "dev", "beta", "next", "prod"].includes(OPENCODE_CHANNEL) ||
process.env.OPENCODE_DISABLE_CHANNEL_DB === "1" ||
process.env.OPENCODE_DISABLE_CHANNEL_DB === "true"
? "opencode.db"
+8 -3
View File
@@ -25,12 +25,12 @@ const decodeInfo = Schema.decodeUnknownEffect(Schema.fromJsonString(Info))
const decodeRegistration = Schema.decodeUnknownEffect(Schema.fromJsonString(Service.Info))
export function filename(channel = OPENCODE_CHANNEL) {
if (channel === "latest" || channel === "next") return "service.json"
if (channel === "latest" || channel === "dev" || channel === "beta" || channel === "next") return "service.json"
return `service-${channel.replace(/[^a-zA-Z0-9._-]/g, "-")}.json`
}
export function defaultPort(channel = OPENCODE_CHANNEL) {
if (channel === "latest" || channel === "next") return 0xc0de
if (channel === "latest" || channel === "dev" || channel === "beta" || channel === "next") return 0xc0de
if (channel === "local") return 0xc0df
return 10_000 + (Number.parseInt(Hash.fast(channel).slice(0, 8), 16) % 50_000)
}
@@ -104,7 +104,12 @@ export const options = Effect.fnUntraced(function* (input: { readonly checkVersi
return {
file,
version: input.checkVersion ? OPENCODE_VERSION : undefined,
command: [...selfCommand(), "serve", "--service"],
command: [
...selfCommand(),
"serve",
"--service",
...(process.env.OPENCODE_CPU_PROFILE ? ["--cpu-profile", process.env.OPENCODE_CPU_PROFILE] : []),
],
}
})
+2 -1
View File
@@ -35,7 +35,8 @@ describe("updater", () => {
test("accepts strict release version variants", () => {
expect(action("v1.2.3", " 1.2.4\n", true)).toBe("upgrade")
expect(action("1.2.3-alpha.1", "1.2.3-alpha.2", true)).toBe("upgrade")
expect(action("0.0.0-next-17403", "0.0.0-next-17403.2", true)).toBe("upgrade")
expect(action("0.0.0-dev-17403", "0.0.0-dev-17403.2", true)).toBe("upgrade")
expect(action("0.0.0-next-17403", "0.0.0-beta-17404", true)).toBe("upgrade")
expect(action("1.2.3+old", "1.2.3+new", true)).toBe("none")
expect(action("v1.2.3+old", "1.2.3", true)).toBe("none")
})
+40
View File
@@ -210,6 +210,46 @@ test("migrates legacy keybind names in an existing cli.json", async () => {
}
})
test("migrates copy_on_select in an existing cli.json", async () => {
for (const item of [
{ legacy: true, expected: "select" },
{ legacy: false, expected: "manual" },
{ legacy: true, copy: "manual", expected: "manual" },
] as const) {
const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim())
const file = path.join(directory, "cli.json")
await Bun.write(
file,
`{
// Preserve this comment
"terminal": {
${item.copy === undefined ? "" : `"copy": ${JSON.stringify(item.copy)},`}
"copy_on_select": ${item.legacy}
}
}
`,
)
try {
const config = await run(
directory,
Effect.gen(function* () {
const service = yield* Config.Service
return yield* service.get()
}),
)
expect(config.terminal).toEqual({ copy: item.expected })
const text = await Bun.file(file).text()
expect(text).toContain("// Preserve this comment")
expect(text).not.toContain("copy_on_select")
expect(parse(text).terminal).toEqual({ copy: item.expected })
} finally {
await Bun.$`rm -rf ${directory}`
}
}
})
test("uses migrated keybinds when persistence fails", async () => {
const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim())
const file = path.join(directory, "cli.json")
+2 -1
View File
@@ -10,9 +10,10 @@ describe("debug config command", () => {
expect(debug.exitCode).toBe(0)
expect(debug.stdout).toContain("config")
expect(debug.stdout).toContain("Show resolved configuration")
expect(debug.stdout).toContain("List configuration sources")
expect(config.exitCode).toBe(0)
expect(config.stdout).toContain("opencode debug config [flags]")
expect(config.stdout).toContain("List configuration sources")
})
test("prints config entries from the invoking directory without reordering permissions", async () => {
@@ -14,7 +14,7 @@ await Effect.runPromise(
const response = yield* Effect.promise(() =>
fetch(new URL("/api/health", endpoint.url), { headers: Service.headers(endpoint) }),
)
console.log(`${endpoint.pid} ${endpoint.url} ${response.status}`)
console.log(`STANDALONE_READY ${endpoint.pid} ${endpoint.url} ${response.status}`)
return yield* Effect.never
}),
),
+27
View File
@@ -11,12 +11,37 @@ import { ServiceConfig } from "../src/services/service-config"
test("managed service ports are stable per installation channel", () => {
expect(ServiceConfig.defaultPort("latest")).toBe(0xc0de)
expect(ServiceConfig.defaultPort("dev")).toBe(0xc0de)
expect(ServiceConfig.defaultPort("beta")).toBe(0xc0de)
expect(ServiceConfig.defaultPort("next")).toBe(0xc0de)
expect(ServiceConfig.defaultPort("local")).toBe(0xc0df)
expect(ServiceConfig.defaultPort("preview-a")).toBe(ServiceConfig.defaultPort("preview-a"))
expect(ServiceConfig.defaultPort("preview-a")).not.toBe(ServiceConfig.defaultPort("preview-b"))
})
test("managed service forwards the CPU profile path to the server", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-profile-"))
const profile = path.join(root, "server.cpuprofile")
try {
const previous = process.env.OPENCODE_CPU_PROFILE
process.env.OPENCODE_CPU_PROFILE = profile
try {
const options = await Effect.runPromise(
ServiceConfig.options().pipe(
Effect.provide(Global.layerWith({ config: path.join(root, "config"), state: path.join(root, "state") })),
Effect.provide(NodeFileSystem.layer),
),
)
expect(options.command.slice(-2)).toEqual(["--cpu-profile", profile])
} finally {
if (previous === undefined) delete process.env.OPENCODE_CPU_PROFILE
else process.env.OPENCODE_CPU_PROFILE = previous
}
} finally {
await fs.rm(root, { recursive: true, force: true })
}
})
test("local channel stores service config with the local service filename", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-"))
try {
@@ -37,6 +62,8 @@ test("local channel stores service config with the local service filename", asyn
test("service filenames share release channels and identify preview channels", () => {
expect(ServiceConfig.filename("latest")).toBe("service.json")
expect(ServiceConfig.filename("dev")).toBe("service.json")
expect(ServiceConfig.filename("beta")).toBe("service.json")
expect(ServiceConfig.filename("next")).toBe("service.json")
expect(ServiceConfig.filename("local")).toBe("service-local.json")
expect(ServiceConfig.filename("preview-a")).toBe("service-preview-a.json")
+10 -7
View File
@@ -9,8 +9,11 @@ test("standalone server exits when its owner is killed", async () => {
stdout: "pipe",
stderr: "pipe",
})
const line = await Promise.race([readLine(owner.stdout), Bun.sleep(10_000).then(() => undefined)])
const [rawPID, url, status] = line?.split(" ") ?? []
const line = await Promise.race([
readLine(owner.stdout, "STANDALONE_READY "),
Bun.sleep(10_000).then(() => undefined),
])
const [, rawPID, url, status] = line?.split(" ") ?? []
const pid = Number(rawPID)
try {
@@ -29,7 +32,7 @@ test("standalone server exits when its owner is killed", async () => {
}
})
async function readLine(stream: ReadableStream<Uint8Array>) {
async function readLine(stream: ReadableStream<Uint8Array>, prefix: string) {
const reader = stream.getReader()
const decoder = new TextDecoder()
const chunks: string[] = []
@@ -38,14 +41,14 @@ async function readLine(stream: ReadableStream<Uint8Array>) {
if (result.done) break
chunks.push(decoder.decode(result.value, { stream: true }))
const output = chunks.join("")
const newline = output.indexOf("\n")
if (newline !== -1) {
const line = output.split("\n").find((line) => line.startsWith(prefix))
if (line) {
reader.releaseLock()
return output.slice(0, newline)
return line
}
}
reader.releaseLock()
return chunks.join("") + decoder.decode()
return (chunks.join("") + decoder.decode()).split("\n").find((line) => line.startsWith(prefix))
}
async function waitForExit(pid: number, attempts = 100): Promise<boolean> {
+40 -41
View File
@@ -20,7 +20,6 @@ import type { InstructionEntry } from "@opencode-ai/schema/instruction-entry"
import type { Schema } from "effect"
import type { EventLog } from "@opencode-ai/schema/event-log"
import type { Shell } from "@opencode-ai/schema/shell"
import type { DateTime } from "effect"
import type { Provider } from "@opencode-ai/schema/provider"
import type { Integration } from "@opencode-ai/schema/integration"
import type { Form } from "@opencode-ai/schema/form"
@@ -316,7 +315,7 @@ export type Endpoint5_31Output =
| (
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.created"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -336,7 +335,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.agent.selected"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -349,7 +348,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.model.selected"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -362,7 +361,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.moved"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -376,7 +375,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.renamed"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -385,7 +384,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.deleted"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -394,7 +393,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.forked"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -410,7 +409,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.inbox.delivered"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -419,7 +418,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.inbox.enqueued"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -432,7 +431,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.inbox.cancelled"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -441,7 +440,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.inbox.delivery.changed"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -454,7 +453,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.execution.started"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -463,7 +462,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.execution.succeeded"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -472,7 +471,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.execution.failed"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -484,7 +483,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.execution.interrupted"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -493,7 +492,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.instructions.updated"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -506,7 +505,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.synthetic"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -520,7 +519,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.skill.activated"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -534,7 +533,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.shell.started"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -543,7 +542,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.shell.ended"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -561,7 +560,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.step.started"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -576,7 +575,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.step.ended"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -598,7 +597,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.step.failed"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -622,7 +621,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.text.started"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -635,7 +634,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.text.ended"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -650,7 +649,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.reasoning.started"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -664,7 +663,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.reasoning.ended"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -679,7 +678,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.tool.input.started"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -693,7 +692,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.tool.input.ended"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -707,7 +706,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.tool.called"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -723,7 +722,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.tool.success"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -759,7 +758,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.tool.failed"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -798,7 +797,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.retry.scheduled"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -813,7 +812,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.compaction.started"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -827,7 +826,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.compaction.ended"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -841,7 +840,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.compaction.failed"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -855,7 +854,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.revert.staged"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -864,7 +863,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.revert.cleared"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -873,7 +872,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.revert.committed"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -882,7 +881,7 @@ export type Endpoint5_31Output =
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.usage.recorded"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
@@ -192,7 +192,7 @@ export type ProviderInfo = {
id: string
integrationID?: string
name: string
disabled?: boolean
activation: "auto" | "enabled" | "disabled"
package: string
settings?: { [x: string]: any }
headers?: { [x: string]: string }
@@ -21,7 +21,7 @@ export default function TermsOfService() {
<section data-component="brand-content">
<article data-component="terms-of-service">
<h1>Terms of Use</h1>
<p class="effective-date">Effective date: Mar 6, 2026</p>
<p class="effective-date">Effective date: Aug 15, 2026</p>
<p>
Welcome to OpenCode. Please read on to learn the rules and restrictions that govern your use of
@@ -154,6 +154,11 @@ export default function TermsOfService() {
is dangerous, harmful, fraudulent, deceptive, threatening, harassing, defamatory, obscene, or
otherwise objectionable;
</li>
<li>
creates, maintains, or uses accounts in bulk, or creates, maintains, or uses multiple accounts to
circumvent usage limits, access restrictions, billing obligations, promotions, suspensions, or any
other restriction or policy applicable to the Services;
</li>
<li>automatically or programmatically extracts data or Output (defined below);</li>
<li>Represent that the Output was human-generated when it was not;</li>
<li>
+43 -5
View File
@@ -435,7 +435,22 @@ function prompt(request: LLMRequest): LanguageModelV3Prompt {
.map((part) => part.text)
.filter(Boolean)
.join("\n\n")
const messages = request.messages.flatMap(message)
const pending: UserContent = []
const messages = request.messages.flatMap((input, index) => {
if (input.role !== "tool") return message(input)
const lowered = toolMessage(input)
pending.push(...lowered.media)
if (request.messages[index + 1]?.role === "tool" || pending.length === 0) return lowered.messages
const media = [...pending]
pending.length = 0
return [
...lowered.messages,
{
role: "user" as const,
content: [{ type: "text" as const, text: "Attached media from tool result:" }, ...media],
},
]
})
if (!system.length) return messages
return [{ role: "system", content: system }, ...messages]
}
@@ -448,10 +463,33 @@ function message(input: LLMRequest["messages"][number]): LanguageModelV3Message[
return [{ role: "user", content: input.content.flatMap(userPart) }]
case "assistant":
return [{ role: "assistant", content: input.content.flatMap(assistantPart) }]
case "tool": {
const content = input.content.flatMap(toolResultPart)
return content.length ? [{ role: "tool", content }] : []
}
case "tool":
return toolMessage(input).messages
}
}
function toolMessage(input: LLMRequest["messages"][number]) {
const media: UserContent = []
const content = input.content.flatMap((part) => {
if (part.type !== "tool-result" || part.result.type !== "content") return toolResultPart(part)
const value = part.result.value.filter((item) => {
if (item.type !== "file") return true
if (!item.mime.startsWith("image/") && item.mime !== "application/pdf") return true
const data = /^data:[^;,]+(?:;[^,]*)*;base64,(.*)$/s.exec(item.uri)?.[1] ?? item.uri
media.push({ type: "file", mediaType: item.mime, data, filename: item.name })
return false
})
return toolResultPart({
...part,
result:
value.length === 0
? { type: "text", value: "Media attached in the following user message." }
: { ...part.result, value },
})
})
return {
messages: content.length ? ([{ role: "tool", content }] satisfies LanguageModelV3Message[]) : [],
media,
}
}
+10 -10
View File
@@ -1,6 +1,6 @@
export * as Bus from "./bus.js"
import { Cause, Context, DateTime, Effect, Layer, Option, PubSub, Schema, Stream } from "effect"
import { Cause, Clock, Context, Effect, Layer, Option, PubSub, Schema, Stream } from "effect"
import { Event } from "@opencode-ai/schema/event"
import type { EventLog } from "@opencode-ai/schema/event-log"
import { and, asc, eq, gt, lte, sql } from "drizzle-orm"
@@ -47,7 +47,7 @@ export const reserveSequence = Effect.fn("Bus.reserveSequence")(function* (
export type SerializedEvent = {
readonly id: Event.ID
readonly type: string
readonly created?: DateTime.Utc
readonly created?: number
readonly seq: number
readonly aggregateID: string
readonly data: Record<string, unknown>
@@ -74,7 +74,7 @@ const decodeSerializedEvent = (event: SerializedEvent): Event.Payload => {
}
return {
id: event.id,
created: event.created ?? DateTime.makeUnsafe(0),
created: event.created ?? 0,
type: definition.type,
durable: envelope(event.aggregateID, event.seq, definition.durable.version),
data: Schema.decodeUnknownSync(definition.data)(event.data),
@@ -283,7 +283,7 @@ export function configured(options?: Options) {
if (
stored?.id === event.id &&
stored.type === versionedType(definition.type, durable.version) &&
stored.created === DateTime.toEpochMillis(event.created ?? DateTime.makeUnsafe(0)) &&
stored.created === (event.created ?? 0) &&
isDeepStrictEqual(stored.data, encoded)
) {
if (input.ownerID && row?.ownerID == null) {
@@ -358,7 +358,7 @@ export function configured(options?: Options) {
id: event.id,
aggregate_id: aggregateID,
seq,
created: DateTime.toEpochMillis(event.created ?? DateTime.makeUnsafe(0)),
created: event.created ?? 0,
type: versionedType(definition.type, durable.version),
data: encoded,
},
@@ -455,7 +455,7 @@ export function configured(options?: Options) {
definition,
{
id: options?.id ?? Event.ID.create(),
created: yield* DateTime.now,
created: yield* Clock.currentTimeMillis,
...(options?.metadata ? { metadata: options.metadata } : {}),
type: definition.type,
...(location ? { location } : {}),
@@ -491,7 +491,7 @@ export function configured(options?: Options) {
commit: options?.commit,
event: {
id: options?.id ?? Event.ID.create(),
created: yield* DateTime.now,
created: yield* Clock.currentTimeMillis,
...(options?.metadata ? { metadata: options.metadata } : {}),
type: definition.type,
...(location ? { location } : {}),
@@ -571,7 +571,7 @@ export function configured(options?: Options) {
id: event.id,
aggregate_id: aggregateID,
seq,
created: DateTime.toEpochMillis(event.created),
created: event.created,
type: versionedType(item.definition.type, item.definition.durable.version),
data: encoded,
})
@@ -619,7 +619,7 @@ export function configured(options?: Options) {
Effect.gen(function* () {
const payload = {
id: event.id,
created: event.created ?? DateTime.makeUnsafe(0),
created: event.created ?? 0,
type: definition.type,
data: Schema.decodeUnknownSync(definition.data)(event.data),
} as Event.Payload
@@ -733,7 +733,7 @@ export function configured(options?: Options) {
return [
decodeSerializedEvent({
id: event.id,
created: DateTime.makeUnsafe(event.created),
created: event.created,
aggregateID: event.aggregate_id,
seq: event.seq,
type: event.type,
+2 -2
View File
@@ -65,8 +65,8 @@ const layer = Layer.effect(
const integrations = yield* Integration.Service
const available = (provider: Provider.Info, integration: Integration.Info | undefined) => {
if (provider.disabled) return false
if (typeof provider.settings?.apiKey === "string") return true
if (provider.activation === "disabled") return false
if (provider.activation === "enabled") return true
if (integration?.connections.length) return true
return provider.integrationID === undefined && !integration
}
+2 -2
View File
@@ -13,7 +13,7 @@ import { ConfigAgentV1 } from "../../v1/config/agent.js"
import { ConfigMigrateV1 } from "../../v1/config/migrate.js"
import { Global } from "@opencode-ai/util/global"
import { Permission } from "../../permission.js"
import type { LocationPath } from "../../location-path.js"
import type { LocationMutation } from "../../location-mutation.js"
import type { ReadTool } from "../../tool/plugin/read.js"
import type { EditTool } from "../../tool/plugin/edit.js"
import { AbsolutePath } from "../../schema.js"
@@ -28,7 +28,7 @@ const decodeAgent = Schema.decodeUnknownOption(ConfigAgent.Info)
const decodeLegacyAgent = Schema.decodeUnknownOption(ConfigAgentV1.Info)
const decodeConfig = Schema.decodeUnknownOption(Info)
type PathAction =
| LocationPath.ExternalDirectoryAuthorization["action"]
| LocationMutation.ExternalDirectoryAuthorization["action"]
| typeof ReadTool.name
| typeof EditTool.name
const pathActions = ["external_directory", "read", "edit"] as const satisfies readonly PathAction[]
@@ -40,6 +40,7 @@ export const Plugin = define({
for (const [id, item] of configuredProviders(loaded.entries)) {
const providerID = id
catalog.provider.update(providerID, (provider) => {
provider.activation = "enabled"
if (item.name !== undefined) provider.name = item.name
if (item.package !== undefined) provider.package = item.package
if (item.settings !== undefined) provider.settings = Provider.mergeOverlay(provider.settings, item.settings)
@@ -1,4 +1,4 @@
export * as LocationPath from "./location-path.js"
export * as LocationMutation from "./location-mutation.js"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import path from "path"
@@ -12,8 +12,8 @@ export const Kind = Schema.Literals(["file", "directory"])
export type Kind = typeof Kind.Type
/**
* Tool paths do not accept project references. Relative paths resolve from
* the active Location. Paths outside its project require separate
* Mutation paths do not accept project references. Relative paths resolve
* from the active Location. Paths outside it require separate
* `external_directory` approval.
*/
export const ResolveInput = Schema.Struct({
@@ -49,13 +49,13 @@ export interface Target {
export interface Interface {
/**
* Resolve a path and derive its permission resources. Relative paths resolve
* from the Location. Paths outside its project require separate
* `external_directory` approval. This does not approve access.
* from the Location. Paths outside it require separate `external_directory`
* approval. This does not approve the mutation.
*/
readonly resolve: (input: ResolveInput) => Effect.Effect<Target, FSUtil.Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/LocationPath") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/LocationMutation") {}
const slash = (value: string) => value.replaceAll("\\", "/")
@@ -65,13 +65,9 @@ const layer = Layer.effect(
const fs = yield* FSUtil.Service
const location = yield* Location.Service
const resolve = Effect.fn("LocationPath.resolve")(function* (input: ResolveInput) {
const resolve = Effect.fn("LocationMutation.resolve")(function* (input: ResolveInput) {
const absolute = path.resolve(location.directory, input.path)
const projectRoot = path.parse(location.project.directory).root
if (
FSUtil.contains(location.directory, absolute) ||
(location.project.directory !== projectRoot && FSUtil.contains(location.project.directory, absolute))
) {
if (FSUtil.contains(location.directory, absolute)) {
return {
absolute,
resource: slash(path.relative(location.directory, absolute) || "."),
+2 -2
View File
@@ -19,7 +19,7 @@ import { Image } from "./image.js"
import { LocationWatcher } from "./filesystem/location-watcher.js"
import { Integration } from "./integration.js"
import { Location } from "./location.js"
import { LocationPath } from "./location-path.js"
import { LocationMutation } from "./location-mutation.js"
import { LocationServiceMap } from "./location-service-map.js"
import { ModelResolver } from "./model-resolver.js"
import { MCP } from "./mcp/index.js"
@@ -76,7 +76,7 @@ const locationServiceNodes = [
Skill.node,
InstructionBuiltIns.node,
InstructionDiscovery.node,
LocationPath.node,
LocationMutation.node,
FileMutation.node,
Formatter.node,
MCP.node,
+43 -10
View File
@@ -358,17 +358,21 @@ export const layer = Layer.effect(
const connection = yield* integrations.connection.active(
provider?.integrationID ?? Integration.ID.make(selected.providerID),
)
const model = yield* resolveModel(
selected,
variant,
connection ? yield* integrations.connection.resolve(connection) : undefined,
{
loadPackage: (specifier) => Provider.loadPackage(specifier, npm),
loadAISDK: (model) => aisdk.model(model),
},
)
const credential = connection ? yield* integrations.connection.resolve(connection) : undefined
const runtimeInfo = yield* withVariant(selected, variant)
const model = yield* fromCatalogModel(runtimeInfo, credential, {
loadPackage: (specifier) => Provider.loadPackage(specifier, npm),
loadAISDK: (model) => aisdk.model(model),
})
const runtime =
provider?.activation === "enabled" &&
credential === undefined &&
!hasConfiguredAuth(runtimeInfo) &&
usesAPIKeyAuth(runtimeInfo.package)
? LanguageModel.update(model, { route: model.route.with({ auth: Auth.none }) })
: model
return {
model,
model: runtime,
ref: Ref.make({
id: selected.id,
providerID: selected.providerID,
@@ -399,6 +403,35 @@ export const layer = Layer.effect(
}),
)
function hasConfiguredAuth(model: Info) {
return [model.settings?.apiKey, model.settings?.authToken, model.settings?.accessToken].some(
(value) => typeof value === "string" && value !== "",
)
}
function usesAPIKeyAuth(packageName: string | undefined) {
const name = Provider.packageName(packageName)
return (
name === "@ai-sdk/openai" ||
name === "@ai-sdk/anthropic" ||
name === "@ai-sdk/openai-compatible" ||
name === "@ai-sdk/google" ||
name === "@ai-sdk/xai" ||
name === "@openrouter/ai-sdk-provider" ||
name === "@ai-sdk/azure" ||
name === "@opencode-ai/ai/providers/openai" ||
name?.startsWith("@opencode-ai/ai/providers/openai/") === true ||
name === "@opencode-ai/ai/providers/anthropic" ||
name === "@opencode-ai/ai/providers/anthropic-compatible" ||
name === "@opencode-ai/ai/providers/openai-compatible" ||
name === "@opencode-ai/ai/providers/google" ||
name === "@opencode-ai/ai/providers/xai" ||
name === "@opencode-ai/ai/providers/openrouter" ||
name === "@opencode-ai/ai/providers/azure" ||
name?.startsWith("@opencode-ai/ai/providers/azure/") === true
)
}
export const node = makeLocationNode({
service: Service,
layer,
+1
View File
@@ -86,6 +86,7 @@ function normalize(input: Record<string, SourceProvider>): readonly Snapshot[] {
const info = {
id: providerID,
name: item.name,
activation: "auto",
package: Provider.aisdk(item.npm),
...(item.api ? { settings: { baseURL: item.api } } : {}),
} satisfies Provider.Info
+4 -4
View File
@@ -32,7 +32,7 @@ import { InstructionDiscovery } from "../instruction-discovery.js"
import { Integration } from "../integration.js"
import { KV } from "../kv.js"
import { Location } from "../location.js"
import { LocationPath } from "../location-path.js"
import { LocationMutation } from "../location-mutation.js"
import { ModelsDev } from "../models-dev.js"
import { Npm } from "@opencode-ai/util/npm"
import { Permission } from "../permission.js"
@@ -92,7 +92,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
const integration = yield* Integration.Service
const kv = yield* KV.Service
const location = yield* Location.Service
const locationMutation = yield* LocationPath.Service
const locationMutation = yield* LocationMutation.Service
const models = yield* ModelsDev.Service
const npm = yield* Npm.Service
const permission = yield* Permission.Service
@@ -129,7 +129,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
Context.make(Integration.Service, integration),
Context.make(KV.Service, kv),
Context.make(Location.Service, location),
Context.make(LocationPath.Service, locationMutation),
Context.make(LocationMutation.Service, locationMutation),
Context.make(ModelsDev.Service, models),
Context.make(Npm.Service, npm),
Context.make(Permission.Service, permission),
@@ -173,7 +173,7 @@ export const requirements = LayerNode.group([
Integration.node,
KV.node,
Location.node,
LocationPath.node,
LocationMutation.node,
ModelsDev.node,
Npm.node,
Permission.node,
+31 -3
View File
@@ -1,6 +1,6 @@
export * as PlanPlugin from "./plan.js"
import { ToolFailure } from "@opencode-ai/ai"
import { Message, ToolFailure } from "@opencode-ai/ai"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Effect, Stream } from "effect"
import { Agent } from "../agent.js"
@@ -38,13 +38,33 @@ export const Plugin = define({
})
})
// Compaction and committed reverts can strip reminders while the session's agent stays
// put. Reconcile per request, appending near the tail so the cached prefix stays warm.
yield* ctx.session.hook("context", (event) => {
const reminder = lastReminder(event.messages)
const missing = event.agent === plan && reminder !== enter
const stale = event.agent !== plan && reminder === enter
const text = missing ? enter : stale ? leave : undefined
if (!text) return Effect.void
// Before the user's prompt, matching where agent-switch reminders land.
const at = event.messages.at(-1)?.role === "user" ? event.messages.length - 1 : event.messages.length
event.messages.splice(at, 0, Message.user(text))
return ctx.session
.synthetic({ sessionID: event.sessionID, text, resume: false })
.pipe(
Effect.catchCause((cause) =>
Effect.logWarning("failed to persist Plan mode reminder", { sessionID: event.sessionID, cause }),
),
)
})
yield* ctx.event.subscribe().pipe(
Stream.filter(
(event): event is SessionEvent.Created | SessionEvent.AgentSelected =>
event.type === "session.created" || event.type === "session.agent.selected",
),
Stream.runForEach((event) => {
const text = reminder(event)
const text = switchReminder(event)
if (!text) return Effect.void
return ctx.session
.synthetic({
@@ -63,7 +83,7 @@ export const Plugin = define({
}),
})
function reminder(event: SessionEvent.Created | SessionEvent.AgentSelected) {
function switchReminder(event: SessionEvent.Created | SessionEvent.AgentSelected) {
if (event.type === "session.created") {
if (event.data.agent !== plan) return
return enter
@@ -72,3 +92,11 @@ function reminder(event: SessionEvent.Created | SessionEvent.AgentSelected) {
if (event.data.agent === plan) return enter
if (event.data.previous === plan) return leave
}
function lastReminder(messages: ReadonlyArray<Message>) {
return messages.reduce<string | undefined>((found, message) => {
const part = message.role === "user" && message.content.length === 1 ? message.content[0] : undefined
if (part?.type !== "text") return found
return part.text === enter || part.text === leave ? part.text : found
}, undefined)
}
@@ -10,7 +10,7 @@ export const LLMGatewayPlugin = define({
const configured = new Set((yield* integrations.list()).map((integration) => integration.id))
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (item.provider.disabled) continue
if (item.provider.activation === "disabled") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://api.llmgateway.io/v1") continue
@@ -5,6 +5,7 @@ import { App } from "../../app.js"
import { Credential } from "../../credential.js"
import { Bus } from "../../bus.js"
import { Integration } from "../../integration.js"
import { Model } from "../../model.js"
import { OauthCallbackPage } from "../../oauth/page.js"
import { Provider } from "../../provider.js"
import type { PluginInternal } from "../internal.js"
@@ -229,6 +230,16 @@ export const OpenAIPlugin = define({
})
}
})
yield* ctx.session.hook("http.request", (evt) =>
Effect.sync(() => {
if (!chatgpt || evt.model.providerID !== Provider.ID.openai) return
const url = new URL(evt.request.url)
evt.request.headers.set("originator", "opencode")
evt.request.headers.set("session-id", evt.sessionID)
if (url.origin !== "https://api.openai.com") return
evt.request = new Request(`${codexBaseURL}${url.pathname.replace(/^\/v1/, "")}${url.search}`, evt.request)
}),
)
const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload())))
yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filter((event) => event.data.integrationID === Integration.ID.make("openai")),
@@ -178,7 +178,10 @@ export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Scope
if (!item) return
const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.settings?.apiKey)
catalog.provider.update(item.provider.id, (provider) => {
if (!hasKey) provider.settings = { ...provider.settings, apiKey: "public" }
if (!hasKey) {
provider.activation = "enabled"
provider.settings = { ...provider.settings, apiKey: "public" }
}
})
if (hasKey) return
for (const model of item.models.values()) {
+28 -1
View File
@@ -63,12 +63,39 @@ examples, but do not fetch it to determine the V2 configuration shape.
See the [full configuration guide](https://opencode.ai/v2/docs/config) for
every field, examples, config locations, and links to dedicated feature guides.
## [MCP servers](https://opencode.ai/v2/docs/mcp-servers)
Configure MCP servers under `mcp.servers`. Prefer the CLI because it preserves
unrelated configuration. Use `--global` when the user asks to set up a service
for themselves without limiting it to the current project; omit it when they
explicitly want project-local configuration.
```sh
opencode2 mcp add <name> --global --url <remote-url>
opencode2 mcp list
```
Remote servers use OAuth by default. If `mcp list` reports that a server needs
authentication, run the OAuth flow and then verify the connection:
```sh
opencode2 mcp auth <name>
opencode2 mcp list
```
The auth command prints an authorization URL, waits for the browser redirect,
and stores credentials outside the OpenCode configuration. Do not ask for or
store an API key when the server supports OAuth. Use header-based credentials
only when OAuth is unavailable or the user explicitly requires them, and use an
environment substitution such as `{env:MCP_API_KEY}` instead of writing a
secret into configuration.
## [V1 to V2 migration](https://opencode.ai/v2/docs/migrate-v1)
For any request to migrate OpenCode configuration, agents, commands, skills,
plugins, integrations, or other behavior from V1 to V2, read the full
[migration guide](https://opencode.ai/v2/docs/migrate-v1) before acting. In
the repository, its source is `packages/www/content/docs/(Get started)/migrate-v1.mdx`.
the repository, its source is `packages/www/content/docs/migrate-v1.mdx`.
V1 config files and `.opencode/` definitions are intended to remain compatible.
The only intentional breaking changes are the server API and plugin API. Native
+5 -7
View File
@@ -789,7 +789,10 @@ const layer = Layer.effect(
return false
}),
)
if (recovered) return
if (recovered) {
yield* execution.wakeActive(input.sessionID)
return
}
yield* execution.wake(input.sessionID)
}),
compact: Effect.fn("Session.compact")(function* (input) {
@@ -873,12 +876,7 @@ const layer = Layer.effect(
),
),
interrupt: Effect.fn("Session.interrupt")((sessionID, options) =>
Effect.uninterruptible(
Effect.gen(function* () {
yield* execution.interrupt(sessionID)
if (options?.continue && (yield* SessionInbox.has(db, sessionID, "any"))) yield* execution.wake(sessionID)
}),
),
Effect.uninterruptible(execution.interrupt(sessionID, options)),
),
revert: {
stage: Effect.fn("Session.revert.stage")(function* (input) {
+22 -11
View File
@@ -1,7 +1,8 @@
export * as SessionExecution from "./execution.js"
import { Cause, Context, Effect, Exit, Layer, Stream } from "effect"
import { Cause, Context, Effect, Exit, Layer } from "effect"
import { Bus } from "../bus.js"
import { Database } from "../database/database.js"
import { LocationServiceMap } from "../location-service-map.js"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { SessionEvent } from "./event.js"
@@ -11,6 +12,7 @@ import { SessionSchema } from "./schema.js"
import { SessionStore } from "./store.js"
import { toSessionError } from "./to-session-error.js"
import { UserInterruptedError } from "./error.js"
import { SessionInbox } from "./inbox.js"
export interface Interface {
/** Snapshots active execution owned by this process. */
@@ -19,8 +21,10 @@ export interface Interface {
readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect<void, SessionRunner.RunError>
/** Registers newly recorded work. Repeated wakeups may coalesce. */
readonly wake: (sessionID: SessionSchema.ID) => Effect.Effect<void>
/** Wakes only an active execution, preserving its current input eligibility. */
readonly wakeActive: (sessionID: SessionSchema.ID) => Effect.Effect<void>
/** Interrupt active work owned by this process. Idle interruption is a no-op. */
readonly interrupt: (sessionID: SessionSchema.ID) => Effect.Effect<void>
readonly interrupt: (sessionID: SessionSchema.ID, options?: { readonly continue?: boolean }) => Effect.Effect<void>
/** Resolves once this process owns no active execution for the Session. Returns immediately when idle and never starts work. */
readonly awaitIdle: (sessionID: SessionSchema.ID) => Effect.Effect<void>
}
@@ -45,6 +49,7 @@ export const layer = Layer.effect(
const store = yield* SessionStore.Service
const locations = yield* LocationServiceMap.Service
const bus = yield* Bus.Service
const db = (yield* Database.Service).db
const reportLifecycle = <A>(sessionID: SessionSchema.ID, effect: Effect.Effect<A>) =>
effect.pipe(
Effect.tapCause((cause) =>
@@ -71,12 +76,13 @@ export const layer = Layer.effect(
sessionID: SessionSchema.ID,
force: boolean,
continuation?: SessionRunner.Continuation,
promotable: SessionInbox.Promotable = "input",
): Effect.Effect<void, SessionRunner.RunError> {
return Effect.gen(function* () {
const session = yield* store.get(sessionID)
if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`))
const result = yield* SessionRunner.Service.use((runner) =>
runner.drain({ sessionID, force, continuation }),
runner.drain({ sessionID, force, continuation, promotable }),
).pipe(
Effect.provide(locations.get(session.location)),
Effect.tapCause((cause) =>
@@ -86,7 +92,7 @@ export const layer = Layer.effect(
),
)
if (result.type === "complete") return
return yield* drain(sessionID, false, result.continuation)
return yield* drain(sessionID, false, result.continuation, promotable)
})
}
const coordinator = yield* SessionRunCoordinator.make<SessionSchema.ID, SessionRunner.RunError, InterruptReason>({
@@ -95,7 +101,7 @@ export const layer = Layer.effect(
sessionID,
bus.publish(SessionEvent.Execution.Started, { sessionID }, claimOnCommit(sessionID)),
),
drain: (sessionID, force) => drain(sessionID, force),
drain: (sessionID, force, promotable) => drain(sessionID, force, undefined, promotable),
// One terminal observation per busy period, covering every coalesced drain.
settled: (sessionID, exit, reason) =>
reportLifecycle(
@@ -127,16 +133,20 @@ export const layer = Layer.effect(
}),
),
})
yield* bus.subscribe(SessionEvent.Moved).pipe(
Stream.runForEach((event) => coordinator.wake(event.data.sessionID)),
Effect.forkScoped,
)
return Service.of({
active: coordinator.active,
interrupt: (sessionID) => coordinator.interrupt(sessionID, "user"),
interrupt: (sessionID, options) =>
coordinator.interrupt(
sessionID,
"user",
options?.continue
? { continue: { request: "steer", when: SessionInbox.has(db, sessionID, "steer") } }
: undefined,
),
resume: coordinator.run,
wake: coordinator.wake,
wakeActive: coordinator.wakeActive,
awaitIdle: coordinator.awaitIdle,
})
}),
@@ -145,7 +155,7 @@ export const layer = Layer.effect(
export const node = makeGlobalNode({
service: Service,
layer,
deps: [SessionStore.node, LocationServiceMap.node, Bus.node],
deps: [SessionStore.node, LocationServiceMap.node, Bus.node, Database.node],
})
/** Low-level compatibility layer for callers that only need durable Session recording. */
@@ -155,6 +165,7 @@ export const noopLayer = Layer.succeed(
active: Effect.succeed(new Set()),
resume: () => Effect.void,
wake: () => Effect.void,
wakeActive: () => Effect.void,
interrupt: () => Effect.void,
awaitIdle: () => Effect.void,
}),
@@ -11,6 +11,7 @@ import { SessionContext } from "./context.js"
import { SessionGenerate } from "./generate.js"
import { SessionHistory } from "./history.js"
import { SessionModelHeaders } from "./model-headers.js"
import { SessionModelHttp } from "./model-http.js"
import { SessionPromptCacheKey } from "./prompt-cache-key.js"
import { SessionRunnerModel } from "./runner/model.js"
import { SessionSystemPrompt } from "./system-prompt.js"
@@ -79,6 +80,13 @@ export const layer = Layer.effect(
messages: contextEvent.messages,
tools: hookedTools,
}),
{
http: SessionModelHttp.middleware(hooks, {
sessionID: selection.session.id,
agent: selection.agent.id,
model: model.ref,
}),
},
)
yield* Effect.logInfo("session generation usage diagnostic", { usage: response.usage })
return response.text
+11 -3
View File
@@ -161,7 +161,7 @@ export const admit = Effect.fn("SessionInbox.admit")(function* (
const base = {
id: request.id,
sessionID: request.sessionID,
timeCreated: event.created,
timeCreated: DateTime.makeUnsafe(event.created),
}
return Effect.succeed(Info.make({ ...base, ...request.item }))
}),
@@ -196,7 +196,7 @@ export const projectAdmitted = Effect.fn("SessionInbox.projectAdmitted")(functio
readonly id: SessionMessage.ID
readonly sessionID: SessionSchema.ID
readonly item: Item
readonly timeCreated: DateTime.Utc
readonly timeCreated: number
},
) {
const message = yield* db
@@ -222,7 +222,7 @@ export const projectAdmitted = Effect.fn("SessionInbox.projectAdmitted")(functio
: encodeMove(request.item.payload),
delivery: request.item.delivery,
enqueued_seq: request.enqueuedSeq,
time_created: DateTime.toEpochMillis(request.timeCreated),
time_created: request.timeCreated,
})
.onConflictDoNothing()
.returning({ id: SessionInboxTable.id })
@@ -349,6 +349,14 @@ export const nextSteer = Effect.fn("SessionInbox.nextSteer")(function* (
return row ? fromRow(row) : undefined
})
export const nextPromotable = Effect.fn("SessionInbox.nextPromotable")(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
promotable: Promotable,
) {
return (yield* nextSteer(db, sessionID)) ?? (promotable === "input" ? yield* nextQueued(db, sessionID) : undefined)
})
/**
* Which pending rows count: "any" counts every row, while "input" means any
* item in either delivery mode.
+22 -21
View File
@@ -26,6 +26,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
type DraftTool = WritableDraft<SessionMessage.AssistantTool>
type DraftText = WritableDraft<SessionMessage.AssistantText>
type DraftReasoning = WritableDraft<SessionMessage.AssistantReasoning>
const created = DateTime.makeUnsafe(event.created)
const latestTool = (assistant: DraftAssistant | undefined, id?: string) =>
assistant?.content.findLast(
@@ -70,7 +71,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
metadata: event.metadata,
agent: event.data.agent,
previous,
time: { created: event.created },
time: { created },
}),
)
})
@@ -85,7 +86,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
metadata: event.metadata,
model: event.data.model,
previous,
time: { created: event.created },
time: { created },
}),
)
})
@@ -101,7 +102,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
projectID: event.data.projectID,
subpath: event.data.subpath,
previous: yield* adapter.getLocation(),
time: { created: event.created },
time: { created },
}),
)
})
@@ -126,7 +127,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
text: event.data.text,
description: `Instructions updated: ${Object.keys(event.data.delta).join(", ")}`,
metadata: event.metadata,
time: { created: event.created },
time: { created },
}),
)
},
@@ -138,7 +139,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
metadata: event.data.metadata,
id: SessionMessage.ID.fromEvent(event.id),
type: "synthetic",
time: { created: event.created },
time: { created },
}),
)
},
@@ -151,7 +152,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
name: event.data.name,
text: event.data.text,
metadata: event.metadata,
time: { created: event.created },
time: { created },
}),
)
},
@@ -164,7 +165,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
shellID: event.data.shell.id,
command: event.data.shell.command,
status: event.data.shell.status,
time: { created: event.created },
time: { created },
}),
)
},
@@ -177,7 +178,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
draft.status = event.data.shell.status
draft.exit = event.data.shell.exit
draft.output = event.data.output
draft.time.completed = event.created
draft.time.completed = created
}),
)
}
@@ -205,7 +206,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
yield* adapter.updateAssistant(
produce(currentAssistant, (draft) => {
draft.retry = undefined
draft.time.completed = event.created
draft.time.completed = created
}),
)
}
@@ -216,7 +217,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
agent: event.data.agent,
model: event.data.model,
metadata: event.metadata,
time: { created: event.created },
time: { created },
content: [],
snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined,
}),
@@ -225,7 +226,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
},
"session.step.ended": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
draft.time.completed = event.created
draft.time.completed = created
draft.finish = event.data.finish
draft.cost = event.data.cost
draft.tokens = event.data.tokens
@@ -239,7 +240,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
},
"session.step.failed": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
draft.time.completed = event.created
draft.time.completed = created
draft.finish = "error"
draft.error = castDraft(event.data.error)
draft.retry = undefined
@@ -277,7 +278,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
type: "tool",
id: event.data.id,
name: event.data.name,
time: { created: event.created },
time: { created },
state: SessionMessage.ToolStateStreaming.make({ status: "streaming", input: "" }),
}),
),
@@ -296,7 +297,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
if (match) {
match.executed = event.data.executed
match.providerState = event.data.state
match.time.ran = event.created
match.time.ran = created
match.state = castDraft(
SessionMessage.ToolStateRunning.make({
status: "running",
@@ -315,7 +316,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
if (match && match.state.status === "running") {
match.executed = event.data.executed || match.executed === true
match.providerResultState = event.data.resultState
match.time.completed = event.created
match.time.completed = created
match.state = castDraft(
SessionMessage.ToolStateCompleted.make({
status: "completed",
@@ -333,7 +334,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
if (match && (match.state.status === "streaming" || match.state.status === "running")) {
match.executed = event.data.executed || match.executed === true
match.providerResultState = event.data.resultState
match.time.completed = event.created
match.time.completed = created
match.state = castDraft(
SessionMessage.ToolStateError.make({
status: "error",
@@ -354,7 +355,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
type: "reasoning",
text: "",
state: event.data.state,
time: { created: event.created },
time: { created },
}),
),
)
@@ -365,7 +366,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
const match = latestReasoning(draft)
if (match) {
match.text = event.data.text
match.time = { created: match.time?.created ?? event.created, completed: event.created }
match.time = { created: match.time?.created ?? created, completed: created }
if (event.data.state !== undefined) match.state = event.data.state
}
})
@@ -389,7 +390,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
reason: event.data.reason,
summary: "",
recent: event.data.recent ?? "",
time: { created: event.created },
time: { created },
}),
),
"session.compaction.ended": (event) => {
@@ -414,7 +415,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
reason: event.data.reason,
summary: event.data.text,
recent: event.data.recent,
time: { created: event.created },
time: { created },
}),
)
})
@@ -429,7 +430,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
metadata: current?.metadata ?? event.metadata,
reason: event.data.reason,
error: event.data.error,
time: current?.time ?? { created: event.created },
time: current?.time ?? { created },
})
if (current?.status === "running") return yield* adapter.updateCompaction(failed)
yield* adapter.appendMessage(failed)
+14 -14
View File
@@ -162,8 +162,8 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
tokens_reasoning: 0,
tokens_cache_read: 0,
tokens_cache_write: 0,
time_created: DateTime.toEpochMillis(event.created),
time_updated: DateTime.toEpochMillis(event.created),
time_created: event.created,
time_updated: event.created,
})
.onConflictDoNothing()
.returning({ sessionID: SessionTable.id })
@@ -411,8 +411,8 @@ const layer = Layer.effectDiscard(
agent: event.data.agent,
model: event.data.model,
version: event.data.version,
time_created: DateTime.toEpochMillis(event.created),
time_updated: DateTime.toEpochMillis(event.created),
time_created: event.created,
time_updated: event.created,
})
.onConflictDoNothing()
.returning({ sessionID: SessionTable.id })
@@ -431,7 +431,7 @@ const layer = Layer.effectDiscard(
path: event.data.subpath,
...(event.data.projectID ? { project_id: event.data.projectID } : {}),
workspace_id: event.data.location.workspaceID ? Workspace.ID.make(event.data.location.workspaceID) : null,
time_updated: DateTime.toEpochMillis(event.created),
time_updated: event.created,
})
.where(eq(SessionTable.id, event.data.sessionID))
.run()
@@ -487,7 +487,7 @@ const layer = Layer.effectDiscard(
yield* run(db, event)
yield* db
.update(SessionTable)
.set({ agent: event.data.agent, time_updated: DateTime.toEpochMillis(event.created) })
.set({ agent: event.data.agent, time_updated: event.created })
.where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie)
@@ -498,7 +498,7 @@ const layer = Layer.effectDiscard(
yield* run(db, event)
yield* db
.update(SessionTable)
.set({ model: event.data.model, time_updated: DateTime.toEpochMillis(event.created) })
.set({ model: event.data.model, time_updated: event.created })
.where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie)
@@ -507,7 +507,7 @@ const layer = Layer.effectDiscard(
yield* bus.project(SessionEvent.Renamed, (event) =>
db
.update(SessionTable)
.set({ title: event.data.title, time_updated: DateTime.toEpochMillis(event.created) })
.set({ title: event.data.title, time_updated: event.created })
.where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie),
@@ -535,7 +535,7 @@ const layer = Layer.effectDiscard(
files: input.payload.files,
agents: input.payload.agents,
skills: input.payload.skills,
time: { created: event.created },
time: { created: DateTime.makeUnsafe(event.created) },
}
: {
id: input.id,
@@ -543,7 +543,7 @@ const layer = Layer.effectDiscard(
text: input.payload.text,
description: input.payload.description,
metadata: input.payload.metadata,
time: { created: event.created },
time: { created: DateTime.makeUnsafe(event.created) },
},
)
}),
@@ -561,7 +561,7 @@ const layer = Layer.effectDiscard(
})
yield* db
.update(SessionTable)
.set({ time_updated: DateTime.toEpochMillis(event.created) })
.set({ time_updated: event.created })
.where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie)
@@ -640,7 +640,7 @@ const layer = Layer.effectDiscard(
.update(SessionTable)
.set({
revert: { ...revert, files: revert.files ? [...revert.files] : undefined },
time_updated: DateTime.toEpochMillis(event.created),
time_updated: event.created,
})
.where(eq(SessionTable.id, event.data.sessionID))
.run()
@@ -650,7 +650,7 @@ const layer = Layer.effectDiscard(
yield* bus.project(SessionEvent.RevertEvent.Cleared, (event) =>
db
.update(SessionTable)
.set({ revert: null, time_updated: DateTime.toEpochMillis(event.created) })
.set({ revert: null, time_updated: event.created })
.where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie, Effect.asVoid),
@@ -685,7 +685,7 @@ const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
yield* db
.update(SessionTable)
.set({ revert: null, time_updated: DateTime.toEpochMillis(event.created) })
.set({ revert: null, time_updated: event.created })
.where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie)
+82 -23
View File
@@ -1,6 +1,7 @@
export * as SessionRunCoordinator from "./run-coordinator.js"
import { Deferred, Effect, Exit, Fiber, FiberSet, Scope } from "effect"
import type { Promotable } from "./inbox.js"
/** Serializes execution for each key while allowing different keys to run concurrently. */
export interface Coordinator<Key, E, Reason = never> {
@@ -9,26 +10,41 @@ export interface Coordinator<Key, E, Reason = never> {
/** Starts an execution while idle, or joins the active execution and returns its exit. */
readonly run: (key: Key) => Effect.Effect<void, E>
/** Rings the doorbell: an idle key starts an execution; an active one drains again before settling. */
readonly wake: (key: Key) => Effect.Effect<void>
readonly wake: (key: Key, request?: Request) => Effect.Effect<void>
/** Rings the current execution's doorbell with its existing request. Idle keys remain idle. */
readonly wakeActive: (key: Key) => Effect.Effect<void>
/** Stops the active execution, clears its doorbell, and waits for cleanup. No-op when idle. */
readonly interrupt: (key: Key, reason?: Reason) => Effect.Effect<void>
readonly interrupt: (
key: Key,
reason?: Reason,
options?: { readonly continue?: { readonly request: Request; readonly when: Effect.Effect<boolean> } },
) => Effect.Effect<void>
/** Resolves once no execution is active for the key. Returns immediately when already idle and never starts work. */
readonly awaitIdle: (key: Key) => Effect.Effect<void>
}
export type Request = Promotable
/**
* One execution is a busy period for one key: one fiber that drains from the first wake
* until the key would stay idle. `pendingWake` is the doorbell: work recorded during the
* execution rings it, and the execution loop drains again instead of ending. The doorbell
* closes the gap between a drain's last eligibility check and the idle transition, since
* those cannot be one atomic step. `done` resolves joiners with this execution's exit.
* execution rings it with its eligibility request, and the execution loop drains again
* instead of ending. The doorbell closes the gap between a drain's last eligibility check
* and the idle transition, since those cannot be one atomic step. `done` resolves joiners
* with this execution's exit.
*/
type Execution<E, Reason> = {
readonly done: Deferred.Deferred<void, E>
owner?: Fiber.Fiber<void>
pendingWake: boolean
request: Request
pendingWake?: Request
stopping: boolean
interruptionReason?: Reason
continuation?: {
readonly request: Request
readonly when: Effect.Effect<boolean>
signaled: boolean
}
}
/**
@@ -43,7 +59,7 @@ type Execution<E, Reason> = {
* ```
*/
export const make = <Key, E, Reason = never>(options: {
readonly drain: (key: Key, force: boolean) => Effect.Effect<void, E>
readonly drain: (key: Key, force: boolean, request: Request) => Effect.Effect<void, E>
/** Runs once when a process-local busy period begins, before its first drain. */
readonly started?: (key: Key) => Effect.Effect<void>
/**
@@ -57,21 +73,22 @@ export const make = <Key, E, Reason = never>(options: {
const fork = yield* FiberSet.makeRuntime<never, void, never>()
const loop = (key: Key, execution: Execution<E, Reason>, force: boolean): Effect.Effect<void, E> =>
Effect.suspend(() => options.drain(key, force)).pipe(
Effect.suspend(() => options.drain(key, force, execution.request)).pipe(
Effect.flatMap(() =>
Effect.suspend(() => {
if (execution.stopping || !execution.pendingWake) return Effect.void
execution.pendingWake = false
if (execution.stopping || execution.pendingWake === undefined) return Effect.void
execution.request = execution.pendingWake
execution.pendingWake = undefined
// Trampoline so drains that complete synchronously cannot grow the stack.
return Effect.yieldNow.pipe(Effect.andThen(loop(key, execution, false)))
}),
),
)
const start = (key: Key, force: boolean) => {
const start = (key: Key, force: boolean, request: Request) => {
const execution: Execution<E, Reason> = {
done: Deferred.makeUnsafe<void, E>(),
pendingWake: false,
request,
stopping: false,
}
executions.set(key, execution)
@@ -87,7 +104,7 @@ export const make = <Key, E, Reason = never>(options: {
execution.owner = undefined
}).pipe(Effect.andThen(options.settled?.(key, exit, execution.interruptionReason) ?? Effect.void)),
),
Effect.onExit((exit) => Effect.sync(() => settle(key, execution, exit))),
Effect.onExit((exit) => finish(key, execution, exit)),
Effect.exit,
Effect.asVoid,
),
@@ -97,12 +114,22 @@ export const make = <Key, E, Reason = never>(options: {
// A doorbell that survives the execution loop (rung after the loop decided to end, or
// during failure or interruption cleanup) starts a fresh execution for the remaining work.
const settle = (key: Key, execution: Execution<E, Reason>, exit: Exit.Exit<void, E>) => {
if (execution.pendingWake) start(key, false)
const settle = (key: Key, execution: Execution<E, Reason>, exit: Exit.Exit<void, E>, resume: boolean) => {
if (resume && execution.continuation) start(key, false, execution.continuation.request)
else if (execution.pendingWake) start(key, false, execution.pendingWake)
else executions.delete(key)
Deferred.doneUnsafe(execution.done, exit)
}
const finish = (key: Key, execution: Execution<E, Reason>, exit: Exit.Exit<void, E>) => {
if (!execution.continuation) return Effect.sync(() => settle(key, execution, exit, false))
return execution.continuation.when.pipe(
Effect.flatMap((ready) =>
Effect.sync(() => settle(key, execution, exit, ready || execution.continuation?.signaled === true)),
),
)
}
const run = (key: Key): Effect.Effect<void, E> =>
Effect.suspend(() => {
const execution = executions.get(key)
@@ -111,26 +138,58 @@ export const make = <Key, E, Reason = never>(options: {
if (execution.stopping) return Deferred.await(execution.done).pipe(Effect.andThen(run(key)))
return Deferred.await(execution.done)
}
return Deferred.await(start(key, true).done)
return Deferred.await(start(key, true, "input").done)
})
const wake = (key: Key) =>
const wake = (key: Key, request: Request = "input") =>
Effect.sync(() => {
const execution = executions.get(key)
if (execution !== undefined) {
execution.pendingWake = true
if (execution.stopping) {
if (execution.continuation) execution.continuation.signaled = true
else execution.continuation = { request, when: Effect.succeed(true), signaled: true }
return
}
// Coalesced wakes keep the widest request: "input" subsumes "steer".
execution.pendingWake = execution.pendingWake === "input" ? "input" : request
return
}
start(key, false)
start(key, false, request)
})
const interrupt = (key: Key, reason?: Reason): Effect.Effect<void> =>
const wakeActive = (key: Key) =>
Effect.suspend(() => {
const execution = executions.get(key)
if (execution?.owner === undefined || execution.stopping) return Effect.void
return execution ? wake(key, execution.request) : Effect.void
})
const interrupt = (
key: Key,
reason?: Reason,
options?: { readonly continue?: { readonly request: Request; readonly when: Effect.Effect<boolean> } },
): Effect.Effect<void> =>
Effect.suspend(() => {
const execution = executions.get(key)
if (execution === undefined) return Effect.void
if (execution.stopping) {
if (options?.continue)
execution.continuation = {
...options.continue,
signaled: execution.continuation?.signaled ?? false,
}
return Deferred.await(execution.done).pipe(Effect.exit, Effect.asVoid)
}
if (execution.owner === undefined) {
if (!options?.continue) return Effect.void
execution.stopping = true
execution.pendingWake = undefined
execution.continuation = { ...options.continue, signaled: false }
return Deferred.await(execution.done).pipe(Effect.exit, Effect.asVoid)
}
execution.stopping = true
execution.pendingWake = false
execution.pendingWake = undefined
execution.interruptionReason = reason
if (options?.continue) execution.continuation = { ...options.continue, signaled: false }
return Fiber.interrupt(execution.owner)
})
@@ -143,5 +202,5 @@ export const make = <Key, E, Reason = never>(options: {
return Deferred.await(execution.done).pipe(Effect.exit, Effect.andThen(awaitIdle(key)))
})
return { active: Effect.sync(() => new Set(executions.keys())), run, wake, interrupt, awaitIdle }
return { active: Effect.sync(() => new Set(executions.keys())), run, wake, wakeActive, interrupt, awaitIdle }
})
@@ -3,6 +3,7 @@ export * as SessionRunner from "./index.js"
import type { AIError } from "@opencode-ai/ai"
import { Context, Effect } from "effect"
import { SessionSchema } from "../schema.js"
import type { Promotable } from "../inbox.js"
import type { AgentNotFoundError, MessageDecodeError, StepFailedError, UserInterruptedError } from "../error.js"
import { SessionRunnerModel } from "./model.js"
import type { Instructions } from "../../instructions/index.js"
@@ -29,6 +30,8 @@ export interface Interface {
readonly sessionID: SessionSchema.ID
readonly force: boolean
readonly continuation?: Continuation
/** "steer" settles the active intent without promoting queued next-turn work. */
readonly promotable?: Promotable
}) => Effect.Effect<DrainResult, RunError>
}
+16 -14
View File
@@ -128,22 +128,25 @@ const layer = Layer.effect(
readonly sessionID: SessionSchema.ID
readonly force: boolean
readonly continuation?: Continuation
readonly promotable?: SessionInbox.Promotable
}) {
let force = input.force
let continuation = input.continuation
if (!force && !continuation && !(yield* SessionInbox.has(db, input.sessionID, "any")))
const promotable = input.promotable ?? "input"
if (!force && !continuation && !(yield* SessionInbox.has(db, input.sessionID, promotable)))
return { type: "complete" as const }
yield* settleStaleToolCalls(input.sessionID)
while (true) {
if (yield* runPendingCompaction(input.sessionID)) {
if (yield* runPendingCompaction(input.sessionID, promotable)) {
force = false
continue
}
if (yield* runPendingMove(input.sessionID, "input")) return { type: "moved" as const }
if (!force && !continuation && !(yield* SessionInbox.has(db, input.sessionID, "input")))
if (yield* runPendingMove(input.sessionID, promotable)) return { type: "moved" as const }
if (!force && !continuation && !(yield* SessionInbox.has(db, input.sessionID, promotable)))
return { type: "complete" as const }
const result = yield* runSteps(input.sessionID, continuation)
const result = yield* runSteps(input.sessionID, continuation, promotable)
if (result.type === "moved") return result
if (promotable === "steer") return { type: "complete" as const }
force = false
continuation = undefined
}
@@ -155,14 +158,15 @@ const layer = Layer.effect(
*/
const runSteps = Effect.fn("SessionRunner.runSteps")(function* (
sessionID: SessionSchema.ID,
continuation?: Continuation,
continuation: Continuation | undefined,
drainPromotable: SessionInbox.Promotable,
) {
// Fresh work may promote queued input; later steps absorb steers only.
let promotable: SessionInbox.Promotable = continuation ? "steer" : "input"
// Fresh work may promote queued input; resumed turns and later steps absorb steers only.
let promotable: SessionInbox.Promotable = continuation ? "steer" : drainPromotable
let step = continuation?.step ?? 1
let next = continuation
while (true) {
if (yield* runPendingCompaction(sessionID)) continue
if (yield* runPendingCompaction(sessionID, "steer")) continue
if (yield* runPendingMove(sessionID, "steer")) return { type: "moved" as const, continuation: next }
const result = yield* runStep(sessionID, promotable, step)
next = result.needsContinuation ? { step: result.step + 1 } : undefined
@@ -515,14 +519,14 @@ const layer = Layer.effect(
/** Executes a previously admitted manual compaction request, if one is pending. */
const runPendingCompaction = Effect.fn("SessionRunner.runPendingCompaction")(function* (
sessionID: SessionSchema.ID,
promotable: SessionInbox.Promotable,
) {
return yield* Effect.uninterruptibleMask((restore) =>
Effect.gen(function* () {
const pending = yield* SessionInbox.serialized(
sessionID,
Effect.gen(function* () {
const selected =
(yield* SessionInbox.nextSteer(db, sessionID)) ?? (yield* SessionInbox.nextQueued(db, sessionID))
const selected = yield* SessionInbox.nextPromotable(db, sessionID, promotable)
if (selected?.type !== "compaction") return
yield* bus.publishAll([
[SessionEvent.InboxDelivered, { sessionID, inboxID: selected.id }],
@@ -564,9 +568,7 @@ const layer = Layer.effect(
return yield* SessionInbox.serialized(
sessionID,
Effect.gen(function* () {
const pending =
(yield* SessionInbox.nextSteer(db, sessionID)) ??
(promotable === "input" ? yield* SessionInbox.nextQueued(db, sessionID) : undefined)
const pending = yield* SessionInbox.nextPromotable(db, sessionID, promotable)
if (pending?.type !== "move") return false
yield* modelTransport.close(sessionID)
yield* bus.publishAll([
@@ -1,5 +1,5 @@
import { type LLMEvent, type ProviderMetadata, type ToolResultValue } from "@opencode-ai/ai"
import { Effect } from "effect"
import { Clock, Effect } from "effect"
import { Bus } from "../../bus.js"
import { Model } from "../../model.js"
import { SessionEvent } from "../event.js"
@@ -81,6 +81,7 @@ const hostedContent = (result: ToolResultValue): NonEmptyContent => {
* and consumers fold by id/ordinal rather than global position.
*/
export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, input: Input) => {
const deltaBatchInterval = 100
const tools = new Map<
string,
{
@@ -123,32 +124,56 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
const fragments = (
name: string,
ended: (id: string, value: string, ordinal: number, state?: Record<string, unknown>) => Effect.Effect<void>,
delta?: (id: string, value: string, ordinal: number) => Effect.Effect<void>,
single = false,
) => {
const chunks = new Map<
string,
{ readonly ordinal: number; readonly values: string[]; state?: Record<string, unknown> }
>()
type Fragment = {
readonly ordinal: number
readonly values: string[]
pending: string
publishedAt?: number
state?: Record<string, unknown>
}
const chunks = new Map<string, Fragment>()
let nextOrdinal = 0
const start = (id: string, state?: Record<string, unknown>) =>
Effect.suspend(() => {
if (chunks.has(id)) return Effect.die(new Error(`Duplicate ${name} start: ${id}`))
if (single && chunks.size > 0) return Effect.die(new Error(`${name} start before end: ${id}`))
const ordinal = nextOrdinal++
chunks.set(id, { ordinal, values: [], state })
chunks.set(id, { ordinal, values: [], pending: "", state })
return Effect.succeed(ordinal)
})
const append = (id: string, value: string, state?: Record<string, unknown>) =>
Effect.suspend(() => {
const current = chunks.get(id)
if (!current) return Effect.die(new Error(`${name} delta before start: ${id}`))
current.values.push(value)
if (state !== undefined) current.state = { ...current.state, ...state }
return Effect.succeed(current.ordinal)
})
const publishDelta = Effect.fnUntraced(function* (id: string, force = false) {
if (!delta) return undefined
const current = chunks.get(id)
if (!current) return yield* Effect.die(new Error(`${name} delta before start: ${id}`))
if (!current.pending) return undefined
const now = yield* Clock.currentTimeMillis
if (!force && current.publishedAt === undefined) {
current.publishedAt = now
return undefined
}
if (!force && current.publishedAt !== undefined && now - current.publishedAt < deltaBatchInterval)
return undefined
yield* delta(id, current.pending, current.ordinal)
current.pending = ""
current.publishedAt = now
return undefined
})
const append = Effect.fnUntraced(function* (id: string, value: string, state?: Record<string, unknown>) {
const current = chunks.get(id)
if (!current) return yield* Effect.die(new Error(`${name} delta before start: ${id}`))
current.values.push(value)
if (delta) current.pending += value
if (state !== undefined) current.state = { ...current.state, ...state }
yield* publishDelta(id)
return current.ordinal
})
const end = Effect.fnUntraced(function* (id: string, state?: Record<string, unknown>, value?: string) {
const current = chunks.get(id)
if (!current) return yield* Effect.die(new Error(`${name} end before start: ${id}`))
yield* publishDelta(id, true)
yield* ended(
id,
value ?? current.values.join(""),
@@ -156,9 +181,10 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
state === undefined ? current.state : { ...current.state, ...state },
)
chunks.delete(id)
return undefined
})
const flush = Effect.fnUntraced(function* () {
for (const id of chunks.keys()) yield* end(id)
for (const id of Array.from(chunks.keys())) yield* end(id)
})
return { start, append, end, flush, has: (id: string) => chunks.has(id) }
}
@@ -175,6 +201,15 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
state,
})
}),
(_textID, value, ordinal) =>
Effect.gen(function* () {
yield* bus.publish(SessionEvent.Text.Delta, {
sessionID: input.sessionID,
assistantMessageID: yield* currentAssistantMessageID(),
ordinal,
delta: value,
})
}),
true,
)
const reasoning = fragments(
@@ -189,6 +224,15 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
state,
})
}),
(_reasoningID, value, ordinal) =>
Effect.gen(function* () {
yield* bus.publish(SessionEvent.Reasoning.Delta, {
sessionID: input.sessionID,
assistantMessageID: yield* currentAssistantMessageID(),
ordinal,
delta: value,
})
}),
true,
)
const toolInput = fragments("tool input", (id, value) =>
@@ -351,13 +395,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
})
return
case "text-delta":
const deltaTextOrdinal = yield* text.append(event.id, event.text, providerState(event.providerMetadata))
yield* bus.publish(SessionEvent.Text.Delta, {
sessionID: input.sessionID,
assistantMessageID: yield* currentAssistantMessageID(),
ordinal: deltaTextOrdinal,
delta: event.text,
})
yield* text.append(event.id, event.text, providerState(event.providerMetadata))
return
case "text-end":
yield* text.end(event.id, providerState(event.providerMetadata))
@@ -373,17 +411,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
})
return
case "reasoning-delta":
const deltaReasoningOrdinal = yield* reasoning.append(
event.id,
event.text,
providerState(event.providerMetadata),
)
yield* bus.publish(SessionEvent.Reasoning.Delta, {
sessionID: input.sessionID,
assistantMessageID: yield* currentAssistantMessageID(),
ordinal: deltaReasoningOrdinal,
delta: event.text,
})
yield* reasoning.append(event.id, event.text, providerState(event.providerMetadata))
return
case "reasoning-end":
yield* reasoning.end(event.id, providerState(event.providerMetadata))
@@ -399,12 +427,6 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
return yield* Effect.die(new Error(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`))
if (!toolInput.has(event.id)) return yield* Effect.die(new Error(`Tool input delta after end: ${event.id}`))
yield* toolInput.append(event.id, event.text)
yield* bus.publish(SessionEvent.Tool.Input.Delta, {
sessionID: input.sessionID,
assistantMessageID: tool.assistantMessageID,
id: event.id,
delta: event.text,
})
return
}
case "tool-input-end":
+3 -3
View File
@@ -16,7 +16,7 @@ import { Environment } from "../../environment/index.js"
import { FileMutation } from "../../file-mutation.js"
import { Formatter } from "../../formatter.js"
import { Location } from "../../location.js"
import { LocationPath } from "../../location-path.js"
import { LocationMutation } from "../../location-mutation.js"
import { Permission } from "../../permission.js"
import { fileDiff } from "./file-diff.js"
@@ -110,7 +110,7 @@ const findLineOccurrences = (content: string, search: string) => {
export const Plugin = {
id: "opencode.tool.edit",
effect: Effect.fn("EditTool.Plugin")(function* (ctx: PluginContext) {
const mutation = yield* LocationPath.Service
const mutation = yield* LocationMutation.Service
const fileMutation = yield* FileMutation.Service
const environment = yield* Environment.Service
const formatter = yield* Formatter.Service
@@ -148,7 +148,7 @@ export const Plugin = {
const external = target.externalDirectory
if (external) {
yield* permission.assert({
...LocationPath.externalDirectoryPermission(external),
...LocationMutation.externalDirectoryPermission(external),
sessionID: context.sessionID,
agent: context.agent,
source: permissionSource,
+3 -3
View File
@@ -7,7 +7,7 @@ import path from "path"
import { Environment } from "../../environment/index.js"
import { FileSystem } from "../../filesystem.js"
import { Location } from "../../location.js"
import { LocationPath } from "../../location-path.js"
import { LocationMutation } from "../../location-mutation.js"
import { Ripgrep } from "../../ripgrep.js"
import { RelativePath } from "../../schema.js"
import { Permission } from "../../permission.js"
@@ -45,7 +45,7 @@ export const Plugin = {
const environment = yield* Environment.Service
const ripgrep = yield* Ripgrep.Service
const location = yield* Location.Service
const mutation = yield* LocationPath.Service
const mutation = yield* LocationMutation.Service
const permission = yield* Permission.Service
yield* ctx.tool
@@ -64,7 +64,7 @@ export const Plugin = {
const external = target.externalDirectory
if (external)
yield* permission.assert({
...LocationPath.externalDirectoryPermission(external),
...LocationMutation.externalDirectoryPermission(external),
sessionID: context.sessionID,
agent: context.agent,
source,
+3 -3
View File
@@ -7,7 +7,7 @@ import path from "path"
import { Environment } from "../../environment/index.js"
import { FileSystem } from "../../filesystem.js"
import { Location } from "../../location.js"
import { LocationPath } from "../../location-path.js"
import { LocationMutation } from "../../location-mutation.js"
import { Permission } from "../../permission.js"
import { Ripgrep } from "../../ripgrep.js"
import { RelativePath } from "../../schema.js"
@@ -61,7 +61,7 @@ export const Plugin = {
const environment = yield* Environment.Service
const ripgrep = yield* Ripgrep.Service
const location = yield* Location.Service
const mutation = yield* LocationPath.Service
const mutation = yield* LocationMutation.Service
const permission = yield* Permission.Service
yield* ctx.tool
@@ -79,7 +79,7 @@ export const Plugin = {
const target = yield* mutation.resolve({ path: input.path ?? "." })
if (target.externalDirectory)
yield* permission.assert({
...LocationPath.externalDirectoryPermission(target.externalDirectory),
...LocationMutation.externalDirectoryPermission(target.externalDirectory),
sessionID: context.sessionID,
agent: context.agent,
source,
+7 -7
View File
@@ -10,7 +10,7 @@ import { Environment } from "../../environment/index.js"
import { Formatter } from "../../formatter.js"
import { FileMutation } from "../../file-mutation.js"
import { Location } from "../../location.js"
import { LocationPath } from "../../location-path.js"
import { LocationMutation } from "../../location-mutation.js"
import { Patch } from "@opencode-ai/util/patch"
import { Permission } from "../../permission.js"
import DESCRIPTION from "../patch.txt"
@@ -46,29 +46,29 @@ export const toModelOutput = (output: Output) =>
type Prepared =
| (Extract<Patch.Hunk, { readonly type: "add" }> & {
readonly target: LocationPath.Target
readonly target: LocationMutation.Target
readonly content: string
readonly before: string
readonly after: string
})
| (Extract<Patch.Hunk, { readonly type: "delete" }> & {
readonly target: LocationPath.Target
readonly target: LocationMutation.Target
readonly before: string
readonly after: string
})
| (Extract<Patch.Hunk, { readonly type: "update" }> & {
readonly target: LocationPath.Target
readonly target: LocationMutation.Target
readonly content: string
readonly before: string
readonly after: string
readonly moveTarget?: LocationPath.Target
readonly moveTarget?: LocationMutation.Target
})
export const Plugin = {
id: "opencode.tool.patch",
effect: Effect.fn("PatchTool.Plugin")(function* (ctx: PluginContext) {
const environment = yield* Environment.Service
const mutation = yield* LocationPath.Service
const mutation = yield* LocationMutation.Service
const fileMutation = yield* FileMutation.Service
const formatter = yield* Formatter.Service
const location = yield* Location.Service
@@ -116,7 +116,7 @@ export const Plugin = {
const target = yield* mutation.resolve({ path: value, kind: "file" })
if (!target.externalDirectory) return target
yield* permission.assert({
...LocationPath.externalDirectoryPermission(target.externalDirectory),
...LocationMutation.externalDirectoryPermission(target.externalDirectory),
metadata: {
filepath: target.absolute,
parentDir: target.externalDirectory.directory,
+3 -3
View File
@@ -6,7 +6,7 @@ import { ToolFailure } from "@opencode-ai/ai"
import { Effect, Schema } from "effect"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Location } from "../../location.js"
import { LocationPath } from "../../location-path.js"
import { LocationMutation } from "../../location-mutation.js"
import { Permission } from "../../permission.js"
import { SessionInstructions } from "../../session/instructions.js"
import { AbsolutePath } from "../../schema.js"
@@ -32,7 +32,7 @@ export const Plugin = {
id: "opencode.tool.read",
effect: Effect.fn("ReadTool.Plugin")(function* (ctx: PluginContext) {
const reader = yield* ReadToolFileSystem.Service
const mutation = yield* LocationPath.Service
const mutation = yield* LocationMutation.Service
const permission = yield* Permission.Service
const sessionInstructions = yield* SessionInstructions.Service
const fs = yield* FSUtil.Service
@@ -58,7 +58,7 @@ export const Plugin = {
const external = target.externalDirectory
if (external)
yield* permission.assert({
...LocationPath.externalDirectoryPermission(external),
...LocationMutation.externalDirectoryPermission(external),
sessionID: context.sessionID,
agent: context.agent,
source,
+2 -2
View File
@@ -7,7 +7,7 @@ import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin
import { Deferred, Effect, Schema, Scope } from "effect"
import { Config } from "../../config.js"
import { Environment } from "../../environment/index.js"
import { LocationPath } from "../../location-path.js"
import { LocationMutation } from "../../location-mutation.js"
import { Permission } from "../../permission.js"
import { PluginRuntime } from "../../plugin/runtime.js"
import { NonNegativeInt } from "../../schema.js"
@@ -84,7 +84,7 @@ export const Plugin = {
const runtime = yield* PluginRuntime.Service
const scope = yield* Scope.Scope
const environment = yield* Environment.Service
const mutation = yield* LocationPath.Service
const mutation = yield* LocationMutation.Service
const shell = yield* Shell.Service
const permission = yield* Permission.Service
const config = yield* Config.Service
+1 -3
View File
@@ -181,9 +181,7 @@ export const Plugin = {
)
const background = input.background === true
yield* context.progress({
metadata: { sessionID: child.id, status: "running" },
})
yield* context.progress({ sessionID: child.id, status: "running" })
const run = Effect.gen(function* () {
// The child session owns its agent/model (set at create); prompt only admits input.
+3 -3
View File
@@ -13,7 +13,7 @@ import { Bom } from "@opencode-ai/util/bom"
import { Environment } from "../../environment/index.js"
import { FileMutation } from "../../file-mutation.js"
import { Formatter } from "../../formatter.js"
import { LocationPath } from "../../location-path.js"
import { LocationMutation } from "../../location-mutation.js"
import { Permission } from "../../permission.js"
import { fileDiff } from "./file-diff.js"
@@ -46,7 +46,7 @@ export const toModelOutput = (output: Output) =>
export const Plugin = {
id: "opencode.tool.write",
effect: Effect.fn("WriteTool.Plugin")(function* (ctx: PluginContext) {
const mutation = yield* LocationPath.Service
const mutation = yield* LocationMutation.Service
const fileMutation = yield* FileMutation.Service
const environment = yield* Environment.Service
const formatter = yield* Formatter.Service
@@ -72,7 +72,7 @@ export const Plugin = {
const external = target.externalDirectory
if (external)
yield* permission.assert({
...LocationPath.externalDirectoryPermission(external),
...LocationMutation.externalDirectoryPermission(external),
sessionID: context.sessionID,
agent: context.agent,
source,
+61 -39
View File
@@ -1,5 +1,6 @@
import { APICallError } from "@ai-sdk/provider"
import type { LanguageModelV3, LanguageModelV3StreamPart } from "@ai-sdk/provider"
import { createMistral } from "@ai-sdk/mistral"
import { AISDK } from "@opencode-ai/core/aisdk"
import { SessionRunnerRetry } from "@opencode-ai/core/session/runner/retry"
import { toSessionError } from "@opencode-ai/core/session/to-session-error"
@@ -277,67 +278,88 @@ it.effect("projects replay metadata onto AI SDK prompt parts", () =>
}),
)
it.effect("preserves tool result content in AI SDK prompts", () =>
it.effect("moves a tool image through the real Mistral provider as a user message", () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
let body: { messages?: unknown[] } | undefined
const mockFetch = Object.assign(
async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
body = JSON.parse(String(init?.body))
const chunks = [
{
id: "response-1",
created: 0,
model: "pixtral-large-latest",
choices: [{ index: 0, delta: { content: [{ type: "text", text: "I see it." }] } }],
},
{
id: "response-1",
created: 0,
model: "pixtral-large-latest",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
},
]
return new Response(chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join(""), {
headers: { "Content-Type": "text/event-stream" },
})
},
{ preconnect: fetch.preconnect },
)
yield* aisdk.hook.sdk((event) => {
event.sdk = { languageModel: () => ({ provider: event.model.providerID }) }
event.sdk = createMistral({ apiKey: "test", fetch: mockFetch })
})
const resolved = yield* aisdk.model(model("test-ai-sdk"))
const prepared = yield* compileRequest(
const resolved = yield* aisdk.model({
...model("@ai-sdk/mistral"),
modelID: Model.ID.make("pixtral-large-latest"),
})
yield* LLMClient.generate(
LLM.request({
model: resolved,
messages: [
Message.user("Inspect the screenshot."),
Message.assistant({ type: "tool-call", id: "call_1", name: "screenshot", input: {} }),
Message.tool({
type: "tool-result",
id: "call_1",
name: "read",
name: "screenshot",
result: {
type: "content",
value: [
{ type: "text", text: "attachments" },
{ type: "file", uri: "data:image/png;base64,AAAA", mime: "image/png", name: "pixel.png" },
{
type: "file",
uri: "data:application/pdf;charset=utf-8;base64,JVBERg==",
mime: "application/pdf",
name: "document.pdf",
},
{ type: "file", uri: "data:audio/mpeg;base64,SUQz", mime: "audio/mpeg", name: "clip.mp3" },
{ type: "file", uri: "https://example.com/pixel.png", mime: "image/png" },
{ type: "file", uri: "https://example.com/document.pdf", mime: "application/pdf" },
{ type: "text", text: "Screenshot captured" },
{ type: "file", uri: "data:image/png;base64,AAAA", mime: "image/png", name: "screen.png" },
],
},
}),
],
}),
)
).pipe(Effect.provide(client))
expect(prepared.body.prompt).toEqual([
expect(body?.messages).toEqual([
{ role: "user", content: [{ type: "text", text: "Inspect the screenshot." }] },
{
role: "assistant",
content: "",
tool_calls: [
{
id: "call_1",
type: "function",
function: { name: "screenshot", arguments: "{}" },
},
],
},
{
role: "tool",
name: "screenshot",
tool_call_id: "call_1",
content: '[{"type":"text","text":"Screenshot captured"}]',
},
{
role: "user",
content: [
{
type: "tool-result",
toolCallId: "call_1",
toolName: "read",
output: {
type: "content",
value: [
{ type: "text", text: "attachments" },
{ type: "image-data", data: "AAAA", mediaType: "image/png" },
{
type: "file-data",
data: "JVBERg==",
mediaType: "application/pdf",
filename: "document.pdf",
},
{ type: "file-data", data: "SUQz", mediaType: "audio/mpeg", filename: "clip.mp3" },
{ type: "image-url", url: "https://example.com/pixel.png" },
{ type: "file-url", url: "https://example.com/document.pdf" },
],
},
},
{ type: "text", text: "Attached media from tool result:" },
{ type: "image_url", image_url: "data:image/png;base64,AAAA" },
],
},
])
+25 -24
View File
@@ -1,5 +1,5 @@
import { describe, expect } from "bun:test"
import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Option, Ref, Schema, Stream } from "effect"
import { Cause, Deferred, Effect, Exit, Fiber, Layer, Option, Ref, Schema, Stream } from "effect"
import { Bus } from "@opencode-ai/core/bus"
import { Event } from "@opencode-ai/schema/event"
import { Session } from "@opencode-ai/schema/session"
@@ -422,7 +422,7 @@ describe("Bus", () => {
const { db } = yield* Database.Service
const aggregateID = Event.ID.create()
yield* bus.publish(SyncMessage, { id: aggregateID, text: "first" })
const event = yield* bus.publish(SyncMessage, { id: aggregateID, text: "first" })
const rows = yield* db
.select()
.from(EventTable)
@@ -433,6 +433,7 @@ describe("Bus", () => {
expect(rows).toHaveLength(1)
expect(rows[0]?.type).toBe(Bus.versionedType(SyncMessage.type, 1))
expect(rows[0]?.aggregate_id).toBe(aggregateID)
expect(rows[0]?.created).toBe(event.created)
}),
)
@@ -706,7 +707,7 @@ describe("Bus", () => {
yield* bus.replay({
id: Event.ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@@ -726,7 +727,7 @@ describe("Bus", () => {
yield* bus.replay({
id: Event.ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@@ -763,7 +764,7 @@ describe("Bus", () => {
const exit = yield* bus
.replay({
id: Event.ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 1,
aggregateID: envelopeAggregateID,
@@ -797,7 +798,7 @@ describe("Bus", () => {
yield* bus.replay({
id: Event.ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@@ -806,7 +807,7 @@ describe("Bus", () => {
const exit = yield* bus
.replay({
id: Event.ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 5,
aggregateID,
@@ -831,14 +832,14 @@ describe("Bus", () => {
yield* bus.replay({
id: Event.ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(SessionEvent.InstructionsUpdated.type, 2),
seq: 0,
aggregateID,
data: { sessionID: aggregateID, delta: { "core/context": "0".repeat(64) } },
})
expect(received[0]?.created).toEqual(DateTime.makeUnsafe(0))
expect(received[0]?.created).toBe(0)
}),
)
@@ -867,7 +868,7 @@ describe("Bus", () => {
const exit = yield* bus
.replay({
id: Event.ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: "unknown.event.1",
seq: 0,
aggregateID: Event.ID.create(),
@@ -895,7 +896,7 @@ describe("Bus", () => {
yield* bus.replay(
{
id: Event.ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 1,
aggregateID,
@@ -915,7 +916,7 @@ describe("Bus", () => {
const id = Event.ID.create()
const replayed = {
id,
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@@ -972,7 +973,7 @@ describe("Bus", () => {
yield* bus.replay(
{
id: Event.ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@@ -1001,7 +1002,7 @@ describe("Bus", () => {
yield* bus.replay(
{
id: Event.ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 1,
aggregateID,
@@ -1012,7 +1013,7 @@ describe("Bus", () => {
yield* bus.replay(
{
id: Event.ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 2,
aggregateID,
@@ -1045,7 +1046,7 @@ describe("Bus", () => {
yield* bus.replay(
{
id: Event.ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@@ -1058,7 +1059,7 @@ describe("Bus", () => {
.replay(
{
id: Event.ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 1,
aggregateID,
@@ -1080,7 +1081,7 @@ describe("Bus", () => {
yield* bus.listen((event) => Effect.sync(() => received.push(event)))
const replayed = {
id: Event.ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@@ -1101,7 +1102,7 @@ describe("Bus", () => {
const aggregateID = Session.ID.create()
const replayed = {
id: Event.ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@@ -1126,7 +1127,7 @@ describe("Bus", () => {
const id = Event.ID.create()
yield* bus.replay({
id,
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@@ -1136,7 +1137,7 @@ describe("Bus", () => {
const exit = yield* bus
.replay({
id,
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 1,
aggregateID,
@@ -1159,7 +1160,7 @@ describe("Bus", () => {
yield* bus.replay(
{
id: Event.ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
@@ -1170,7 +1171,7 @@ describe("Bus", () => {
yield* bus.replay(
{
id: Event.ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 1,
aggregateID,
@@ -1232,7 +1233,7 @@ describe("Bus", () => {
yield* bus.replay({
id: Event.ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: Bus.versionedType(DurableMessage.type, 1),
seq: 0,
aggregateID,
+30 -1
View File
@@ -102,6 +102,35 @@ describe("Catalog", () => {
}).pipe(Effect.provide(localCatalogLayer))
})
it.effect("makes an explicitly enabled provider available without a connection", () => {
const integrationID = Integration.ID.make("gateway")
const providerID = Provider.ID.make("remote")
const localCatalogLayer = Layer.fresh(
AppNodeBuilder.build(LayerNode.group([Catalog.node, Credential.node, Integration.node]), [
[Location.node, locationLayer],
]),
)
return Effect.gen(function* () {
const catalog = yield* Catalog.Service
yield* (yield* Integration.Service).transform((editor) => editor.update(integrationID, () => {}))
yield* catalog.transform((editor) =>
editor.provider.update(providerID, (provider) => {
provider.integrationID = integrationID
provider.settings = { baseURL: "https://gateway.example.com/v1" }
}),
)
expect(yield* catalog.provider.available()).toEqual([])
yield* catalog.transform((editor) =>
editor.provider.update(providerID, (provider) => {
provider.activation = "enabled"
}),
)
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([providerID])
}).pipe(Effect.provide(localCatalogLayer))
})
it.effect("projects environment connections without a catalog plugin", () =>
Effect.acquireUseRelease(
Effect.sync(() => {
@@ -278,7 +307,7 @@ describe("Catalog", () => {
const fallbackModel = Model.ID.make("fallback")
yield* catalog.transform((catalog) => {
catalog.provider.update(disabledProvider, (provider) => {
provider.disabled = true
provider.activation = "disabled"
})
catalog.model.update(disabledProvider, disabledModel, () => {})
catalog.provider.update(enabledProvider, () => {})
+1 -1
View File
@@ -342,7 +342,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
names: ["CUSTOM_API_KEY"],
})
expect((yield* integrations.get(Integration.ID.make("custom")))?.name).toBe("Renamed")
expect(provider.disabled).toBeUndefined()
expect(provider.activation).toBe("enabled")
expect(provider.package).toBe("aisdk:custom-sdk")
expect(provider.settings).toEqual({ baseURL: "https://example.test" })
expect(provider.headers).toEqual({ first: "first", shared: "last", last: "last" })
+9 -9
View File
@@ -7,7 +7,7 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { FileMutation } from "@opencode-ai/core/file-mutation"
import { Environment } from "@opencode-ai/core/environment/index"
import { Location } from "@opencode-ai/core/location"
import { LocationPath } from "@opencode-ai/core/location-path"
import { LocationMutation } from "@opencode-ai/core/location-mutation"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { type EnvironmentFilesTransform, transformEnvironmentFiles } from "./fixture/environment"
import { location } from "./fixture/location"
@@ -20,7 +20,7 @@ function provide(directory: string, transformFiles: EnvironmentFilesTransform =
Location.Service.of(location({ directory: AbsolutePath.make(directory) })),
)
return Effect.provide(
AppNodeBuilder.build(LayerNode.group([LocationPath.node, FileMutation.node]), [
AppNodeBuilder.build(LayerNode.group([LocationMutation.node, FileMutation.node]), [
[Location.node, activeLocation],
[Environment.node, transformEnvironmentFiles(activeLocation, transformFiles)],
]),
@@ -40,7 +40,7 @@ describe("FileMutation", () => {
Effect.gen(function* () {
const targetPath = path.join(directory, "hello.txt")
yield* Effect.promise(() => fs.writeFile(targetPath, "before"))
const target = yield* (yield* LocationPath.Service).resolve({ path: "hello.txt" })
const target = yield* (yield* LocationMutation.Service).resolve({ path: "hello.txt" })
expect(yield* (yield* FileMutation.Service).write({ target, content: "after" })).toEqual({
operation: "write",
@@ -56,7 +56,7 @@ describe("FileMutation", () => {
it.live("writes a prospective internal file and creates parent directories", () =>
withTmp((directory) =>
Effect.gen(function* () {
const target = yield* (yield* LocationPath.Service).resolve({
const target = yield* (yield* LocationMutation.Service).resolve({
path: path.join("src", "nested", "hello.txt"),
})
const result = yield* (yield* FileMutation.Service).write({ target, content: "hello" })
@@ -77,8 +77,8 @@ describe("FileMutation", () => {
Effect.gen(function* () {
const preservedPath = path.join(directory, "preserved.txt")
yield* Effect.promise(() => fs.writeFile(preservedPath, "\uFEFFbefore"))
const preserved = yield* (yield* LocationPath.Service).resolve({ path: "preserved.txt" })
const created = yield* (yield* LocationPath.Service).resolve({ path: "created.txt" })
const preserved = yield* (yield* LocationMutation.Service).resolve({ path: "preserved.txt" })
const created = yield* (yield* LocationMutation.Service).resolve({ path: "created.txt" })
const files = yield* FileMutation.Service
yield* files.writeTextPreservingBom({ target: preserved, content: "\uFEFFafter" })
@@ -95,7 +95,7 @@ describe("FileMutation", () => {
withTmp((outside) =>
Effect.gen(function* () {
const targetPath = path.join(outside, "external.txt")
const target = yield* (yield* LocationPath.Service).resolve({ path: targetPath })
const target = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
const result = yield* (yield* FileMutation.Service).write({ target, content: "external" })
expect(result).toEqual({
@@ -133,7 +133,7 @@ describe("FileMutation", () => {
)
yield* Effect.gen(function* () {
const mutation = yield* LocationPath.Service
const mutation = yield* LocationMutation.Service
const files = yield* FileMutation.Service
const firstPlan = yield* mutation.resolve({ path: "shared.txt" })
const secondPlan = yield* mutation.resolve({ path: "shared.txt" })
@@ -222,7 +222,7 @@ describe("FileMutation", () => {
)
yield* Effect.gen(function* () {
const mutation = yield* LocationPath.Service
const mutation = yield* LocationMutation.Service
const files = yield* FileMutation.Service
const firstPlan = yield* mutation.resolve({ path: "first.txt" })
const secondPlan = yield* mutation.resolve({ path: "second.txt" })
@@ -4,26 +4,18 @@ import { describe, expect, test } from "bun:test"
import { Effect, Layer, Schema } from "effect"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { Location } from "@opencode-ai/core/location"
import { LocationPath } from "@opencode-ai/core/location-path"
import { LocationMutation } from "@opencode-ai/core/location-mutation"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { tmpdir } from "./fixture/tmpdir"
import { location } from "./fixture/location"
import { it } from "./lib/effect"
function provide(directory: string, projectDirectory = directory) {
function provide(directory: string) {
return Effect.provide(
LayerNode.compile(LocationPath.node, [
LayerNode.compile(LocationMutation.node, [
[
Location.node,
Layer.succeed(
Location.Service,
Location.Service.of(
location(
{ directory: AbsolutePath.make(directory) },
{ projectDirectory: AbsolutePath.make(projectDirectory) },
),
),
),
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))),
],
]),
)
@@ -36,13 +28,13 @@ function withTmp<A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) {
).pipe(Effect.flatMap((tmp) => f(tmp.path)))
}
describe("LocationPath", () => {
describe("LocationMutation", () => {
it.live("resolves an active relative existing file target", () =>
withTmp((directory) =>
Effect.gen(function* () {
const targetPath = path.join(directory, "hello.txt")
yield* Effect.promise(() => fs.writeFile(targetPath, "hello"))
const target = yield* (yield* LocationPath.Service).resolve({ path: "hello.txt" })
const target = yield* (yield* LocationMutation.Service).resolve({ path: "hello.txt" })
expect(target).toMatchObject({
absolute: targetPath,
@@ -57,7 +49,7 @@ describe("LocationPath", () => {
withTmp((directory) =>
Effect.gen(function* () {
yield* Effect.promise(() => fs.mkdir(path.join(directory, "src")))
const target = yield* (yield* LocationPath.Service).resolve({ path: path.join("src", "new.txt") })
const target = yield* (yield* LocationMutation.Service).resolve({ path: path.join("src", "new.txt") })
expect(target).toMatchObject({
absolute: path.join(directory, "src", "new.txt"),
resource: "src/new.txt",
@@ -66,43 +58,10 @@ describe("LocationPath", () => {
),
)
it.live("does not require external authorization inside the project but outside the active directory", () =>
withTmp((project) =>
Effect.gen(function* () {
const directory = path.join(project, "packages", "app")
const targetPath = path.join(project, "README.md")
yield* Effect.promise(() => fs.mkdir(directory, { recursive: true }))
yield* Effect.promise(() => fs.writeFile(targetPath, "hello"))
const locationPath = yield* LocationPath.Service
const target = yield* locationPath.resolve({ path: targetPath })
expect(target).toMatchObject({
absolute: targetPath,
resource: "../../README.md",
})
expect(target.externalDirectory).toBeUndefined()
}).pipe(provide(path.join(project, "packages", "app"), project)),
),
)
it.live("does not treat a filesystem-root project fallback as internal", () =>
withTmp((directory) =>
withTmp((outside) =>
Effect.gen(function* () {
const locationPath = yield* LocationPath.Service
const target = yield* locationPath.resolve({ path: path.join(outside, "target.txt") })
expect(target.externalDirectory).toBeDefined()
}).pipe(provide(directory, path.parse(directory).root)),
),
),
)
it.live("requires external-directory authorization for a relative lexical escape", () =>
withTmp((directory) =>
Effect.gen(function* () {
const target = yield* (yield* LocationPath.Service).resolve({ path: "../outside.txt" })
const target = yield* (yield* LocationMutation.Service).resolve({ path: "../outside.txt" })
const root = path.dirname(directory)
expect(target).toMatchObject({
absolute: path.join(root, "outside.txt"),
@@ -125,7 +84,7 @@ describe("LocationPath", () => {
await fs.mkdir(outside)
await fs.symlink(outside, path.join(directory, "escape"))
})
const target = yield* (yield* LocationPath.Service).resolve({ path: path.join("escape", "new.txt") })
const target = yield* (yield* LocationMutation.Service).resolve({ path: path.join("escape", "new.txt") })
expect(target).toMatchObject({
absolute: path.join(directory, "escape", "new.txt"),
resource: "escape/new.txt",
@@ -145,7 +104,7 @@ describe("LocationPath", () => {
await fs.symlink(path.join(directory, "actual"), path.join(directory, "linked"))
})
expect(yield* (yield* LocationPath.Service).resolve({ path: "linked/new.txt" })).toMatchObject({
expect(yield* (yield* LocationMutation.Service).resolve({ path: "linked/new.txt" })).toMatchObject({
absolute: path.join(directory, "linked", "new.txt"),
resource: "linked/new.txt",
})
@@ -157,7 +116,7 @@ describe("LocationPath", () => {
withTmp((directory) =>
Effect.gen(function* () {
const targetPath = path.join(directory, "new.txt")
const target = yield* (yield* LocationPath.Service).resolve({ path: targetPath })
const target = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
expect(target).toMatchObject({
absolute: targetPath,
resource: "new.txt",
@@ -172,7 +131,7 @@ describe("LocationPath", () => {
withTmp((outside) =>
Effect.gen(function* () {
const targetPath = path.join(outside, "new.txt")
const target = yield* (yield* LocationPath.Service).resolve({ path: targetPath })
const target = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
const root = outside
expect(target).toMatchObject({
absolute: path.join(root, "new.txt"),
@@ -193,7 +152,7 @@ describe("LocationPath", () => {
Effect.gen(function* () {
const targetPath = path.join(outside, "existing.txt")
yield* Effect.promise(() => fs.writeFile(targetPath, "existing"))
const target = yield* (yield* LocationPath.Service).resolve({ path: targetPath })
const target = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
expect(target).toMatchObject({ absolute: targetPath })
expect(target.externalDirectory?.directory).toBe(outside)
}).pipe(provide(directory)),
@@ -205,7 +164,7 @@ describe("LocationPath", () => {
withTmp((directory) =>
withTmp((outside) =>
Effect.gen(function* () {
const target = yield* (yield* LocationPath.Service).resolve({ path: outside, kind: "file" })
const target = yield* (yield* LocationMutation.Service).resolve({ path: outside, kind: "file" })
expect(target.externalDirectory).toMatchObject({
directory: path.dirname(outside),
resource: path.join(path.dirname(outside), "*").replaceAll("\\", "/"),
@@ -220,7 +179,7 @@ describe("LocationPath", () => {
withTmp((outside) =>
Effect.gen(function* () {
const targetPath = path.join(outside, "new", "nested", "file.txt")
const target = yield* (yield* LocationPath.Service).resolve({ path: targetPath })
const target = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
const parent = path.dirname(targetPath)
expect(target.externalDirectory).toMatchObject({
directory: parent,
@@ -231,9 +190,9 @@ describe("LocationPath", () => {
),
)
test("ignores unknown path input fields", () => {
expect(Object.keys(LocationPath.ResolveInput.fields)).toEqual(["path", "kind"])
expect(Schema.decodeUnknownSync(LocationPath.ResolveInput)({ path: "README.md", reference: "docs" })).toEqual({
test("ignores unknown mutation input fields", () => {
expect(Object.keys(LocationMutation.ResolveInput.fields)).toEqual(["path", "kind"])
expect(Schema.decodeUnknownSync(LocationMutation.ResolveInput)({ path: "README.md", reference: "docs" })).toEqual({
path: "README.md",
})
})
+1 -1
View File
@@ -1002,7 +1002,7 @@ test("reconciles only changed MCP server config", async () => {
const publishUpdate = () =>
PubSub.publish(updates, {
id: ID.create(),
created: DateTime.makeUnsafe(0),
created: 0,
type: Event.Updated.type,
data: {},
} satisfies Payload<typeof Event.Updated>)
+107 -1
View File
@@ -2,13 +2,16 @@ import { describe, expect } from "bun:test"
import { LLM, LanguageModel } from "@opencode-ai/ai"
import { OpenAIChat } from "@opencode-ai/ai/protocols"
import { compileRequest } from "@opencode-ai/ai/route/client"
import { Effect } from "effect"
import { Effect, Layer } from "effect"
import { Headers } from "effect/unstable/http"
import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration"
import { Compatibility, ID, Info, VariantID } from "@opencode-ai/core/model"
import { Provider } from "@opencode-ai/core/provider"
import { ModelResolver } from "@opencode-ai/core/model-resolver"
import { Catalog } from "@opencode-ai/core/catalog"
import { AISDK } from "@opencode-ai/core/aisdk"
import { Npm } from "@opencode-ai/util/npm"
import { it } from "./lib/effect"
interface ModelOptions {
@@ -269,6 +272,109 @@ describe("ModelResolver", () => {
}),
)
it.effect("uses no native API-key auth for an explicitly enabled provider without credentials", () => {
const selected = model(Provider.aisdk("@ai-sdk/google"), {
providerID: Provider.ID.make("gateway"),
settings: { baseURL: "https://gateway.example.com/v1" },
headers: { "cf-access-token": "access-token" },
})
const provider = Provider.Info.make({
...Provider.Info.empty(selected.providerID),
activation: "enabled",
package: selected.package ?? "",
settings: selected.settings,
headers: selected.headers,
})
const catalog = Layer.mock(Catalog.Service, {
provider: {
get: () => Effect.succeed(provider),
all: () => Effect.die("unused"),
available: () => Effect.die("unused"),
},
model: {
get: () => Effect.succeed(selected),
all: () => Effect.die("unused"),
available: () => Effect.die("unused"),
default: () => Effect.die("unused"),
small: () => Effect.die("unused"),
},
})
const integrations = Layer.mock(Integration.Service, {
connection: {
active: () => Effect.succeed(undefined),
resolve: () => Effect.die("unused"),
key: () => Effect.die("unused"),
update: () => Effect.die("unused"),
remove: () => Effect.die("unused"),
},
oauth: {
connect: () => Effect.die("unused"),
status: () => Effect.die("unused"),
complete: () => Effect.die("unused"),
cancel: () => Effect.die("unused"),
},
command: {
connect: () => Effect.die("unused"),
status: () => Effect.die("unused"),
cancel: () => Effect.die("unused"),
},
})
const npm = Layer.mock(Npm.Service, {
add: () => Effect.die("unused"),
which: () => Effect.die("unused"),
})
const aisdk = Layer.mock(AISDK.Service, {
hook: {
sdk: () => Effect.die("unused"),
language: () => Effect.die("unused"),
},
model: () => Effect.die("unused"),
})
const layer = ModelResolver.layer.pipe(Layer.provide(Layer.mergeAll(catalog, integrations, npm, aisdk)))
return withEnv({ GOOGLE_GENERATIVE_AI_API_KEY: undefined }, () =>
Effect.gen(function* () {
const resolver = yield* ModelResolver.Service
const resolved = yield* resolver.resolveModel(selected)
const headers = yield* resolved.model.route.auth.apply({
request: LLM.request({ model: resolved.model, prompt: "Hello" }),
method: "POST",
url: "https://gateway.example.com/v1",
body: "{}",
headers: Headers.fromInput(resolved.model.route.defaults.headers),
})
expect(headers["cf-access-token"]).toBe("access-token")
expect(headers.authorization).toBeUndefined()
expect(headers["x-goog-api-key"]).toBeUndefined()
}).pipe(Effect.provide(layer)),
)
})
it.effect("keeps native provider environment auth strict when no API key is configured", () =>
withEnv({ GOOGLE_GENERATIVE_AI_API_KEY: undefined }, () =>
Effect.gen(function* () {
const resolved = yield* ModelResolver.fromCatalogModel(
model(Provider.aisdk("@ai-sdk/google"), {
settings: { baseURL: "https://google.example.com/v1" },
}),
)
const exit = yield* Effect.exit(
resolved.route.auth.apply({
request: LLM.request({ model: resolved, prompt: "Hello" }),
method: "POST",
url: "https://google.example.com/v1",
body: "{}",
headers: Headers.empty,
}),
)
expect(exit._tag).toBe("Failure")
}),
),
)
it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () =>
Effect.gen(function* () {
const resolved = yield* ModelResolver.fromCatalogModel(
+2
View File
@@ -47,6 +47,7 @@ const fixtureSnapshot = [
info: {
id: Provider.ID.make("acme"),
name: "Acme",
activation: "auto",
package: Provider.aisdk("@ai-sdk/openai-compatible"),
},
models: [
@@ -109,6 +110,7 @@ const fixture2Snapshot = [
info: {
id: Provider.ID.make("beta"),
name: "Beta",
activation: "auto",
package: Provider.aisdk("@ai-sdk/openai-compatible"),
},
models: [
@@ -64,6 +64,7 @@ describe("ModelsDevPlugin", () => {
info: {
id: providerID,
name: "Acme",
activation: "auto",
package: Provider.aisdk("@ai-sdk/openai-compatible"),
settings: { baseURL: "https://api.acme.test/v1" },
},
@@ -239,6 +240,7 @@ describe("ModelsDevPlugin", () => {
info: {
id: providerID,
name: "Acme",
activation: "auto",
package: Provider.aisdk("@ai-sdk/openai-compatible"),
},
environment: [],
@@ -330,6 +332,7 @@ describe("ModelsDevPlugin", () => {
info: {
id: providerID,
name: "Acme",
activation: "auto",
package: Provider.aisdk("@ai-sdk/openai-compatible"),
settings: { baseURL: "https://${ACME_HOST}/${UNDECLARED_HOST}/v1" },
},
@@ -385,6 +388,7 @@ describe("ModelsDevPlugin", () => {
info: {
id: Provider.ID.make(id),
name,
activation: "auto",
package: Provider.aisdk(packageName),
},
environment: id === "azure" ? ["AZURE_RESOURCE_NAME", environment] : [environment],
+180
View File
@@ -0,0 +1,180 @@
import { describe, expect } from "bun:test"
import { Message } from "@opencode-ai/ai"
import { DateTime, Effect, Stream } from "effect"
import type { SessionContext } from "@opencode-ai/plugin/effect/session"
import { Agent } from "@opencode-ai/core/agent"
import { Event } from "@opencode-ai/schema/event"
import { Model } from "@opencode-ai/core/model"
import { PlanPlugin } from "@opencode-ai/core/plugin/plan"
import { Provider } from "@opencode-ai/core/provider"
import { Session } from "@opencode-ai/core/session"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionInbox } from "@opencode-ai/core/session/inbox"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { it } from "../lib/effect"
import { host } from "./host"
const sessionID = Session.ID.make("ses_plan_test")
const plan = Agent.ID.make("plan")
const build = Agent.ID.make("build")
const agentSelected = (agent: Agent.ID, previous: Agent.ID): SessionEvent.AgentSelected => ({
id: Event.ID.create(),
created: 0,
durable: { aggregateID: sessionID, seq: Event.Seq.make(0), version: Event.Version.make(1) },
type: "session.agent.selected",
data: { sessionID, agent, previous },
})
/** Runs the plan plugin against stubbed domains, capturing persisted reminders and the context hook. */
const run = Effect.fnUntraced(function* (events: ReadonlyArray<SessionEvent.AgentSelected> = []) {
const persisted = new Array<string>()
let contextHook: ((input: SessionContext) => Effect.Effect<void>) | undefined
yield* PlanPlugin.Plugin.effect(
host({
agent: {
get: () => Effect.die("unused agent.get"),
list: () => Effect.die("unused agent.list"),
reload: () => Effect.die("unused agent.reload"),
transform: () => Effect.succeed({ dispose: Effect.void }),
},
tool: {
transform: () => Effect.die("unused tool.transform"),
hook: () => Effect.succeed({ dispose: Effect.void }),
},
event: {
subscribe: () => Stream.fromIterable(events),
},
session: {
hook: (name, callback) => {
if (name === "context") contextHook = callback as (input: SessionContext) => Effect.Effect<void>
return Effect.succeed({ dispose: Effect.void })
},
synthetic: (input) => {
persisted.push(input.text)
return Effect.succeed(
SessionInbox.Synthetic.make({
id: SessionMessage.ID.make("msg_plan_test"),
sessionID,
timeCreated: DateTime.makeUnsafe(0),
type: "synthetic",
payload: { text: input.text },
delivery: "steer",
}),
)
},
},
}),
)
if (!contextHook) return yield* Effect.die("plan plugin did not register a context hook")
return { persisted, contextHook }
})
const request = (agent: Agent.ID, messages: Array<Message>): SessionContext => ({
sessionID,
agent,
model: { id: Model.ID.make("test-model"), providerID: Provider.ID.make("test") },
system: [],
messages,
tools: {},
})
const settle = (persisted: ReadonlyArray<string>, expected: number, remaining = 1000): Effect.Effect<void, Error> =>
Effect.gen(function* () {
if (persisted.length >= expected) return
if (remaining === 0) {
return yield* Effect.fail(new Error(`Timed out waiting for ${expected} reminders, saw ${persisted.length}`))
}
yield* Effect.promise(() => Bun.sleep(1))
yield* settle(persisted, expected, remaining - 1)
})
/** The exact reminder texts, derived from plugin behavior rather than duplicated here. */
const reminders = Effect.gen(function* () {
const planRun = yield* run()
yield* planRun.contextHook(request(plan, []))
const buildRun = yield* run()
yield* buildRun.contextHook(request(build, [Message.user(planRun.persisted[0]!)]))
return { enter: planRun.persisted[0]!, leave: buildRun.persisted[0]! }
})
describe("plan plugin reminders", () => {
it.effect("injects enter and leave reminders on agent switches", () =>
Effect.gen(function* () {
const { persisted } = yield* run([agentSelected(plan, build), agentSelected(build, plan)])
yield* settle(persisted, 2)
expect(persisted[0]).toContain("You are in Plan mode")
expect(persisted[1]).toContain("NO LONGER in Plan mode")
}),
)
it.effect("reconciles a missing enter reminder into the request and persists it", () =>
Effect.gen(function* () {
const { persisted, contextHook } = yield* run()
const messages = [Message.user("what agent are you?")]
yield* contextHook(request(plan, messages))
expect(messages).toHaveLength(2)
// Inserted before the user's prompt, matching where agent-switch reminders land.
const first = messages[0]?.content[0]
expect(first?.type === "text" && first.text).toContain("You are in Plan mode")
expect(persisted).toHaveLength(1)
}),
)
it.effect("does nothing when the transcript already has a live enter reminder", () =>
Effect.gen(function* () {
const { enter } = yield* reminders
const { persisted, contextHook } = yield* run()
const messages = [Message.user(enter), Message.user("hello")]
yield* contextHook(request(plan, messages))
expect(messages).toHaveLength(2)
expect(persisted).toHaveLength(0)
}),
)
it.effect("reconciles a stale enter reminder with a leave reminder", () =>
Effect.gen(function* () {
const { enter } = yield* reminders
const { persisted, contextHook } = yield* run()
const messages = [Message.user(enter), Message.user("ok implement it")]
yield* contextHook(request(build, messages))
expect(messages).toHaveLength(3)
const middle = messages[1]?.content[0]
expect(middle?.type === "text" && middle.text).toContain("NO LONGER in Plan mode")
expect(persisted).toHaveLength(1)
}),
)
it.effect("does nothing for non-plan sessions without plan history", () =>
Effect.gen(function* () {
const { persisted, contextHook } = yield* run()
const messages = [Message.user("hello")]
yield* contextHook(request(build, messages))
expect(messages).toHaveLength(1)
expect(persisted).toHaveLength(0)
}),
)
it.effect("does nothing when a leave reminder already follows the enter reminder", () =>
Effect.gen(function* () {
const { enter, leave } = yield* reminders
const { persisted, contextHook } = yield* run()
const messages = [Message.user(enter), Message.user(leave), Message.user("continue")]
yield* contextHook(request(build, messages))
expect(messages).toHaveLength(3)
expect(persisted).toHaveLength(0)
}),
)
it.effect("treats reminder text quoted inside a larger message as not live", () =>
Effect.gen(function* () {
const { enter } = yield* reminders
const { persisted, contextHook } = yield* run()
// Mirrors a compaction checkpoint quoting the reminder inside <recent-context>.
const messages = [Message.user(`<conversation-checkpoint>\n${enter}\n</conversation-checkpoint>`)]
yield* contextHook(request(plan, messages))
expect(messages).toHaveLength(2)
expect(persisted).toHaveLength(1)
}),
)
})
@@ -60,14 +60,14 @@ describe("LLMGatewayPlugin", () => {
})
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.make("llmgateway"), (provider) => {
provider.disabled = true
provider.activation = "disabled"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://api.llmgateway.io/v1" }
})
})
yield* addPlugin()
expect((yield* catalog.provider.get(Provider.ID.make("llmgateway")))?.disabled).toBe(true)
expect((yield* catalog.provider.get(Provider.ID.make("llmgateway")))?.activation).toBe("disabled")
expect((yield* catalog.provider.get(Provider.ID.make("llmgateway")))?.headers).toBeUndefined()
}),
)
@@ -1,4 +1,6 @@
import { Money } from "@opencode-ai/schema/money"
import { Agent } from "@opencode-ai/schema/agent"
import { Session } from "@opencode-ai/schema/session"
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
@@ -27,9 +29,14 @@ function required<T>(value: T | undefined): T {
return value
}
const httpMiddlewareCount = Effect.fn(function* () {
const hooks = yield* PluginHooks.Service
return Number(yield* hooks.has("session", "http.request")) + Number(yield* hooks.has("session", "http.response"))
const http = Effect.fn(function* (providerID: Provider.ID, url: string) {
const event = yield* (yield* PluginHooks.Service).trigger("session", "http.request", {
sessionID: Session.ID.make("ses_test"),
agent: Agent.ID.make("build"),
model: Model.Ref.make({ providerID, id: Model.ID.make("gpt-5.5") }),
request: new Request(url, { method: "POST", body: "{}" }),
})
return { url: event.request.url, headers: Object.fromEntries(event.request.headers.entries()) }
})
describe("OpenAIPlugin", () => {
@@ -103,13 +110,22 @@ describe("OpenAIPlugin", () => {
})
yield* addPlugin()
const request = yield* http(Provider.ID.openai, "https://api.openai.com/v1/responses")
const custom = yield* http(Provider.ID.make("custom-openai"), "https://custom.example/v1/responses")
const proxy = yield* http(Provider.ID.openai, "https://proxy.example/v1/responses?region=us")
const provider = required(yield* catalog.provider.get(Provider.ID.openai))
expect(provider.package).toBe("@opencode-ai/ai/providers/openai")
expect(provider.settings).toMatchObject({ baseURL: "https://chatgpt.com/backend-api/codex" })
expect(provider.headers).toMatchObject({ originator: "opencode", "chatgpt-account-id": "acct_123" })
expect(yield* httpMiddlewareCount()).toBe(0)
expect(request.url).toBe("https://chatgpt.com/backend-api/codex/responses")
expect(request.headers).toMatchObject({ originator: "opencode", "session-id": "ses_test" })
expect(custom.headers).not.toHaveProperty("originator")
expect(proxy.url).toBe("https://proxy.example/v1/responses?region=us")
expect(proxy.headers).toMatchObject({ originator: "opencode", "session-id": "ses_test" })
const eligible = required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5")))
expect(eligible.package).toBe("@opencode-ai/ai/providers/openai")
expect(eligible.headers).toMatchObject({ originator: "opencode", "chatgpt-account-id": "acct_123" })
expect(eligible.cost).toEqual([])
expect(eligible.limit).toEqual({ context: 400_000, input: 272_000, output: 128_000 })
expect(eligible.enabled).toBe(true)
@@ -151,13 +167,15 @@ describe("OpenAIPlugin", () => {
})
yield* addPlugin()
const request = yield* http(Provider.ID.openai, "https://api.openai.com/v1/responses")
const provider = required(yield* catalog.provider.get(Provider.ID.openai))
const model = required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5")))
expect(model.package).toBe("@opencode-ai/ai/providers/openai")
expect(model.enabled).toBe(true)
expect(model.limit).toEqual({ context: 1_050_000, input: 922_000, output: 128_000 })
expect(request.headers).not.toHaveProperty("originator")
expect(provider.headers).not.toHaveProperty("originator")
expect(yield* httpMiddlewareCount()).toBe(0)
expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-4.1"))).enabled).toBe(true)
}),
)
@@ -347,6 +347,8 @@ describe("OpencodePlugin", () => {
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(Provider.ID.opencode)).settings?.apiKey).toBe("public")
expect(required(yield* catalog.provider.get(Provider.ID.opencode)).activation).toBe("enabled")
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toContain(Provider.ID.opencode)
expect(required(yield* catalog.model.get(Provider.ID.opencode, Model.ID.make("free"))).enabled).toBe(true)
}),
),

Some files were not shown because too many files have changed in this diff Show More