Compare commits

...

18 Commits

Author SHA1 Message Date
opencode-agent[bot] 0f071468fe chore: update nix node_modules hashes
nix-hashes / compute-hash (blacksmith-4vcpu-ubuntu-2404, x86_64-linux) (push) Waiting to run
nix-hashes / compute-hash (blacksmith-4vcpu-ubuntu-2404-arm, aarch64-linux) (push) Waiting to run
nix-hashes / compute-hash (macos-15-intel, x86_64-darwin) (push) Waiting to run
nix-hashes / compute-hash (macos-latest, aarch64-darwin) (push) Waiting to run
nix-hashes / update-hashes (push) Blocked by required conditions
publish / version (push) Waiting to run
publish / build-cli (push) Blocked by required conditions
publish / sign-cli-windows (push) Blocked by required conditions
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=arm64 host:macos-26 platform_flag:--mac --arm64 target:aarch64-apple-darwin]) (push) Blocked by required conditions
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=x64 host:macos-26-intel platform_flag:--mac --x64 target:x86_64-apple-darwin]) (push) Blocked by required conditions
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404 platform_flag:--linux target:x86_64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404-arm platform_flag:--linux --arm64 target:aarch64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / build-electron (map[host:blacksmith-4vcpu-windows-2025 platform_flag:--win target:x86_64-pc-windows-msvc]) (push) Blocked by required conditions
publish / build-electron (map[host:windows-2025 platform_flag:--win --arm64 target:aarch64-pc-windows-msvc]) (push) Blocked by required conditions
publish / publish (push) Blocked by required conditions
2026-07-28 06:49:16 +00:00
opencode-agent[bot] 7379f9a7cc Apply PR #39261: fix(app): guard reentrant Solid cleanup 2026-07-28 06:31:54 +00:00
opencode-agent[bot] 69f05bb98f Apply PR #38403: fix(ui): standardize v2 tooltip delay 2026-07-28 06:31:54 +00:00
opencode-agent[bot] 62e02e7e37 Apply PR #37927: fix(app): navigate tabs by selection history 2026-07-28 06:31:53 +00:00
opencode-agent[bot] 7abe6dd615 Apply PR #29948: fix(tui): keep command palette available in questions 2026-07-28 06:30:21 +00:00
opencode 8021dbd80f sync release versions for v1.18.8
containers / build (push) Waiting to run
deploy / deploy (push) Waiting to run
docs-locale-sync / sync-locales (push) Waiting to run
generate / generate (push) Waiting to run
nix-eval / nix-eval (push) Waiting to run
nix-hashes / compute-hash (blacksmith-4vcpu-ubuntu-2404, x86_64-linux) (push) Waiting to run
nix-hashes / compute-hash (blacksmith-4vcpu-ubuntu-2404-arm, aarch64-linux) (push) Waiting to run
nix-hashes / compute-hash (macos-15-intel, x86_64-darwin) (push) Waiting to run
nix-hashes / compute-hash (macos-latest, aarch64-darwin) (push) Waiting to run
nix-hashes / update-hashes (push) Blocked by required conditions
publish / version (push) Waiting to run
publish / build-cli (push) Blocked by required conditions
publish / sign-cli-windows (push) Blocked by required conditions
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=arm64 host:macos-26 platform_flag:--mac --arm64 target:aarch64-apple-darwin]) (push) Blocked by required conditions
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=x64 host:macos-26-intel platform_flag:--mac --x64 target:x86_64-apple-darwin]) (push) Blocked by required conditions
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404 platform_flag:--linux target:x86_64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404-arm platform_flag:--linux --arm64 target:aarch64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / build-electron (map[host:blacksmith-4vcpu-windows-2025 platform_flag:--win target:x86_64-pc-windows-msvc]) (push) Blocked by required conditions
publish / build-electron (map[host:windows-2025 platform_flag:--win --arm64 target:aarch64-pc-windows-msvc]) (push) Blocked by required conditions
publish / publish (push) Blocked by required conditions
storybook / storybook build (push) Waiting to run
typecheck / typecheck (push) Waiting to run
2026-07-28 06:07:51 +00:00
opencode-agent[bot] c8487bac54 chore: update nix node_modules hashes 2026-07-28 05:50:46 +00:00
Aiden Cline 484f00ebf4 fix(mcp): recover expired SDK sessions (#39265) 2026-07-28 00:35:19 -05:00
Jack f22053fa14 fix(app): guard reentrant Solid cleanup 2026-07-28 05:06:47 +00:00
Aiden Cline c3be6c4965 fix(mcp): honor callback port in debug (#39259) 2026-07-28 00:00:38 -05:00
opencode-agent[bot] 172d08cb98 chore: update nix node_modules hashes 2026-07-28 04:34:03 +00:00
opencode-agent[bot] 5e39051183 fix(provider): omit deprecated Gemini sampling defaults (#38924)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-07-27 23:21:00 -05:00
opencode-agent[bot] d50b9e8f9a chore: generate 2026-07-28 04:20:55 +00:00
Aiden Cline 921b1c6a34 feat(mcp): upgrade client SDK to v2 (#39247)
Co-authored-by: Matt Carey <mcarey@cloudflare.com>
2026-07-27 23:19:33 -05:00
Luke Parker f77f5a343e fix(ui): standardize tooltip delay 2026-07-27 13:24:01 +08:00
LukeParkerDev 985ee1e2ec fix(app): navigate tabs by selection history 2026-07-20 23:15:53 +10:00
Test 83bee1e776 Merge remote-tracking branch 'origin/dev' into beta-sync-29948
# Conflicts:
#	packages/tui/test/keymap.test.tsx
2026-06-25 07:02:20 +00:00
opencode-agent[bot] 124714ca3a fix(tui): keep command palette available in questions 2026-05-29 23:19:01 +00:00
82 changed files with 1184 additions and 1195 deletions
+46 -95
View File
@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
@@ -96,7 +96,7 @@
},
"packages/cli": {
"name": "@opencode-ai/cli",
"version": "1.18.7",
"version": "1.18.8",
"bin": {
"lildax": "./bin/lildax.cjs",
},
@@ -144,7 +144,7 @@
},
"packages/codemode": {
"name": "@opencode-ai/codemode",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -158,7 +158,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -194,7 +194,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -221,7 +221,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@ai-sdk/anthropic": "3.0.82",
"@ai-sdk/openai": "3.0.48",
@@ -243,7 +243,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -267,7 +267,7 @@
},
"packages/console/support": {
"name": "@opencode-ai/console-support",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode-ai/console-core": "workspace:*",
@@ -287,7 +287,7 @@
},
"packages/core": {
"name": "@opencode-ai/core",
"version": "1.18.7",
"version": "1.18.8",
"bin": {
"opencode": "./bin/opencode",
},
@@ -381,7 +381,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"effect": "catalog:",
@@ -435,7 +435,7 @@
},
"packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -449,7 +449,7 @@
},
"packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"effect": "catalog:",
},
@@ -461,7 +461,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -493,7 +493,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -509,7 +509,7 @@
},
"packages/http-recorder": {
"name": "@opencode-ai/http-recorder",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@effect/platform-node": "4.0.0-beta.83",
"@effect/platform-node-shared": "4.0.0-beta.83",
@@ -540,7 +540,7 @@
},
"packages/llm": {
"name": "@opencode-ai/llm",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14",
@@ -559,7 +559,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.18.7",
"version": "1.18.8",
"bin": {
"opencode": "./bin/opencode",
},
@@ -592,7 +592,7 @@
"@effect/platform-node": "catalog:",
"@ff-labs/fff-bun": "0.9.4",
"@gitlab/opencode-gitlab-auth": "1.3.3",
"@modelcontextprotocol/sdk": "1.29.0",
"@modelcontextprotocol/client": "2.0.0-beta.5",
"@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
@@ -666,6 +666,7 @@
},
"devDependencies": {
"@babel/core": "7.28.4",
"@modelcontextprotocol/server": "2.0.0-beta.5",
"@octokit/webhooks-types": "7.6.1",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/http-recorder": "workspace:*",
@@ -690,7 +691,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode-ai/sdk": "workspace:*",
@@ -766,7 +767,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -781,7 +782,7 @@
},
"packages/server": {
"name": "@opencode-ai/server",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
@@ -796,7 +797,7 @@
},
"packages/session-ui": {
"name": "@opencode-ai/session-ui",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
@@ -841,7 +842,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -854,7 +855,7 @@
},
"packages/stats/app": {
"name": "@opencode-ai/stats-app",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
@@ -888,7 +889,7 @@
},
"packages/stats/core": {
"name": "@opencode-ai/stats-core",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
@@ -907,7 +908,7 @@
},
"packages/stats/server": {
"name": "@opencode-ai/stats-server",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -949,7 +950,7 @@
},
"packages/tui": {
"name": "@opencode-ai/tui",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
@@ -976,7 +977,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -1027,7 +1028,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -1070,9 +1071,9 @@
],
"patchedDependencies": {
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
"@modelcontextprotocol/client@2.0.0-beta.5": "patches/@modelcontextprotocol%2Fclient@2.0.0-beta.5.patch",
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
@@ -1674,8 +1675,6 @@
"@hey-api/types": ["@hey-api/types@0.1.2", "", {}, "sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA=="],
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
"@hono/standard-validator": ["@hono/standard-validator@0.2.0", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-pFq0UVAnjzXcDAgqFpDeVL3MOUPrlIh/kPqBDvbCYoThVhhS+Vf37VcdsakdOFFGiqoiYVxp3LifXFhGhp/rgQ=="],
"@ibm/plex": ["@ibm/plex@6.4.1", "", { "dependencies": { "@ibm/telemetry-js": "^1.5.1" } }, "sha512-fnsipQywHt3zWvsnlyYKMikcVI7E2fEwpiPnIHFqlbByXVfQfANAAeJk1IV4mNnxhppUIDlhU0TzwYwL++Rn2g=="],
@@ -1830,7 +1829,11 @@
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
"@modelcontextprotocol/client": ["@modelcontextprotocol/client@2.0.0-beta.5", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0-beta.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "jose": "^6.1.3", "pkce-challenge": "^5.0.0", "zod": "^4.2.0" } }, "sha512-YuuNm5f2TMoFQRje1UqVP8TJRjijCXMz4ckvoVpx1cUXuBEmykWQ2d8R536pek6UKcXT41T5nWc4qR1JFIbEmg=="],
"@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.0.0-beta.5", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-HKbY9XTbsDy1Y6r2I55TGE3JEapM0vg96e1MUmBIF9LGjos5gjhcIrTz1yvBPLg2aFKHjwhUAQfRdrCEnPxNew=="],
"@modelcontextprotocol/server": ["@modelcontextprotocol/server@2.0.0-beta.5", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0-beta.5", "zod": "^4.2.0" } }, "sha512-i1E5l75rQKsgY/AKAIspgMBH1vEL7dqiK7tHr0L+raYcb0SWOziqNGJXGIG6NY4AlXDWIKGJQGB7Nqfs3oUi5g=="],
"@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
@@ -3390,7 +3393,7 @@
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
@@ -3402,8 +3405,6 @@
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
"crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="],
"crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
@@ -3708,8 +3709,6 @@
"express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="],
"express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="],
"expressive-code": ["expressive-code@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "@expressive-code/plugin-frames": "^0.41.7", "@expressive-code/plugin-shiki": "^0.41.7", "@expressive-code/plugin-text-markers": "^0.41.7" } }, "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA=="],
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
@@ -4094,8 +4093,6 @@
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
@@ -5004,8 +5001,6 @@
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
@@ -5954,15 +5949,13 @@
"@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"@modelcontextprotocol/client/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
"@modelcontextprotocol/sdk/hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="],
"@modelcontextprotocol/client/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@modelcontextprotocol/sdk/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
"@modelcontextprotocol/core/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"@modelcontextprotocol/sdk/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@modelcontextprotocol/server/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@npmcli/config/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="],
@@ -6238,6 +6231,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=="],
"body-parser/content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
@@ -6320,6 +6315,8 @@
"execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],
"express/content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
@@ -6468,8 +6465,6 @@
"roarr/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
"router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
@@ -6798,58 +6793,28 @@
"@malept/flatpak-bundler/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
"@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
"@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
"@modelcontextprotocol/sdk/express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
"@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"@modelcontextprotocol/sdk/express/send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
"@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
"@modelcontextprotocol/sdk/express/type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
"@octokit/auth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="],
"@octokit/auth-app/@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
"@octokit/auth-app/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"@octokit/auth-app/@octokit/request-error/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
"@octokit/auth-oauth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="],
"@octokit/auth-oauth-app/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="],
"@octokit/auth-oauth-app/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"@octokit/auth-oauth-app/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/auth-oauth-device/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="],
"@octokit/auth-oauth-device/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="],
"@octokit/auth-oauth-device/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"@octokit/auth-oauth-device/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/auth-oauth-user/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="],
"@octokit/auth-oauth-user/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="],
"@octokit/auth-oauth-user/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"@octokit/auth-oauth-user/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
@@ -6862,14 +6827,10 @@
"@octokit/graphql/@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
"@octokit/graphql/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"@octokit/graphql/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
"@octokit/oauth-methods/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="],
"@octokit/oauth-methods/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"@octokit/oauth-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/plugin-paginate-rest/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
@@ -7378,10 +7339,6 @@
"@jsx-email/cli/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
"@modelcontextprotocol/sdk/express/type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"@octokit/auth-app/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/auth-app/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
@@ -7390,20 +7347,14 @@
"@octokit/plugin-paginate-rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="],
"@octokit/plugin-paginate-rest/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"@octokit/plugin-paginate-rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="],
"@octokit/rest/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@opencode-ai/core/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-szhy0258K6IMWi3WRrAzRfhEEIDkGTS04KQwQw+DXyI=",
"aarch64-linux": "sha256-eVqUhXGKgx7rmRzpdr9G59Xrxvj2J0Dhx4Q7QRBHKiQ=",
"aarch64-darwin": "sha256-r/xAkD/yktfxOVTAArWYb0ZMWe/hEqh80dkYsigRkm8=",
"x86_64-darwin": "sha256-xEKSQZM4gYoz9RoaHEvuxrT8AFXHx8hMD1oKGi8lc3Y="
"x86_64-linux": "sha256-TJg4wSxeEUqsmSjVt5kGmygCzKISN0uhk+HXDNMqWZw=",
"aarch64-linux": "sha256-W1hM9pmrIFJafrbNlV4anP+j1xEBBGSYpvMyW7Kdx5k=",
"aarch64-darwin": "sha256-EVhn24RmSNth2J0UzkvbuHfd9fliHzb6al7p3RJWYF8=",
"x86_64-darwin": "sha256-r9kxGH3Sl/BDWZao6yuJhsltlMIZ0q25NdLSyLQi9HI="
}
}
+2 -2
View File
@@ -154,8 +154,8 @@
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch"
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
"@modelcontextprotocol/client@2.0.0-beta.5": "patches/@modelcontextprotocol%2Fclient@2.0.0-beta.5.patch"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.18.7",
"version": "1.18.8",
"description": "",
"type": "module",
"exports": {
@@ -88,7 +88,7 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
class="w-full"
placement="right-start"
gutter={6}
openDelay={0}
delay="intent"
contentStyle={{ "font-family": "var(--v2-font-family-sans)" }}
value={
<ModelTooltip
@@ -460,7 +460,7 @@ function ModelSelectorPopoverV2View(props: {
class="w-full"
placement="right-start"
gutter={6}
openDelay={0}
delay="intent"
value={
<ModelTooltip
model={item}
@@ -47,7 +47,7 @@ export const PromptContextItems: Component<ContextItemsProps> = (props) => {
</span>
}
placement="top"
openDelay={800}
{...(!props.newLayoutDesigns ? { openDelay: 800 } : {})}
>
<div
classList={{
@@ -52,12 +52,7 @@ export const PromptImageAttachments: Component<PromptImageAttachmentsProps> = (p
<For each={props.comments ?? []}>
{(item) => (
<div class="relative group shrink-0">
<TooltipV2
value={item.comment}
placement="top"
openDelay={800}
contentClass="max-w-[300px] break-words"
>
<TooltipV2 value={item.comment} placement="top" contentClass="max-w-[300px] break-words">
<CommentCardV2
comment={item.comment ?? ""}
path={item.path}
+16
View File
@@ -338,6 +338,22 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
keybind: "mod+shift+t",
onSelect: () => tabsStoreActions.reopenClosedTab(),
},
{
id: `tab.prev`,
category: "tab",
title: "",
keybind: `mod+option+ArrowLeft,ctrl+shift+tab`,
hidden: true,
onSelect: tabs.previous,
},
{
id: `tab.next`,
category: "tab",
title: "",
keybind: `mod+option+ArrowRight,ctrl+tab`,
hidden: true,
onSelect: tabs.next,
},
].filter((v) => v !== undefined)
})
@@ -0,0 +1,43 @@
import { describe, expect, test } from "bun:test"
import { nextTab, previousTab, rememberTab, type TabHistory } from "./tab-history"
function history(): TabHistory {
return { stack: [], index: -1 }
}
describe("tab history", () => {
test("moves backward and forward through selected tabs", () => {
const selected = ["a", "b", "c"].reduce(rememberTab, history())
const available = new Set(selected.stack)
const previous = previousTab(selected, available)
expect(previous?.key).toBe("b")
const first = previousTab(previous!.state, available)
expect(first?.key).toBe("a")
const next = nextTab(first!.state, available)
expect(next?.key).toBe("b")
})
test("replaces forward history after a new selection", () => {
const selected = ["a", "b", "c"].reduce(rememberTab, history())
const previous = previousTab(selected, new Set(selected.stack))
const next = rememberTab(previous!.state, "d")
expect(next).toEqual({ stack: ["a", "b", "d"], index: 2 })
expect(nextTab(next, new Set(next.stack))).toBeUndefined()
})
test("skips tabs that are no longer open", () => {
const selected = ["a", "b", "c"].reduce(rememberTab, history())
expect(previousTab(selected, new Set(["a", "c"]))?.key).toBe("a")
})
test("skips a repeated current tab after closing the previous selection", () => {
const selected = ["a", "b", "c", "b"].reduce(rememberTab, history())
expect(previousTab(selected, new Set(["a", "b"]))?.key).toBe("a")
})
})
+28
View File
@@ -0,0 +1,28 @@
const MAX_TAB_HISTORY = 100
export type TabHistory = {
stack: string[]
index: number
}
export function rememberTab(state: TabHistory, key: string): TabHistory {
if (state.stack[state.index] === key) return state
const stack = state.stack.slice(0, state.index + 1).concat(key).slice(-MAX_TAB_HISTORY)
return { stack, index: stack.length - 1 }
}
export function previousTab(state: TabHistory, available: Set<string>) {
return move(state, -1, available)
}
export function nextTab(state: TabHistory, available: Set<string>) {
return move(state, 1, available)
}
function move(state: TabHistory, offset: -1 | 1, available: Set<string>) {
const current = state.stack[state.index]
for (let index = state.index + offset; index >= 0 && index < state.stack.length; index += offset) {
const key = state.stack[index]
if (key && key !== current && available.has(key)) return { state: { ...state, index }, key }
}
}
+16
View File
@@ -12,6 +12,7 @@ import { sessionHref } from "@/utils/session-route"
import { createTabMemory } from "./tab-memory"
import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs"
import { createDraftPromptSession, type PromptModel } from "./prompt-state"
import { nextTab, previousTab, rememberTab, type TabHistory } from "./tab-history"
export type SessionTab = {
type: "session"
@@ -79,6 +80,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
const memory = createTabMemory(getOwner())
const closing = new Set<string>()
let history: TabHistory = { stack: [], index: -1 }
let recentWrite = 0
let recentValue: string | undefined
@@ -150,10 +152,21 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
const navigateTab = (tab: Tab) => {
const href = tabHref(tab)
history = rememberTab(history, tabKey(tab))
setRecentKey(tabKey(tab))
navigate(href)
}
const moveHistory = (direction: "previous" | "next") => {
const available = new Set(store.map(tabKey))
const result = direction === "previous" ? previousTab(history, available) : nextTab(history, available)
if (!result) return
const tab = store.find((item) => tabKey(item) === result.key)
if (!tab) return
history = result.state
navigateTab(tab)
}
const removeTab = (index: number) => {
const tab = store[index]
if (!tab) return
@@ -359,8 +372,11 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
select: navigateTab,
remember(tab: Tab) {
const key = tabKey(tab)
history = rememberTab(history, key)
if (recentKey() !== key) setRecentKey(key)
},
previous: () => moveHistory("previous"),
next: () => moveHistory("next"),
toggleHome(input: { home: boolean; current?: Tab }) {
if (input.home) {
const tab = store.find((tab) => tabKey(tab) === recentKey())
+11
View File
@@ -2,6 +2,17 @@ import { describe, expect, test } from "bun:test"
import { DESKTOP_MENU } from "./desktop-menu"
describe("desktop menu", () => {
test("navigates between tabs", () => {
const items = DESKTOP_MENU.flatMap((menu) => menu.items ?? []).filter(
(item) => item.type === "item" && (item.label === "Previous Tab" || item.label === "Next Tab"),
)
expect(items).toEqual([
{ type: "item", label: "Previous Tab", command: "tab.prev", accelerator: { macos: "Option+Up" } },
{ type: "item", label: "Next Tab", command: "tab.next", accelerator: { macos: "Option+Down" } },
])
})
test("exports logs through the desktop command registry", () => {
const items = DESKTOP_MENU.flatMap((menu) => menu.items ?? []).filter(
(item) => item.type === "item" && item.label === "Export Logs...",
+2 -2
View File
@@ -168,8 +168,8 @@ export const DESKTOP_MENU: DesktopMenu[] = [
{ type: "item", label: "Back", command: "common.goBack", accelerator: { macos: "Cmd+[" } },
{ type: "item", label: "Forward", command: "common.goForward", accelerator: { macos: "Cmd+]" } },
{ type: "separator" },
{ type: "item", label: "Previous Session", command: "session.previous", accelerator: { macos: "Option+Up" } },
{ type: "item", label: "Next Session", command: "session.next", accelerator: { macos: "Option+Down" } },
{ type: "item", label: "Previous Tab", command: "tab.prev", accelerator: { macos: "Option+Up" } },
{ type: "item", label: "Next Tab", command: "tab.next", accelerator: { macos: "Option+Down" } },
{ type: "separator" },
{
type: "item",
-1
View File
@@ -271,7 +271,6 @@ function ProviderTip(props: { ready: () => boolean; connected: () => boolean; op
<TooltipV2
class="hover-reveal absolute left-full top-0 flex h-6 w-7 items-center justify-end delay-0 duration-0 group-hover/provider-tip:delay-[250ms] group-hover/provider-tip:duration-150 group-hover/provider-tip:opacity-100 focus-within:delay-0 focus-within:duration-0 focus-within:opacity-100"
placement="top"
openDelay={1000}
value={language.t("common.dismiss")}
>
<button
@@ -0,0 +1,64 @@
import { expect, test } from "bun:test"
import { MetaProvider, Title } from "@solidjs/meta"
import { MemoryRouter, Route, createMemoryHistory, useParams } from "@solidjs/router"
import { createMemo } from "solid-js"
import { createComponent, render } from "solid-js/web"
test("route cleanup cannot invalidate an owner list being disposed", async () => {
const host = document.createElement("div")
document.body.append(host)
const history = createMemoryHistory()
const RepoPage = () => {
const params = useParams<{ id?: string }>()
const title = createMemo(() => params.id ?? "")
const button = document.createElement("button")
button.textContent = "Back"
button.addEventListener("click", () => history.set({ value: "/", scroll: false, replace: false }))
return [
createComponent(Title, {
get children() {
return title()
},
}),
button,
]
}
const HomePage = () => {
const button = document.createElement("button")
button.textContent = "Go"
button.addEventListener("click", () => history.set({ value: "/project", scroll: false, replace: false }))
return button
}
const App = () =>
createComponent(MetaProvider, {
get children() {
return createComponent(MemoryRouter, {
history,
get children() {
return [
createComponent(Route, { path: "/", component: HomePage }),
createComponent(Route, { path: "/:id", component: RepoPage }),
]
},
})
},
})
const dispose = render(() => createComponent(App, {}), host)
const go = host.querySelector("button")
expect(go?.textContent).toBe("Go")
go?.click()
await new Promise((resolve) => setTimeout(resolve, 0))
const back = host.querySelector("button")
expect(back?.textContent).toBe("Back")
back?.click()
await new Promise((resolve) => setTimeout(resolve, 0))
expect(host.querySelector("button")?.textContent).toBe("Go")
dispose()
host.remove()
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/cli",
"version": "1.18.7",
"version": "1.18.8",
"type": "module",
"license": "MIT",
"bin": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/codemode",
"version": "1.18.7",
"version": "1.18.8",
"description": "Effect-native confined code execution over schema-described tools",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.18.7",
"version": "1.18.8",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.18.7",
"version": "1.18.8",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.18.7",
"version": "1.18.8",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.18.7",
"version": "1.18.8",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-support",
"version": "1.18.7",
"version": "1.18.8",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.7",
"version": "1.18.8",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.18.7",
"version": "1.18.8",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.7",
"version": "1.18.8",
"name": "@opencode-ai/effect-drizzle-sqlite",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.7",
"version": "1.18.8",
"name": "@opencode-ai/effect-sqlite-node",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.18.7",
"version": "1.18.8",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.18.7",
"version": "1.18.8",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.7",
"version": "1.18.8",
"name": "@opencode-ai/http-recorder",
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.7",
"version": "1.18.8",
"name": "@opencode-ai/llm",
"type": "module",
"license": "MIT",
+3 -2
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.7",
"version": "1.18.8",
"name": "opencode",
"type": "module",
"license": "MIT",
@@ -30,6 +30,7 @@
},
"devDependencies": {
"@babel/core": "7.28.4",
"@modelcontextprotocol/server": "2.0.0-beta.5",
"@octokit/webhooks-types": "7.6.1",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/http-recorder": "workspace:*",
@@ -80,7 +81,7 @@
"@effect/platform-node": "catalog:",
"@ff-labs/fff-bun": "0.9.4",
"@gitlab/opencode-gitlab-auth": "1.3.3",
"@modelcontextprotocol/sdk": "1.29.0",
"@modelcontextprotocol/client": "2.0.0-beta.5",
"@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
+46 -103
View File
@@ -2,13 +2,10 @@ import { cmd } from "./cmd"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { effectCmd } from "../effect-cmd"
import { Cause } from "effect"
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js"
import { Client, StreamableHTTPClientTransport, UnauthorizedError } from "@modelcontextprotocol/client"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
import { MCP } from "../../mcp"
import { CLIENT_OPTIONS, MCP } from "../../mcp"
import { McpAuth } from "../../mcp/auth"
import { McpOAuthProvider } from "../../mcp/oauth-provider"
import { Config } from "@/config/config"
@@ -731,107 +728,53 @@ export const McpDebugCommand = effectCmd({
const spinner = prompts.spinner()
spinner.start("Testing connection...")
// Test basic HTTP connectivity first
try {
const response = await fetch(serverConfig.url, {
method: "POST",
headers: {
...serverConfig.headers,
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined
let authorizationUrl: URL | undefined
const authProvider = new McpOAuthProvider(
serverName,
serverConfig.url,
{
clientId: oauthConfig?.clientId,
clientSecret: oauthConfig?.clientSecret,
scope: oauthConfig?.scope,
callbackPort: oauthConfig?.callbackPort,
redirectUri: oauthConfig?.redirectUri,
},
{
onRedirect: async (url) => {
authorizationUrl = url
},
body: JSON.stringify({
jsonrpc: "2.0",
method: "initialize",
params: {
protocolVersion: LATEST_PROTOCOL_VERSION,
capabilities: {},
clientInfo: { name: "opencode-debug", version: InstallationVersion },
},
id: 1,
}),
})
},
auth,
)
const transport = new StreamableHTTPClientTransport(new URL(serverConfig.url), {
authProvider,
requestInit: serverConfig.headers ? { headers: serverConfig.headers } : undefined,
})
const client = new Client({ name: "opencode-debug", version: InstallationVersion }, CLIENT_OPTIONS)
spinner.stop(`HTTP response: ${response.status} ${response.statusText}`)
// Check for WWW-Authenticate header
const wwwAuth = response.headers.get("www-authenticate")
if (wwwAuth) {
prompts.log.info(`WWW-Authenticate: ${wwwAuth}`)
}
if (response.status === 401) {
prompts.log.info("Initial unauthenticated check returned 401, so this server requires OAuth")
// Try to discover OAuth metadata
const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined
const authProvider = new McpOAuthProvider(
serverName,
serverConfig.url,
{
clientId: oauthConfig?.clientId,
clientSecret: oauthConfig?.clientSecret,
scope: oauthConfig?.scope,
redirectUri: oauthConfig?.redirectUri,
},
{
onRedirect: async () => {},
},
auth,
)
prompts.log.info("Testing OAuth flow (without completing authorization)...")
// Try creating transport with auth provider to trigger discovery
const transport = new StreamableHTTPClientTransport(new URL(serverConfig.url), {
authProvider,
requestInit: serverConfig.headers ? { headers: serverConfig.headers } : undefined,
})
try {
const client = new Client({
name: "opencode-debug",
version: InstallationVersion,
})
await client.connect(transport)
prompts.log.success("Connection successful (already authenticated)")
await client.close()
} catch (error) {
if (error instanceof UnauthorizedError) {
prompts.log.info(`OAuth flow triggered: ${error.message}`)
// Check if dynamic registration would be attempted
const clientInfo = await authProvider.clientInformation()
if (clientInfo) {
prompts.log.info(`Client ID available: ${clientInfo.client_id}`)
} else {
prompts.log.info("No client ID - dynamic registration will be attempted")
}
} else {
prompts.log.error(`Connection error: ${error instanceof Error ? error.message : String(error)}`)
}
}
} else if (response.status >= 200 && response.status < 300) {
prompts.log.success("Server responded successfully (no auth required or already authenticated)")
const body = await response.text()
try {
const json = JSON.parse(body)
if (json.result?.serverInfo) {
prompts.log.info(`Server info: ${JSON.stringify(json.result.serverInfo)}`)
}
} catch {
// Not JSON, ignore
}
} else {
prompts.log.warn(`Unexpected status: ${response.status}`)
const body = await response.text().catch(() => "")
if (body) {
prompts.log.info(`Response body: ${body.substring(0, 500)}`)
}
}
try {
await client.connect(transport)
spinner.stop("SDK connection successful")
prompts.log.success(
`Connected using MCP ${client.getNegotiatedProtocolVersion() ?? "unknown"} (${client.getProtocolEra() ?? "unknown"})`,
)
const serverInfo = client.getServerVersion()
if (serverInfo) prompts.log.info(`Server info: ${JSON.stringify(serverInfo)}`)
} catch (error) {
spinner.stop("Connection failed", 1)
prompts.log.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
if (error instanceof UnauthorizedError) {
spinner.stop("OAuth required")
prompts.log.info(`OAuth flow triggered: ${error.message}`)
if (authorizationUrl) prompts.log.info(`Authorization URL: ${authorizationUrl}`)
const clientInfo = await authProvider.clientInformation()
if (clientInfo) prompts.log.info(`Client ID available: ${clientInfo.client_id}`)
if (!clientInfo) prompts.log.info("No client ID - dynamic registration will be attempted")
} else {
spinner.stop("Connection failed", 1)
prompts.log.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
}
} finally {
await client.close().catch(() => {})
}
prompts.outro("Debug complete")
+4
View File
@@ -11,6 +11,7 @@ export const Tokens = Schema.Struct({
refreshToken: Schema.mutableKey(Schema.optional(Schema.String)),
expiresAt: Schema.mutableKey(Schema.optional(Schema.Number)),
scope: Schema.mutableKey(Schema.optional(Schema.String)),
issuer: Schema.mutableKey(Schema.optional(Schema.String)),
})
export type Tokens = Schema.Schema.Type<typeof Tokens>
@@ -19,6 +20,9 @@ export const ClientInfo = Schema.Struct({
clientSecret: Schema.mutableKey(Schema.optional(Schema.String)),
clientIdIssuedAt: Schema.mutableKey(Schema.optional(Schema.Number)),
clientSecretExpiresAt: Schema.mutableKey(Schema.optional(Schema.Number)),
redirectUris: Schema.mutableKey(Schema.optional(Schema.Array(Schema.String))),
issuer: Schema.mutableKey(Schema.optional(Schema.String)),
configPreRegistered: Schema.mutableKey(Schema.optional(Schema.Boolean)),
})
export type ClientInfo = Schema.Schema.Type<typeof ClientInfo>
+44 -91
View File
@@ -1,77 +1,57 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import {
CallToolResultSchema,
ListToolsResultSchema,
ToolSchema,
type Tool as MCPToolDef,
} from "@modelcontextprotocol/sdk/types.js"
import { Client, type CallToolResult, type Tool as MCPToolDef } from "@modelcontextprotocol/client"
import { dynamicTool, jsonSchema, type JSONSchema7, type Tool } from "ai"
import { Effect } from "effect"
const DEFAULT_TIMEOUT = 30_000
const MAX_LIST_PAGES = 1_000
const TolerantListToolsResultSchema = ListToolsResultSchema.extend({
tools: ToolSchema.omit({ outputSchema: true }).array(),
})
export interface McpTool {
readonly def: MCPToolDef
readonly client: Client
readonly timeout?: number
}
export async function paginate<T, R extends { nextCursor?: string }>(
list: (cursor?: string) => Promise<R>,
items: (result: R) => T[],
) {
const result: T[] = []
const cursors = new Set<string>()
let cursor: string | undefined
for (let page = 0; page < MAX_LIST_PAGES; page++) {
const page = await list(cursor)
result.push(...items(page))
if (page.nextCursor === undefined) return result
if (cursors.has(page.nextCursor)) throw new Error(`MCP list returned duplicate cursor: ${page.nextCursor}`)
cursors.add(page.nextCursor)
cursor = page.nextCursor
}
throw new Error(`MCP list exceeded ${MAX_LIST_PAGES} pages`)
export async function callTool(
tool: McpTool,
args: Record<string, unknown>,
signal?: AbortSignal,
): Promise<CallToolResult> {
const result = await tool.client.callTool(
{ name: tool.def.name, arguments: args },
{
resetTimeoutOnProgress: true,
signal,
timeout: tool.timeout,
// The MCP SDK only sends a progress token when this hook is present, enabling timeout resets.
onprogress: () => {},
},
)
if (result.isError)
throw new Error(
result.content
.flatMap((item) => (item.type === "text" ? [item.text] : []))
.filter((text) => text.trim())
.join("\n\n") || "MCP tool returned an error",
)
return result
}
export function defs(client: Client, timeout?: number) {
return listTools(client, timeout ?? DEFAULT_TIMEOUT).pipe(Effect.catch(() => Effect.void))
}
export function convertTool(mcpTool: MCPToolDef, client: Client, timeout?: number): Tool {
export function convertTool(tool: McpTool): Tool {
const inputSchema: JSONSchema7 = {
...(mcpTool.inputSchema as JSONSchema7),
...(tool.def.inputSchema as JSONSchema7),
type: "object",
properties: (mcpTool.inputSchema.properties ?? {}) as JSONSchema7["properties"],
properties: (tool.def.inputSchema.properties ?? {}) as JSONSchema7["properties"],
additionalProperties: false,
}
return dynamicTool({
description: mcpTool.description ?? "",
description: tool.def.description ?? "",
inputSchema: jsonSchema(inputSchema),
execute: async (args: unknown, options) => {
const result = await client.callTool(
{
name: mcpTool.name,
arguments: (args || {}) as Record<string, unknown>,
},
CallToolResultSchema,
{
resetTimeoutOnProgress: true,
signal: options.abortSignal,
timeout,
// The MCP SDK only sends a progress token when this hook is present, enabling timeout resets.
onprogress: () => {},
},
)
if (result.isError)
throw new Error(
result.content
.flatMap((item) => (item.type === "text" ? [item.text] : []))
.filter((text) => text.trim())
.join("\n\n") || "MCP tool returned an error",
)
const result = await callTool(tool, (args || {}) as Record<string, unknown>, options.abortSignal)
if (result.content.length > 0 || result.structuredContent === undefined || result.structuredContent === null)
return result
return {
@@ -118,53 +98,26 @@ export const sanitize = (value: string) => value.replace(/[^a-zA-Z0-9_-]/g, "_")
export const toolName = (clientName: string, name: string) => sanitize(clientName) + "_" + sanitize(name)
export function prompts(client: Client, timeout?: number) {
if (!client.getServerCapabilities()?.prompts) return Promise.resolve([])
return paginate(
(cursor) => client.listPrompts(cursor === undefined ? undefined : { cursor }, { timeout }),
(result) => result.prompts,
)
export async function prompts(client: Client, timeout?: number) {
if (!client.getServerCapabilities()?.prompts) return []
return (await client.listPrompts(undefined, { timeout })).prompts
}
export function resources(client: Client, timeout?: number) {
if (!client.getServerCapabilities()?.resources) return Promise.resolve([])
return paginate(
(cursor) => client.listResources(cursor === undefined ? undefined : { cursor }, { timeout }),
(result) => result.resources,
)
export async function resources(client: Client, timeout?: number) {
if (!client.getServerCapabilities()?.resources) return []
return (await client.listResources(undefined, { timeout })).resources
}
export function resourceTemplates(client: Client, timeout?: number) {
if (!client.getServerCapabilities()?.resources) return Promise.resolve([])
return paginate(
(cursor) => client.listResourceTemplates(cursor === undefined ? undefined : { cursor }, { timeout }),
(result) => result.resourceTemplates,
)
export async function resourceTemplates(client: Client, timeout?: number) {
if (!client.getServerCapabilities()?.resources) return []
return (await client.listResourceTemplates(undefined, { timeout })).resourceTemplates
}
function listTools(client: Client, timeout: number) {
return Effect.tryPromise({
try: () =>
paginate(
async (cursor) => {
const params = cursor === undefined ? undefined : { cursor }
try {
return await client.listTools(params, { timeout })
} catch (error) {
if (!(error instanceof Error) || !isOutputSchemaValidationError(error)) throw error
return client.request({ method: "tools/list", params }, TolerantListToolsResultSchema, { timeout })
}
},
(result) => result.tools,
),
try: async () => (await client.listTools(undefined, { timeout })).tools,
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
})
}
function isOutputSchemaValidationError(error: Error) {
return /can't resolve reference|resolves to more than one schema|outputSchema|schema.*reference|reference.*schema/i.test(
error.message,
)
}
export * as McpCatalog from "./catalog"
+52 -30
View File
@@ -3,18 +3,18 @@ import { pathToFileURL } from "node:url"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Client, type ClientOptions } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
import {
ListRootsRequestSchema,
Client,
type ClientOptions,
StreamableHTTPClientTransport,
SSEClientTransport,
UnauthorizedError,
RegistrationRejectedError,
SdkHttpError,
type LoggingMessageNotification,
LoggingMessageNotificationSchema,
type Tool as MCPToolDef,
ToolListChangedNotificationSchema,
} from "@modelcontextprotocol/sdk/types.js"
} from "@modelcontextprotocol/client"
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"
import { Config } from "@/config/config"
import { ConfigMCPV1 } from "@opencode-ai/core/v1/config/mcp"
import { NamedError } from "@opencode-ai/core/util/error"
@@ -36,7 +36,7 @@ import { McpEvent } from "@opencode-ai/schema/mcp-event"
import { McpBrowser } from "./browser"
const DEFAULT_TIMEOUT = 30_000
const CLIENT_OPTIONS = {
export const CLIENT_OPTIONS = {
capabilities: {
// https://github.com/anomalyco/opencode/issues/11948
// sampling: {},
@@ -47,6 +47,8 @@ const CLIENT_OPTIONS = {
// https://github.com/anomalyco/opencode/issues/28567
// tasks: {},
},
versionNegotiation: { mode: "auto" },
listMaxPages: 1_000,
} satisfies ClientOptions
export const Resource = Schema.Struct({
@@ -70,13 +72,19 @@ export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("MCP
name: Schema.String,
}) {}
type MCPClient = Client
type MCPClient = Client & { onToolsChanged?: (error: Error | null) => void }
function createClient(directory: string) {
const client = new Client({ name: "opencode", version: InstallationVersion }, CLIENT_OPTIONS)
client.setRequestHandler(ListRootsRequestSchema, () =>
Promise.resolve({ roots: [{ uri: pathToFileURL(directory).href }] }),
const client: MCPClient = new Client(
{ name: "opencode", version: InstallationVersion },
{
...CLIENT_OPTIONS,
listChanged: {
tools: { autoRefresh: false, onChanged: (error) => client.onToolsChanged?.(error) },
},
},
)
client.setRequestHandler("roots/list", async () => ({ roots: [{ uri: pathToFileURL(directory).href }] }))
return client
}
@@ -154,12 +162,7 @@ export interface ServerInstructions {
}
/** An MCP tool in its native shape; consumers adapt it to their own tool format. */
export interface McpTool {
/** Shared cached definition; consumers must copy rather than mutate it. */
readonly def: MCPToolDef
readonly client: MCPClient
readonly timeout?: number
}
export type McpTool = McpCatalog.McpTool
export interface Interface {
readonly status: () => Effect.Effect<Record<string, Status>>
@@ -190,7 +193,11 @@ export interface Interface {
mcpName: string,
onAuthorization?: (authorizationUrl: string) => void,
) => Effect.Effect<Status, NotFoundError>
readonly finishAuth: (mcpName: string, authorizationCode: string) => Effect.Effect<Status, NotFoundError>
readonly finishAuth: (
mcpName: string,
authorizationCode: string,
iss?: string,
) => Effect.Effect<Status, NotFoundError>
readonly removeAuth: (mcpName: string) => Effect.Effect<void>
readonly supportsOAuth: (mcpName: string) => Effect.Effect<boolean, NotFoundError>
readonly hasStoredTokens: (mcpName: string) => Effect.Effect<boolean>
@@ -291,11 +298,18 @@ const layer = Layer.effect(
Effect.map((client) => ({ client, transportName: name })),
Effect.catch((error) => {
const lastError = error instanceof Error ? error : new Error(String(error))
const registrationRejected =
error instanceof RegistrationRejectedError ||
lastError.message.includes("registration") ||
lastError.message.includes("client_id")
const isAuthError =
error instanceof UnauthorizedError || (authProvider && lastError.message.includes("OAuth"))
error instanceof UnauthorizedError ||
registrationRejected ||
(authProvider && error instanceof SdkHttpError && error.status === 401) ||
(authProvider && lastError.message.includes("OAuth"))
if (isAuthError) {
if (lastError.message.includes("registration") || lastError.message.includes("client_id")) {
if (registrationRejected) {
lastStatus = {
status: "needs_client_registration" as const,
error: "Server does not support dynamic client registration. Please provide clientId in config.",
@@ -454,12 +468,16 @@ const layer = Layer.effect(
)
}
client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) =>
client.setNotificationHandler("notifications/message", (notification) =>
bridge.promise(serverLog(name, notification.params)),
)
if (!client.getServerCapabilities()?.tools) return
client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
client.onToolsChanged = async (error) => {
if (error) {
await bridge.promise(Effect.logWarning("failed to refresh MCP tools", { server: name, error: error.message }))
return
}
if (s.clients[name] !== client || s.status[name]?.status !== "connected") return
const listed = await bridge.promise(McpCatalog.defs(client, timeout))
@@ -468,7 +486,7 @@ const layer = Layer.effect(
s.defs[name] = listed
await bridge.promise(events.publish(ToolsChanged, { server: name }).pipe(Effect.ignore))
})
}
}
function serverLog(name: string, params: LoggingMessageNotification["params"]) {
@@ -904,7 +922,7 @@ const layer = Layer.effect(
}),
)
const code = yield* Effect.promise(() => callbackPromise)
const callback = yield* Effect.promise(() => callbackPromise)
const storedState = yield* auth.getOAuthState(mcpName)
if (storedState !== result.oauthState) {
@@ -912,16 +930,20 @@ const layer = Layer.effect(
throw new Error("OAuth state mismatch - potential CSRF attack")
}
yield* auth.clearOAuthState(mcpName)
return yield* finishAuth(mcpName, code)
return yield* finishAuth(mcpName, callback.code, callback.iss)
})
const finishAuth = Effect.fn("MCP.finishAuth")(function* (mcpName: string, authorizationCode: string) {
const finishAuth = Effect.fn("MCP.finishAuth")(function* (
mcpName: string,
authorizationCode: string,
iss?: string,
) {
yield* requireMcpConfig(mcpName)
const pending = pendingOAuthTransports.get(mcpName)
if (!pending) throw new Error(`No pending OAuth flow for MCP server: ${mcpName}`)
const error = yield* Effect.tryPromise({
try: () => pending.transport.finishAuth(authorizationCode),
try: () => pending.transport.finishAuth(authorizationCode, iss),
catch: (error) => error,
}).pipe(
Effect.match({
+9 -3
View File
@@ -9,8 +9,13 @@ const OAUTH_CALLBACK_HOST = "127.0.0.1"
let currentPort = OAUTH_CALLBACK_PORT
let currentPath = OAUTH_CALLBACK_PATH
export interface AuthorizationCallback {
code: string
iss?: string
}
interface PendingAuth {
resolve: (code: string) => void
resolve: (callback: AuthorizationCallback) => void
reject: (error: Error) => void
timeout: ReturnType<typeof setTimeout>
}
@@ -49,6 +54,7 @@ function handleRequest(req: import("http").IncomingMessage, res: import("http").
}
const code = url.searchParams.get("code")
const iss = url.searchParams.get("iss") ?? undefined
const state = url.searchParams.get("state")
const error = url.searchParams.get("error")
const errorDescription = url.searchParams.get("error_description")
@@ -95,7 +101,7 @@ function handleRequest(req: import("http").IncomingMessage, res: import("http").
clearTimeout(pending.timeout)
pendingAuths.delete(state)
cleanupStateIndex(state)
pending.resolve(code)
pending.resolve({ code, iss })
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
res.end(OauthCallbackPage.success({ provider: "MCP" }))
@@ -130,7 +136,7 @@ export async function ensureRunning(redirectUri?: string): Promise<void> {
})
}
export function waitForCallback(oauthState: string, mcpName?: string): Promise<string> {
export function waitForCallback(oauthState: string, mcpName?: string): Promise<AuthorizationCallback> {
if (mcpName) mcpNameToState.set(mcpName, oauthState)
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
+63 -22
View File
@@ -1,10 +1,9 @@
import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
import type {
OAuthClientProvider,
OAuthClientMetadata,
OAuthTokens,
OAuthClientInformation,
OAuthClientInformationFull,
} from "@modelcontextprotocol/sdk/shared/auth.js"
StoredOAuthTokens,
StoredOAuthClientInformation,
} from "@modelcontextprotocol/client"
import { Effect } from "effect"
import { McpAuth } from "./auth"
@@ -23,6 +22,14 @@ export interface McpOAuthCallbacks {
onRedirect: (url: URL) => void | Promise<void>
}
function registrationMetadata(info: StoredOAuthClientInformation) {
return {
clientIdIssuedAt: "client_id_issued_at" in info ? info.client_id_issued_at : undefined,
clientSecretExpiresAt: "client_secret_expires_at" in info ? info.client_secret_expires_at : undefined,
redirectUris: "redirect_uris" in info ? info.redirect_uris : undefined,
}
}
export class McpOAuthProvider implements OAuthClientProvider {
constructor(
protected mcpName: string,
@@ -52,18 +59,21 @@ export class McpOAuthProvider implements OAuthClientProvider {
}
}
async clientInformation(): Promise<OAuthClientInformation | undefined> {
async clientInformation(): Promise<StoredOAuthClientInformation | undefined> {
const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl))
if (this.config.clientId) {
const issuer = entry?.clientInfo?.clientId === this.config.clientId ? entry.clientInfo.issuer : undefined
return {
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
...(issuer !== undefined ? { issuer } : {}),
}
}
// Check stored client info (from dynamic registration)
// Use getForUrl to validate credentials are for the current server URL
const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl))
if (entry?.clientInfo) {
if (entry.clientInfo.configPreRegistered) return undefined
// Check if client secret has expired
if (entry.clientInfo.clientSecretExpiresAt && entry.clientInfo.clientSecretExpiresAt < Date.now() / 1000) {
return undefined
@@ -71,6 +81,14 @@ export class McpOAuthProvider implements OAuthClientProvider {
return {
client_id: entry.clientInfo.clientId,
client_secret: entry.clientInfo.clientSecret,
...(entry.clientInfo.clientIdIssuedAt !== undefined
? { client_id_issued_at: entry.clientInfo.clientIdIssuedAt }
: {}),
...(entry.clientInfo.clientSecretExpiresAt !== undefined
? { client_secret_expires_at: entry.clientInfo.clientSecretExpiresAt }
: {}),
redirect_uris: entry.clientInfo.redirectUris ? [...entry.clientInfo.redirectUris] : [this.redirectUrl],
...(entry.clientInfo.issuer !== undefined ? { issuer: entry.clientInfo.issuer } : {}),
}
}
@@ -78,22 +96,36 @@ export class McpOAuthProvider implements OAuthClientProvider {
return undefined
}
async saveClientInformation(info: OAuthClientInformationFull): Promise<void> {
async saveClientInformation(info: StoredOAuthClientInformation): Promise<void> {
if (this.config.clientId && info.client_id === this.config.clientId) {
await Effect.runPromise(
this.auth.updateClientInfo(
this.mcpName,
{ clientId: info.client_id, issuer: info.issuer, configPreRegistered: true },
this.serverUrl,
),
)
return
}
const metadata = registrationMetadata(info)
await Effect.runPromise(
this.auth.updateClientInfo(
this.mcpName,
{
clientId: info.client_id,
clientSecret: info.client_secret,
clientIdIssuedAt: info.client_id_issued_at,
clientSecretExpiresAt: info.client_secret_expires_at,
clientIdIssuedAt: metadata.clientIdIssuedAt,
clientSecretExpiresAt: metadata.clientSecretExpiresAt,
redirectUris: metadata.redirectUris ? [...metadata.redirectUris] : [this.redirectUrl],
issuer: info.issuer,
},
this.serverUrl,
),
)
}
async tokens(): Promise<OAuthTokens | undefined> {
async tokens(): Promise<StoredOAuthTokens | undefined> {
// Use getForUrl to validate tokens are for the current server URL
const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl))
if (!entry?.tokens) return undefined
@@ -106,18 +138,20 @@ export class McpOAuthProvider implements OAuthClientProvider {
? Math.max(0, Math.floor(entry.tokens.expiresAt - Date.now() / 1000))
: undefined,
scope: entry.tokens.scope,
issuer: entry.tokens.issuer,
}
}
async saveTokens(tokens: OAuthTokens): Promise<void> {
async saveTokens(tokens: StoredOAuthTokens): Promise<void> {
await Effect.runPromise(
this.auth.updateTokens(
this.mcpName,
{
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: tokens.expires_in ? Date.now() / 1000 + tokens.expires_in : undefined,
expiresAt: tokens.expires_in !== undefined ? Date.now() / 1000 + tokens.expires_in : undefined,
scope: tokens.scope,
issuer: tokens.issuer,
},
this.serverUrl,
),
@@ -181,10 +215,10 @@ export class McpOAuthProvider implements OAuthClientProvider {
}
export class McpOAuthPendingProvider extends McpOAuthProvider {
private pendingClientInfo?: OAuthClientInformationFull
private pendingTokens?: OAuthTokens
private pendingClientInfo?: StoredOAuthClientInformation
private pendingTokens?: StoredOAuthTokens
override async clientInformation(): Promise<OAuthClientInformation | undefined> {
override async clientInformation(): Promise<StoredOAuthClientInformation | undefined> {
if (!this.config.clientId) return this.pendingClientInfo
return {
client_id: this.config.clientId,
@@ -192,15 +226,15 @@ export class McpOAuthPendingProvider extends McpOAuthProvider {
}
}
override async saveClientInformation(info: OAuthClientInformationFull): Promise<void> {
override async saveClientInformation(info: StoredOAuthClientInformation): Promise<void> {
this.pendingClientInfo = info
}
override async tokens(): Promise<OAuthTokens | undefined> {
override async tokens(): Promise<StoredOAuthTokens | undefined> {
return this.pendingTokens
}
override async saveTokens(tokens: OAuthTokens): Promise<void> {
override async saveTokens(tokens: StoredOAuthTokens): Promise<void> {
this.pendingTokens = tokens
}
@@ -211,6 +245,7 @@ export class McpOAuthPendingProvider extends McpOAuthProvider {
async commit(): Promise<void> {
if (!this.pendingTokens) return
const pendingMetadata = this.pendingClientInfo ? registrationMetadata(this.pendingClientInfo) : undefined
await Effect.runPromise(
this.auth.set(
this.mcpName,
@@ -218,16 +253,22 @@ export class McpOAuthPendingProvider extends McpOAuthProvider {
tokens: {
accessToken: this.pendingTokens.access_token,
refreshToken: this.pendingTokens.refresh_token,
expiresAt: this.pendingTokens.expires_in ? Date.now() / 1000 + this.pendingTokens.expires_in : undefined,
expiresAt:
this.pendingTokens.expires_in !== undefined
? Date.now() / 1000 + this.pendingTokens.expires_in
: undefined,
scope: this.pendingTokens.scope,
issuer: this.pendingTokens.issuer,
},
clientInfo:
this.pendingClientInfo && !this.config.clientId
? {
clientId: this.pendingClientInfo.client_id,
clientSecret: this.pendingClientInfo.client_secret,
clientIdIssuedAt: this.pendingClientInfo.client_id_issued_at,
clientSecretExpiresAt: this.pendingClientInfo.client_secret_expires_at,
clientIdIssuedAt: pendingMetadata?.clientIdIssuedAt,
clientSecretExpiresAt: pendingMetadata?.clientSecretExpiresAt,
redirectUris: pendingMetadata?.redirectUris ? [...pendingMetadata.redirectUris] : [this.redirectUrl],
issuer: this.pendingClientInfo.issuer,
}
: undefined,
},
+17 -6
View File
@@ -516,12 +516,20 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re
return msgs
}
const GEMINI_MODELS_WITH_SAMPLING_DEFAULTS = [
/gemini-2[.-]5(?:[.-]|$)/,
/gemini-3-(?:flash|pro)(?:[.-]|$)/,
/gemini-3[.-]1(?:[.-]|$)/,
/gemini-3[.-]5-flash(?!-lite)(?:[.-]|$)/,
]
export function temperature(model: Provider.Model) {
const id = model.id.toLowerCase()
const id = model.api.id.toLowerCase()
if (id.includes("north-mini-code")) return 1.0
if (id.includes("qwen")) return 0.55
if (id.includes("claude")) return undefined
if (id.includes("gemini")) return 1.0
if (id.includes("gemini"))
return GEMINI_MODELS_WITH_SAMPLING_DEFAULTS.some((model) => model.test(id)) ? 1.0 : undefined
if (id.includes("glm-4.6")) return 1.0
if (id.includes("glm-4.7")) return 1.0
if (id.includes("minimax-m2")) return 1.0
@@ -536,21 +544,24 @@ export function temperature(model: Provider.Model) {
}
export function topP(model: Provider.Model) {
const id = model.id.toLowerCase()
const id = model.api.id.toLowerCase()
if (id.includes("qwen")) return 1
if (["minimax-m2", "gemini", "kimi-k2.5", "kimi-k2p5", "kimi-k2-5"].some((s) => id.includes(s))) {
if (id.includes("gemini"))
return GEMINI_MODELS_WITH_SAMPLING_DEFAULTS.some((model) => model.test(id)) ? 0.95 : undefined
if (["minimax-m2", "kimi-k2.5", "kimi-k2p5", "kimi-k2-5"].some((s) => id.includes(s))) {
return 0.95
}
return undefined
}
export function topK(model: Provider.Model) {
const id = model.id.toLowerCase()
const id = model.api.id.toLowerCase()
if (id.includes("minimax-m2")) {
if (["m2.", "m25", "m21"].some((s) => id.includes(s))) return 40
return 20
}
if (id.includes("gemini")) return 64
if (id.includes("gemini"))
return GEMINI_MODELS_WITH_SAMPLING_DEFAULTS.some((model) => model.test(id)) ? 64 : undefined
return undefined
}
@@ -20,6 +20,7 @@ export const AuthStartResponse = Schema.Struct({
})
export const AuthCallbackPayload = Schema.Struct({
code: Schema.String,
iss: Schema.optional(Schema.String),
})
export const AuthRemoveResponse = Schema.Struct({
success: Schema.Literal(true),
@@ -38,7 +38,7 @@ export const mcpHandlers = HttpApiBuilder.group(InstanceHttpApi, "mcp", (handler
payload: typeof AuthCallbackPayload.Type
}) {
return yield* mcp
.finishAuth(ctx.params.name, ctx.payload.code)
.finishAuth(ctx.params.name, ctx.payload.code, ctx.payload.iss)
.pipe(
Effect.catchTag("MCP.NotFoundError", (error) =>
Effect.fail(
+1 -1
View File
@@ -388,7 +388,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
if (flags.experimentalCodeMode) return tools
for (const [key, entry] of Object.entries(yield* mcp.tools())) {
const item = McpCatalog.convertTool(entry.def, entry.client, entry.timeout)
const item = McpCatalog.convertTool(entry)
const execute = item.execute
if (!execute) continue
+2 -23
View File
@@ -1,5 +1,5 @@
import * as Tool from "./tool"
import { CallToolResultSchema, type CallToolResult } from "@modelcontextprotocol/sdk/types.js"
import { type CallToolResult } from "@modelcontextprotocol/client"
import { Cause, Effect, Schema } from "effect"
import { CodeMode, Tool as SandboxTool, toolError } from "@opencode-ai/codemode"
import { MCP } from "@/mcp"
@@ -145,28 +145,7 @@ const invokeChildTool = Effect.fn("CodeMode.invokeChildTool")(function* (input:
)
const result: CallToolResult = yield* Effect.gen(function* () {
yield* input.ctx.ask({ permission: input.entry.key, metadata: {}, patterns: ["*"], always: ["*"] })
// Deliberately mirrors McpCatalog.convertTool's transport call so the MCP service stays free of tool-loop concerns.
return yield* Effect.promise(async () => {
const raw = await input.entry.tool.client.callTool(
{ name: input.entry.tool.def.name, arguments: input.args },
CallToolResultSchema,
{
resetTimeoutOnProgress: true,
signal: input.ctx.abort,
timeout: input.entry.tool.timeout,
// The MCP SDK only sends a progress token when this hook is present, enabling timeout resets.
onprogress: () => {},
},
)
if (raw.isError)
throw new Error(
raw.content
.flatMap((item) => (item.type === "text" ? [item.text] : []))
.filter((text) => text.trim())
.join("\n\n") || "MCP tool returned an error",
)
return raw
})
return yield* Effect.promise(() => McpCatalog.callTool(input.entry.tool, input.args, input.ctx.abort))
}).pipe(
Effect.withSpan("Tool.execute", {
attributes: {
@@ -1,6 +1,5 @@
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
import { Server } from "@modelcontextprotocol/server"
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"
if (process.argv.includes("--hang")) {
const pidFile = process.env.MCP_LIFECYCLE_PID_FILE
@@ -11,7 +10,7 @@ if (process.argv.includes("--hang")) {
const server = new Server({ name: "mcp-lifecycle-stdio", version: "1.0.0" }, { capabilities: { tools: {} } })
server.setRequestHandler(ListToolsRequestSchema, () =>
server.setRequestHandler("tools/list", () =>
Promise.resolve({
tools: [
{
@@ -1,10 +1,11 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js"
import { Client, LATEST_PROTOCOL_VERSION, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"
const posts: Array<{ method: string; session: string | null }> = []
const concurrent = process.env.MCP_RECOVERY_CONCURRENT === "1"
let initializeCount = 0
let pingCount = 0
let replacementStarted!: () => void
const replacement = new Promise<void>((resolve) => (replacementStarted = resolve))
const server = Bun.serve({
port: 0,
async fetch(request) {
@@ -17,6 +18,7 @@ const server = Bun.serve({
if (message.method === "initialize") {
initializeCount++
if (initializeCount === 2) replacementStarted()
return Response.json(
{
jsonrpc: "2.0",
@@ -34,7 +36,8 @@ const server = Bun.serve({
if (message.method === "notifications/initialized") return new Response(null, { status: 202 })
pingCount++
if (pingCount === 1) return new Response("Session not found", { status: 404 })
if (concurrent && pingCount === 2) await replacement
if (pingCount <= (concurrent ? 2 : 1)) return new Response("Session not found", { status: 404 })
return Response.json({ jsonrpc: "2.0", id: message.id, result: {} })
},
})
@@ -42,7 +45,8 @@ const client = new Client({ name: "test", version: "1" })
try {
await client.connect(new StreamableHTTPClientTransport(server.url))
await client.ping()
if (concurrent) await Promise.all([client.ping(), client.ping()])
else await client.ping()
process.stdout.write(JSON.stringify(posts))
} finally {
await client.close()
+51 -15
View File
@@ -1,8 +1,6 @@
import { describe, expect, test } from "bun:test"
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
import { Client, InMemoryTransport } from "@modelcontextprotocol/client"
import { Server } from "@modelcontextprotocol/server"
import { McpCatalog } from "@/mcp/catalog"
import { Effect } from "effect"
@@ -30,7 +28,10 @@ describe("McpCatalog.convertTool", () => {
test("preserves content when structuredContent is also present", async () => {
const content = [{ type: "image" as const, mimeType: "image/png", data: "AAAA" }]
const structuredContent = { image: { mimeType: "image/png", data: "AAAA" } }
const converted = McpCatalog.convertTool(mcpTool(), clientReturning({ content, structuredContent }))
const converted = McpCatalog.convertTool({
def: mcpTool(),
client: clientReturning({ content, structuredContent }),
})
const output = await converted.execute?.({}, options)
@@ -39,7 +40,10 @@ describe("McpCatalog.convertTool", () => {
test("falls back to structuredContent only when content is absent", async () => {
const structuredContent = { results: [{ title: "one" }] }
const converted = McpCatalog.convertTool(mcpTool(), clientReturning({ content: [], structuredContent }))
const converted = McpCatalog.convertTool({
def: mcpTool(),
client: clientReturning({ content: [], structuredContent }),
})
const output = await converted.execute?.({}, options)
@@ -50,18 +54,52 @@ describe("McpCatalog.convertTool", () => {
})
})
describe("McpCatalog.callTool", () => {
test("forwards the request options", async () => {
const controller = new AbortController()
let request: unknown
let options: unknown
const client = {
callTool: async (input: unknown, config: unknown) => {
request = input
options = config
return { content: [] }
},
} as unknown as Client
await McpCatalog.callTool({ def: mcpTool(), client, timeout: 123 }, { value: true }, controller.signal)
expect(request).toEqual({ name: "screenshot", arguments: { value: true } })
expect(options).toMatchObject({ resetTimeoutOnProgress: true, signal: controller.signal, timeout: 123 })
expect(typeof (options as { onprogress?: unknown }).onprogress).toBe("function")
})
test("throws text returned by an MCP tool error", async () => {
const client = clientReturning({
isError: true,
content: [
{ type: "image", data: "AAAA", mimeType: "image/png" },
{ type: "text", text: "first" },
{ type: "text", text: "second" },
],
})
await expect(McpCatalog.callTool({ def: mcpTool(), client }, {})).rejects.toThrow("first\n\nsecond")
})
})
test("preserves output schema validation across paginated tool discovery", async () => {
const server = new Server({ name: "pagination", version: "1.0.0" }, { capabilities: { tools: {} } })
server.setRequestHandler(ListToolsRequestSchema, ({ params }) =>
server.setRequestHandler("tools/list", ({ params }) =>
Promise.resolve(
params?.cursor === "page-2"
? {
tools: [
{
name: "second",
inputSchema: { type: "object" },
inputSchema: { type: "object" as const },
outputSchema: {
type: "object",
type: "object" as const,
properties: { value: { type: "number" } },
required: ["value"],
},
@@ -72,9 +110,9 @@ test("preserves output schema validation across paginated tool discovery", async
tools: [
{
name: "first",
inputSchema: { type: "object" },
inputSchema: { type: "object" as const },
outputSchema: {
type: "object",
type: "object" as const,
properties: { value: { type: "string" } },
required: ["value"],
},
@@ -84,7 +122,7 @@ test("preserves output schema validation across paginated tool discovery", async
},
),
)
server.setRequestHandler(CallToolRequestSchema, ({ params }) =>
server.setRequestHandler("tools/call", ({ params }) =>
Promise.resolve({
content: [],
structuredContent: { value: params.name === "first" ? 42 : 1 },
@@ -98,9 +136,7 @@ test("preserves output schema validation across paginated tool discovery", async
try {
const tools = await Effect.runPromise(McpCatalog.defs(client))
expect(tools?.map((tool) => tool.name)).toEqual(["first", "second"])
await expect(client.callTool({ name: "first", arguments: {} })).rejects.toThrow(
"Structured content does not match the tool's output schema",
)
await expect(client.callTool({ name: "first", arguments: {} })).rejects.toThrow(/output schema/i)
} finally {
await Promise.all([client.close(), server.close()])
}
+21 -4
View File
@@ -1,7 +1,5 @@
import { describe, expect } from "bun:test"
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
import { Server, WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect } from "effect"
import { testEffect } from "../lib/effect"
@@ -13,7 +11,7 @@ const serve = Effect.acquireRelease(
Effect.promise(async () => {
const requests: Headers[] = []
const protocol = new Server({ name: "headers", version: "1.0.0" }, { capabilities: { tools: {} } })
protocol.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [] }))
protocol.setRequestHandler("tools/list", () => Promise.resolve({ tools: [] }))
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
enableJsonResponse: true,
@@ -38,6 +36,11 @@ const serve = Effect.acquireRelease(
(server) => Effect.promise(server.close),
)
const serveUnauthorized = Effect.acquireRelease(
Effect.sync(() => Bun.serve({ port: 0, fetch: () => new Response("Unauthorized", { status: 401 }) })),
(server) => Effect.sync(() => server.stop(true)),
)
describe("mcp.headers", () => {
it.instance("headers are passed to transports when oauth is enabled (default)", () =>
Effect.gen(function* () {
@@ -99,4 +102,18 @@ describe("mcp.headers", () => {
}
}),
)
it.instance("reports 401 as failed when oauth is explicitly disabled", () =>
Effect.gen(function* () {
const server = yield* serveUnauthorized
const mcp = yield* MCP.Service
const result = yield* mcp.add("unauthorized-no-oauth", {
type: "remote",
url: server.url.toString(),
oauth: false,
})
expect(result.status).toMatchObject({ "unauthorized-no-oauth": { status: "failed" } })
}),
)
})
+13 -18
View File
@@ -1,18 +1,12 @@
import path from "node:path"
import { pathToFileURL } from "node:url"
import { expect } from "bun:test"
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"
import {
GetPromptRequestSchema,
ListPromptsRequestSchema,
ListResourcesRequestSchema,
ListResourceTemplatesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
Server,
WebStandardStreamableHTTPServerTransport,
type ServerCapabilities,
type Tool,
} from "@modelcontextprotocol/sdk/types.js"
} from "@modelcontextprotocol/server"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Cause, Effect, Exit } from "effect"
import type { MCP as MCPNS } from "../../src/mcp/index"
@@ -66,35 +60,35 @@ function lifecycleServer(input?: { capabilities?: ServerCapabilities; instructio
})
if (capabilities.tools) {
protocol.setRequestHandler(ListToolsRequestSchema, (request) => {
protocol.setRequestHandler("tools/list", (request) => {
if (state.listToolsError) throw new Error(state.listToolsError)
const page = state.toolPages?.[request.params?.cursor ?? "initial"]
return Promise.resolve({ tools: page?.items ?? state.tools, nextCursor: page?.nextCursor })
})
}
if (capabilities.prompts) {
protocol.setRequestHandler(ListPromptsRequestSchema, (request) => {
protocol.setRequestHandler("prompts/list", (request) => {
const page = state.promptPages?.[request.params?.cursor ?? "initial"]
return Promise.resolve({ prompts: page?.items ?? state.prompts, nextCursor: page?.nextCursor })
})
protocol.setRequestHandler(GetPromptRequestSchema, async () => {
protocol.setRequestHandler("prompts/get", async () => {
if (state.requestDelay) await Bun.sleep(state.requestDelay)
return { messages: [{ role: "user", content: { type: "text", text: "prompt result" } }] }
})
}
if (capabilities.resources) {
protocol.setRequestHandler(ListResourcesRequestSchema, (request) => {
protocol.setRequestHandler("resources/list", (request) => {
const page = state.resourcePages?.[request.params?.cursor ?? "initial"]
return Promise.resolve({ resources: page?.items ?? state.resources, nextCursor: page?.nextCursor })
})
protocol.setRequestHandler(ListResourceTemplatesRequestSchema, (request) => {
protocol.setRequestHandler("resources/templates/list", (request) => {
const page = state.resourceTemplatePages?.[request.params?.cursor ?? "initial"]
return Promise.resolve({
resourceTemplates: page?.items ?? state.resourceTemplates,
nextCursor: page?.nextCursor,
})
})
protocol.setRequestHandler(ReadResourceRequestSchema, async (request) => {
protocol.setRequestHandler("resources/read", async (request) => {
if (state.requestDelay) await Bun.sleep(state.requestDelay)
return { contents: [{ uri: request.params.uri, text: "resource result" }] }
})
@@ -145,7 +139,7 @@ function hangingLifecycleServer() {
return Effect.acquireRelease(
Effect.promise(async () => {
const protocol = new Server({ name: "mcp-lifecycle-hanging", version: "1.0.0" }, { capabilities: { tools: {} } })
protocol.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [] }))
protocol.setRequestHandler("tools/list", () => Promise.resolve({ tools: [] }))
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
enableJsonResponse: true,
@@ -284,7 +278,7 @@ it.instance("follows cursors when listing tools, prompts, resources, and templat
}),
)
it.instance("accepts empty cursors and rejects repeated cursors", () =>
it.instance("accepts empty cursors and terminates on repeated cursors", () =>
Effect.gen(function* () {
const empty = yield* lifecycleServer({ capabilities: { prompts: {} } })
empty.state.promptPages = {
@@ -301,7 +295,8 @@ it.instance("accepts empty cursors and rejects repeated cursors", () =>
const result = yield* mcp.add("looping-cursor", remote(looping.url))
expect(Object.keys(yield* mcp.prompts())).toEqual(["empty-cursor:prompt-one", "empty-cursor:prompt-two"])
expect(statusName(result.status, "looping-cursor")).toBe("failed")
expect(statusName(result.status, "looping-cursor")).toBe("connected")
expect(Object.keys(yield* mcp.tools())).toEqual([])
}),
)
@@ -1,7 +1,5 @@
import { expect } from "bun:test"
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"
import { ListResourcesRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
import { Server, WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { FSUtil } from "@opencode-ai/core/fs-util"
@@ -40,13 +38,13 @@ function serveOAuthMcp(options: OAuthMcpOptions = {}) {
let requiresAuth = true
if (capabilities === "tools") {
protocol.setRequestHandler(ListToolsRequestSchema, () => {
protocol.setRequestHandler("tools/list", () => {
listToolsCalls++
return Promise.resolve({ tools: [{ name: "test_tool", inputSchema: { type: "object" } }] })
})
}
if (capabilities === "resources") {
protocol.setRequestHandler(ListResourcesRequestSchema, () =>
protocol.setRequestHandler("resources/list", () =>
Promise.resolve({ resources: [{ name: "docs", uri: "docs://readme" }] }),
)
}
@@ -1,7 +1,5 @@
import { expect } from "bun:test"
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
import { Server, WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Deferred, Effect, Layer, Option } from "effect"
import { Config } from "../../src/config/config"
@@ -41,7 +39,7 @@ const serveOAuthMcp = Effect.acquireRelease(
Effect.promise(async () => {
const requests: Array<{ pathname: string; headers: Headers }> = []
const protocol = new Server({ name: "oauth-browser", version: "1.0.0" }, { capabilities: { tools: {} } })
protocol.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [] }))
protocol.setRequestHandler("tools/list", () => Promise.resolve({ tools: [] }))
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
enableJsonResponse: true,
@@ -74,7 +74,7 @@ describe("McpOAuthCallback.ensureRunning", () => {
const response = await fetch(`${redirectUri}?code=code&state=success`)
expect(response.status).toBe(200)
expect(await callback).toBe("code")
expect(await callback).toEqual({ code: "code", iss: undefined })
expect(McpOAuthCallback.isRunning()).toBe(false)
})
@@ -1,5 +1,4 @@
import { test, expect, describe } from "bun:test"
import { determineScope } from "@modelcontextprotocol/sdk/client/auth.js"
import { McpOAuthProvider, OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "../../src/mcp/oauth-provider"
import type { McpAuth } from "../../src/mcp/auth"
@@ -60,43 +59,3 @@ describe("McpOAuthProvider.clientMetadata", () => {
expect(provider.clientMetadata.token_endpoint_auth_method).toBe("none")
})
})
describe("MCP OAuth scope selection", () => {
test("adds offline_access when the authorization server and client support refresh tokens", () => {
expect(
determineScope({
resourceMetadata: {
resource: "https://mcp.example.com/mcp",
scopes_supported: ["resource.read"],
},
authServerMetadata: {
issuer: "https://auth.example.com",
authorization_endpoint: "https://auth.example.com/authorize",
token_endpoint: "https://auth.example.com/token",
response_types_supported: ["code"],
scopes_supported: ["resource.read", "offline_access"],
},
clientMetadata: makeProvider({}).clientMetadata,
}),
).toBe("resource.read offline_access")
})
test("does not add unsupported authorization server scopes", () => {
expect(
determineScope({
resourceMetadata: {
resource: "https://mcp.example.com/mcp",
scopes_supported: ["resource.read"],
},
authServerMetadata: {
issuer: "https://auth.example.com",
authorization_endpoint: "https://auth.example.com/authorize",
token_endpoint: "https://auth.example.com/token",
response_types_supported: ["code"],
scopes_supported: ["resource.read"],
},
clientMetadata: makeProvider({}).clientMetadata,
}),
).toBe("resource.read")
})
})
@@ -24,4 +24,24 @@ describe("mcp session recovery", () => {
{ method: "ping", session: "replacement" },
])
})
test("retries a concurrent stale response after recovery completes", async () => {
const child = Bun.spawn([process.execPath, path.join(import.meta.dir, "../fixture/mcp-session-recovery.ts")], {
cwd: path.join(import.meta.dir, "../.."),
env: { ...process.env, MCP_RECOVERY_CONCURRENT: "1" },
stdout: "pipe",
stderr: "pipe",
})
const [code, stdout, stderr] = await Promise.all([
child.exited,
Bun.readableStreamToText(child.stdout),
Bun.readableStreamToText(child.stderr),
])
expect(code, stderr).toBe(0)
const posts = JSON.parse(stdout) as Array<{ method: string; session: string | null }>
expect(posts.filter((post) => post.method === "initialize").map((post) => post.session)).toEqual([null, null])
expect(posts.filter((post) => post.method === "ping" && post.session === "expired")).toHaveLength(2)
expect(posts.filter((post) => post.method === "ping" && post.session === "replacement")).toHaveLength(2)
})
})
@@ -3186,7 +3186,69 @@ describe("ProviderTransform.message - cache control on gateway", () => {
describe("ProviderTransform.temperature - Cohere North", () => {
test("defaults north-mini-code models to 1.0", () => {
expect(ProviderTransform.temperature({ id: "cohere/North-Mini-Code-1-0-latest" } as any)).toBe(1.0)
expect(
ProviderTransform.temperature({
id: "cohere/North-Mini-Code-1-0-latest",
api: { id: "North-Mini-Code-1-0-latest" },
} as any),
).toBe(1.0)
})
})
describe("ProviderTransform sampling defaults - Gemini", () => {
const model = (id: string) =>
({
id: `google/${id}`,
api: { id },
}) as any
const alias = (id: string, apiID: string) =>
({
id,
api: { id: apiID },
}) as any
test.each([
"gemini-3.5-flash-lite",
"gemini-3-5-flash-lite",
"gemini-3.6-flash",
"gemini-3-6-flash",
"gemini-4-pro",
"gemini-future",
])("omits deprecated sampling controls for %s", (id) => {
expect(ProviderTransform.temperature(model(id))).toBeUndefined()
expect(ProviderTransform.topP(model(id))).toBeUndefined()
expect(ProviderTransform.topK(model(id))).toBeUndefined()
})
test.each([
"gemini-2.5-flash",
"gemini-2.5-pro",
"gemini-2.5-flash-lite",
"gemini-2-5-flash-lite",
"gemini-3-flash-preview",
"gemini-3-pro-image",
"gemini-3.1-flash-lite",
"gemini-3.1-pro-preview",
"gemini-3-1-pro-preview",
"gemini-3.5-flash",
"gemini-3-5-flash",
])("preserves sampling defaults for %s", (id) => {
expect(ProviderTransform.temperature(model(id))).toBe(1)
expect(ProviderTransform.topP(model(id))).toBe(0.95)
expect(ProviderTransform.topK(model(id))).toBe(64)
})
test("uses the API model ID for configured aliases", () => {
const deprecated = alias("google/gemini-3.5-flash", "google/gemini-3.6-flash")
expect(ProviderTransform.temperature(deprecated)).toBeUndefined()
expect(ProviderTransform.topP(deprecated)).toBeUndefined()
expect(ProviderTransform.topK(deprecated)).toBeUndefined()
const supported = alias("my-gemini", "google/gemini-2.5-flash")
expect(ProviderTransform.temperature(supported)).toBe(1)
expect(ProviderTransform.topP(supported)).toBe(0.95)
expect(ProviderTransform.topK(supported)).toBe(64)
})
})
@@ -8,15 +8,14 @@ import { Session } from "@/session/session"
import { Tool } from "@/tool/tool"
import * as Truncate from "@/tool/truncate"
import { MessageID, SessionID } from "@/session/schema"
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"
import type { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { Server } from "@modelcontextprotocol/server"
import {
CallToolRequestSchema,
InMemoryTransport,
LATEST_PROTOCOL_VERSION,
ListToolsRequestSchema,
type CallToolResult,
type Client,
type Tool as MCPToolDef,
} from "@modelcontextprotocol/sdk/types.js"
} from "@modelcontextprotocol/client"
import { Cause, Effect, Exit, Layer } from "effect"
const PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
@@ -100,7 +99,7 @@ const TOOL_DEFS: MCPToolDef[] = [
},
] as MCPToolDef[]
function handleCall(name: string, args: Record<string, unknown>) {
function handleCall(name: string, args: Record<string, unknown>): CallToolResult {
switch (name) {
case "get_text":
return { content: [{ type: "text", text: `hello ${args.name}` }] }
@@ -122,8 +121,8 @@ let description: string
async function buildTool() {
const server = new Server({ name: SERVER, version: "1.0.0" }, { capabilities: { tools: {} } })
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_DEFS }))
server.setRequestHandler(CallToolRequestSchema, async (req) =>
server.setRequestHandler("tools/list", async () => ({ tools: TOOL_DEFS }))
server.setRequestHandler("tools/call", async (req) =>
handleCall(req.params.name, (req.params.arguments ?? {}) as Record<string, unknown>),
)
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import { CODE_MODE_TOOL, CodeModeTool, Parameters, describeCatalog } from "@/tool/code-mode"
import type { Tool as MCPToolDef } from "@modelcontextprotocol/sdk/types.js"
import type { Tool as MCPToolDef } from "@modelcontextprotocol/client"
import type { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Agent } from "@/agent/agent"
import { MCP } from "@/mcp"
+1 -1
View File
@@ -20,7 +20,7 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { MCP } from "@/mcp"
import type { Tool as MCPToolDef } from "@modelcontextprotocol/sdk/types.js"
import type { Tool as MCPToolDef } from "@modelcontextprotocol/client"
const configLayer = TestConfig.layer({
directories: () => InstanceState.directory.pipe(Effect.map((dir) => [path.join(dir, ".opencode")])),
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.18.7",
"version": "1.18.8",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.18.7",
"version": "1.18.8",
"type": "module",
"license": "MIT",
"scripts": {
+2
View File
@@ -2328,6 +2328,7 @@ export class Auth2 extends HeyApiClient {
directory?: string
workspace?: string
code?: string
iss?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -2340,6 +2341,7 @@ export class Auth2 extends HeyApiClient {
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
{ in: "body", key: "code" },
{ in: "body", key: "iss" },
],
},
],
+1
View File
@@ -8563,6 +8563,7 @@ export type McpAuthStartResponse = McpAuthStartResponses[keyof McpAuthStartRespo
export type McpAuthCallbackData = {
body?: {
code: string
iss?: string
}
path: {
name: string
+3
View File
@@ -3360,6 +3360,9 @@
"properties": {
"code": {
"type": "string"
},
"iss": {
"type": "string"
}
},
"required": ["code"],
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/server",
"version": "1.18.7",
"version": "1.18.8",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/session-ui",
"version": "1.18.7",
"version": "1.18.8",
"private": true,
"type": "module",
"license": "MIT",
@@ -32,7 +32,6 @@ export function CommentCardV2(props: {
return (
<TooltipV2
placement="top"
openDelay={1000}
value={props.title ?? props.comment}
disabled={!props.tooltip || !truncated()}
class={props.wide ? "w-full" : undefined}
@@ -385,12 +385,7 @@ export function PromptInputV2Attachments(props: {
<For each={props.comments ?? []}>
{(comment) => (
<div class="relative group shrink-0">
<TooltipV2
value={comment.comment}
placement="top"
openDelay={800}
contentClass="max-w-[300px] break-words"
>
<TooltipV2 value={comment.comment} placement="top" contentClass="max-w-[300px] break-words">
<CommentCardV2
comment={comment.comment ?? ""}
path={comment.path}
@@ -214,7 +214,6 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
</Show>
<div class="flex items-center">
<TooltipV2
openDelay={2000}
inactive={!prev()}
value={
<>
@@ -234,7 +233,6 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
/>
</TooltipV2>
<TooltipV2
openDelay={2000}
inactive={!next()}
value={
<>
@@ -268,12 +266,12 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
class="session-review-v2-segmented-control session-review-v2-segmented-control--icon"
aria-label={i18n.t("ui.sessionReviewV2.expandMode")}
>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.showAllLines")}>
<TooltipV2 value={i18n.t("ui.sessionReviewV2.showAllLines")}>
<SegmentedControlItemV2 value="expand" aria-label={i18n.t("ui.sessionReviewV2.showAllLines")}>
<Icon name="expand" />
</SegmentedControlItemV2>
</TooltipV2>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
<TooltipV2 value={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
<SegmentedControlItemV2 value="collapse" aria-label={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
<Icon name="collapse" />
</SegmentedControlItemV2>
@@ -289,12 +287,12 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
class="session-review-v2-segmented-control session-review-v2-segmented-control--icon"
aria-label={i18n.t("ui.sessionReviewV2.diffView")}
>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
<TooltipV2 value={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
<SegmentedControlItemV2 value="unified" aria-label={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
<Icon name="unified" />
</SegmentedControlItemV2>
</TooltipV2>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.splitDiff")}>
<TooltipV2 value={i18n.t("ui.sessionReviewV2.splitDiff")}>
<SegmentedControlItemV2 value="split" aria-label={i18n.t("ui.sessionReviewV2.splitDiff")}>
<Icon name="split" />
</SegmentedControlItemV2>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.18.7",
"version": "1.18.8",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-app",
"version": "1.18.7",
"version": "1.18.8",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-core",
"version": "1.18.7",
"version": "1.18.8",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-server",
"version": "1.18.7",
"version": "1.18.8",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/tui",
"version": "1.18.7",
"version": "1.18.8",
"private": true,
"type": "module",
"license": "MIT",
+5 -1
View File
@@ -104,7 +104,6 @@ const appGlobalBindingCommands = [
] as const
const appBindingCommands = [
"command.palette.show",
"model.list",
"model.cycle_recent",
"model.cycle_recent_reverse",
@@ -963,6 +962,11 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
commands: appCommands(),
}))
useBindings(() => ({
enabled: () => dialog.stack.length === 0,
bindings: tuiConfig.keybinds.get(COMMAND_PALETTE_COMMAND),
}))
useBindings(() => ({
mode: OPENCODE_BASE_MODE,
bindings: tuiConfig.keybinds.gather("app", appBindingCommands),
+34 -4
View File
@@ -5,7 +5,13 @@ import { testRender, useRenderer } from "@opentui/solid"
import { expect, test } from "bun:test"
import { onCleanup } from "solid-js"
import { TuiKeybind } from "../src/config/keybind"
import { getOpencodeModeStack, OPENCODE_BASE_MODE, OpencodeKeymapProvider, registerOpencodeKeymap } from "../src/keymap"
import {
COMMAND_PALETTE_COMMAND,
getOpencodeModeStack,
OPENCODE_BASE_MODE,
OpencodeKeymapProvider,
registerOpencodeKeymap,
} from "../src/keymap"
function createResolvedKeymapConfig(input: TuiKeybind.KeybindOverrides = {}) {
const keybinds = TuiKeybind.parse(input)
@@ -73,12 +79,14 @@ test("mode-less bindings stay active when opencode mode changes", async () => {
const offKeymap = registerOpencodeKeymap(keymap, renderer, config)
const offGlobal = keymap.registerLayer({
commands: [
{ name: COMMAND_PALETTE_COMMAND, run() {} },
{ name: "session.list", run() {} },
{ name: "session.new", run() {} },
{ name: "session.page.up", run() {} },
{ name: "session.first", run() {} },
],
bindings: config.keybinds.gather("test.global", [
COMMAND_PALETTE_COMMAND,
"session.list",
"session.new",
"session.page.up",
@@ -95,7 +103,14 @@ test("mode-less bindings stay active when opencode mode changes", async () => {
Array.from(
keymap.getCommandBindings({
visibility: "active",
commands: ["session.list", "session.new", "session.page.up", "session.first", "model.list"],
commands: [
COMMAND_PALETTE_COMMAND,
"session.list",
"session.new",
"session.page.up",
"session.first",
"model.list",
],
}),
([command, bindings]) => [command, bindings.length],
),
@@ -125,9 +140,24 @@ test("mode-less bindings stay active when opencode mode changes", async () => {
const app = await testRender(() => <Harness />)
try {
expect(counts).toEqual({
base: { "session.list": 1, "session.new": 1, "session.page.up": 2, "session.first": 2, "model.list": 1 },
question: { "session.list": 1, "session.new": 1, "session.page.up": 2, "session.first": 2, "model.list": 0 },
base: {
[COMMAND_PALETTE_COMMAND]: 1,
"session.list": 1,
"session.new": 1,
"session.page.up": 2,
"session.first": 2,
"model.list": 1,
},
question: {
[COMMAND_PALETTE_COMMAND]: 1,
"session.list": 1,
"session.new": 1,
"session.page.up": 2,
"session.first": 2,
"model.list": 0,
},
autocomplete: {
[COMMAND_PALETTE_COMMAND]: 1,
"session.list": 1,
"session.new": 1,
"session.page.up": 2,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.18.7",
"version": "1.18.8",
"type": "module",
"license": "MIT",
"repository": {
@@ -0,0 +1,35 @@
const OPEN_DELAY = 1_000
// Kobalte warms every tooltip globally. Keep intent previews isolated so opening
// a model picker never inherits warm state from an unrelated tooltip.
let warm = false
let reset: ReturnType<typeof setTimeout> | undefined
export function openTooltipIntent(open: () => void) {
clearTimeout(reset)
reset = undefined
if (warm) {
open()
return
}
const timer = setTimeout(() => {
warm = true
open()
}, OPEN_DELAY)
return () => clearTimeout(timer)
}
export function closeTooltipIntent() {
clearTimeout(reset)
// Adjacent triggers enter before the next task; leaving the group does not.
reset = setTimeout(() => {
warm = false
reset = undefined
})
}
export function resetTooltipIntent() {
clearTimeout(reset)
reset = undefined
warm = false
}
+36 -6
View File
@@ -2,19 +2,22 @@ import { Tooltip as KobalteTooltip } from "@kobalte/core/tooltip"
import { createEffect, Match, onCleanup, splitProps, Switch, type JSX } from "solid-js"
import type { ComponentProps } from "solid-js"
import { createStore } from "solid-js/store"
import { closeTooltipIntent, openTooltipIntent, resetTooltipIntent } from "./tooltip-intent"
import "./tooltip-v2.css"
export interface TooltipV2Props extends ComponentProps<typeof KobalteTooltip> {
export interface TooltipV2Props extends Omit<ComponentProps<typeof KobalteTooltip>, "openDelay"> {
value: JSX.Element
class?: string
contentClass?: string
contentStyle?: JSX.CSSProperties
inactive?: boolean
delay?: "standard" | "intent"
forceOpen?: boolean
}
export function TooltipV2(props: TooltipV2Props) {
let ref: HTMLDivElement | undefined
let cancelIntent: (() => void) | undefined
const [state, setState] = createStore({
open: false,
block: false,
@@ -26,19 +29,37 @@ export function TooltipV2(props: TooltipV2Props) {
"contentClass",
"contentStyle",
"inactive",
"delay",
"forceOpen",
"ignoreSafeArea",
"value",
])
const close = () => setState("open", false)
const inside = () => {
const active = document.activeElement
if (!ref || !active) return false
return ref.contains(active)
}
const close = () => {
cancelIntent?.()
cancelIntent = undefined
if (local.delay === "intent") closeTooltipIntent()
setState("open", false)
}
const show = () => {
if (local.delay !== "intent" || inside()) {
setState("open", true)
return
}
if (cancelIntent) return
cancelIntent = openTooltipIntent(() => {
cancelIntent = undefined
setState("open", true)
})
}
const drop = (expand = state.expand) => {
if (expand) return
if (ref?.matches(":hover")) return
@@ -80,6 +101,11 @@ export function TooltipV2(props: TooltipV2Props) {
onCleanup(() => obs.disconnect())
})
onCleanup(() => {
cancelIntent?.()
if (local.delay === "intent") resetTooltipIntent()
})
let justClickedTrigger = false
return (
@@ -88,8 +114,8 @@ export function TooltipV2(props: TooltipV2Props) {
<Match when={true}>
<KobalteTooltip
gutter={4}
openDelay={400}
skipDelayDuration={300}
openDelay={local.delay === "intent" ? 0 : 400}
skipDelayDuration={local.delay === "intent" ? 0 : 300}
{...others}
closeDelay={0}
ignoreSafeArea={local.ignoreSafeArea ?? true}
@@ -101,7 +127,11 @@ export function TooltipV2(props: TooltipV2Props) {
justClickedTrigger = false
return
}
setState("open", open)
if (open) {
show()
return
}
close()
}}
>
<KobalteTooltip.Trigger
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@opencode-ai/web",
"type": "module",
"license": "MIT",
"version": "1.18.7",
"version": "1.18.8",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
@@ -0,0 +1,214 @@
diff --git a/dist/index.cjs b/dist/index.cjs
index 1c43bac25a1037416fdf2ddfb4534ba0897a2e69..7e2108326f368ccbc48897195d57e370a4553855 100644
--- a/dist/index.cjs
+++ b/dist/index.cjs
@@ -3154,6 +3154,7 @@ var Client = class extends require_src.Protocol {
*/
async _connectPlainLegacy(transport, options) {
await super.connect(transport);
+ transport.onsessionexpired = () => this._legacyHandshake(transport, options);
if (transport.sessionId !== void 0) {
const negotiatedProtocolVersion = this._negotiatedProtocolVersion;
if (negotiatedProtocolVersion !== void 0) transport.setProtocolVersion?.(negotiatedProtocolVersion);
@@ -3170,6 +3171,7 @@ var Client = class extends require_src.Protocol {
* the handshake; its completion sets the negotiated (legacy) version.
*/
async _legacyHandshake(transport, options) {
+ transport.onsessionexpired = () => this._legacyHandshake(transport, options);
const legacyVersions = require_src.legacyProtocolVersions(this._supportedProtocolVersions);
try {
const offeredVersion = legacyVersions[0];
@@ -3208,6 +3210,7 @@ var Client = class extends require_src.Protocol {
await super.connect(transport);
const negotiatedProtocolVersion = this._negotiatedProtocolVersion;
if (negotiatedProtocolVersion !== void 0 && transport.setProtocolVersion) transport.setProtocolVersion(negotiatedProtocolVersion);
+ if (negotiatedProtocolVersion !== void 0 && !require_src.isModernProtocolVersion(negotiatedProtocolVersion)) transport.onsessionexpired = () => this._legacyHandshake(transport, options);
return;
}
this._resetConnectionState();
@@ -5211,10 +5214,32 @@ var StreamableHTTPClientTransport = class {
}
}
async send(message, options) {
- return this._send(message, options, false);
+ return this._send(message, options, false, 0, false);
+ }
+ async _recoverSession(expiredSessionId) {
+ if (this._sessionRecovery) return this._sessionRecovery;
+ if (!this.onsessionexpired) return false;
+ if (this._sessionId !== expiredSessionId) return true;
+ this._sessionId = void 0;
+ this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired()).then(() => true);
+ try {
+ return await this._sessionRecovery;
+ } catch (error) {
+ this._sessionId = void 0;
+ await this.close();
+ throw error;
+ } finally {
+ this._sessionRecovery = void 0;
+ }
}
- async _send(message, options, isAuthRetry, stepUpRetries = 0) {
+ async _send(message, options, isAuthRetry, stepUpRetries = 0, isSessionRetry = false) {
try {
+ const isHandshake = Array.isArray(message) ? message.some((m) => require_src.isInitializeRequest(m)) : require_src.isInitializeRequest(message);
+ const isInitialized = Array.isArray(message) ? message.some((m) => require_src.isInitializedNotification(m)) : require_src.isInitializedNotification(message);
+ if (this._sessionRecovery && !isHandshake && !isInitialized) {
+ await this._sessionRecovery;
+ options?.requestSignal?.throwIfAborted();
+ }
const { resumptionToken, onresumptiontoken } = options || {};
if (resumptionToken) {
this._startOrAuthSse({
@@ -5226,8 +5251,8 @@ var StreamableHTTPClientTransport = class {
}
const headers = await this._commonHeaders();
this._applyBodyDerivedHeaders(headers, message);
- const isHandshake = Array.isArray(message) ? message.some((m) => require_src.isInitializeRequest(m)) : require_src.isInitializeRequest(message);
if (isHandshake) headers.delete("mcp-session-id");
+ const requestSessionId = headers.get("mcp-session-id") || void 0;
if (options?.headers !== void 0) for (const [name, value] of Object.entries(options.headers)) {
if (RESERVED_REQUEST_HEADER_NAMES.has(name.toLowerCase())) continue;
headers.set(name, value);
@@ -5249,8 +5274,14 @@ var StreamableHTTPClientTransport = class {
signal
};
const response = await (this._fetch ?? fetch)(this._url, init);
- if (isHandshake && response.ok) this._sessionId = response.headers.get("mcp-session-id") || void 0;
+ if (isHandshake && response.ok && (requestSessionId === void 0 || this._sessionId === requestSessionId)) this._sessionId = response.headers.get("mcp-session-id") || void 0;
if (!response.ok) {
+ if (response.status === 404 && requestSessionId && !isSessionRetry && !isInitialized) {
+ if (await this._recoverSession(requestSessionId)) {
+ options?.requestSignal?.throwIfAborted();
+ return this._send(message, options, isAuthRetry, stepUpRetries, true);
+ }
+ }
if (response.status === 401 && this._authProvider) {
if (response.headers.has("www-authenticate")) {
const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response);
@@ -5264,7 +5295,7 @@ var StreamableHTTPClientTransport = class {
fetchFn: this._fetchWithInit
});
await response.text?.().catch(() => {});
- return this._send(message, options, true, stepUpRetries);
+ return this._send(message, options, true, stepUpRetries, isSessionRetry);
}
await response.text?.().catch(() => {});
if (isAuthRetry) throw new require_src.SdkHttpError(require_src.SdkErrorCode.ClientHttpAuthentication, "Server returned 401 after re-authentication", {
@@ -5284,7 +5315,7 @@ var StreamableHTTPClientTransport = class {
statusText: response.statusText,
text
}, stepUpRetries) !== "AUTHORIZED") throw new UnauthorizedError();
- return this._send(message, options, isAuthRetry, stepUpRetries + 1);
+ return this._send(message, options, isAuthRetry, stepUpRetries + 1, isSessionRetry);
}
}
if (response.status === 400 && typeof text === "string" && this._isModernEnvelopedRequest(message)) try {
diff --git a/dist/index.mjs b/dist/index.mjs
index 77e2389913cb5c5c2b047f95d990ab2892bef923..4b5e4ff2869189d600ca644488a7749668c39747 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -3151,6 +3151,7 @@ var Client = class extends Protocol {
*/
async _connectPlainLegacy(transport, options) {
await super.connect(transport);
+ transport.onsessionexpired = () => this._legacyHandshake(transport, options);
if (transport.sessionId !== void 0) {
const negotiatedProtocolVersion = this._negotiatedProtocolVersion;
if (negotiatedProtocolVersion !== void 0) transport.setProtocolVersion?.(negotiatedProtocolVersion);
@@ -3167,6 +3168,7 @@ var Client = class extends Protocol {
* the handshake; its completion sets the negotiated (legacy) version.
*/
async _legacyHandshake(transport, options) {
+ transport.onsessionexpired = () => this._legacyHandshake(transport, options);
const legacyVersions = legacyProtocolVersions(this._supportedProtocolVersions);
try {
const offeredVersion = legacyVersions[0];
@@ -3205,6 +3207,7 @@ var Client = class extends Protocol {
await super.connect(transport);
const negotiatedProtocolVersion = this._negotiatedProtocolVersion;
if (negotiatedProtocolVersion !== void 0 && transport.setProtocolVersion) transport.setProtocolVersion(negotiatedProtocolVersion);
+ if (negotiatedProtocolVersion !== void 0 && !isModernProtocolVersion(negotiatedProtocolVersion)) transport.onsessionexpired = () => this._legacyHandshake(transport, options);
return;
}
this._resetConnectionState();
@@ -5208,10 +5211,32 @@ var StreamableHTTPClientTransport = class {
}
}
async send(message, options) {
- return this._send(message, options, false);
+ return this._send(message, options, false, 0, false);
+ }
+ async _recoverSession(expiredSessionId) {
+ if (this._sessionRecovery) return this._sessionRecovery;
+ if (!this.onsessionexpired) return false;
+ if (this._sessionId !== expiredSessionId) return true;
+ this._sessionId = void 0;
+ this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired()).then(() => true);
+ try {
+ return await this._sessionRecovery;
+ } catch (error) {
+ this._sessionId = void 0;
+ await this.close();
+ throw error;
+ } finally {
+ this._sessionRecovery = void 0;
+ }
}
- async _send(message, options, isAuthRetry, stepUpRetries = 0) {
+ async _send(message, options, isAuthRetry, stepUpRetries = 0, isSessionRetry = false) {
try {
+ const isHandshake = Array.isArray(message) ? message.some((m) => isInitializeRequest(m)) : isInitializeRequest(message);
+ const isInitialized = Array.isArray(message) ? message.some((m) => isInitializedNotification(m)) : isInitializedNotification(message);
+ if (this._sessionRecovery && !isHandshake && !isInitialized) {
+ await this._sessionRecovery;
+ options?.requestSignal?.throwIfAborted();
+ }
const { resumptionToken, onresumptiontoken } = options || {};
if (resumptionToken) {
this._startOrAuthSse({
@@ -5223,8 +5248,8 @@ var StreamableHTTPClientTransport = class {
}
const headers = await this._commonHeaders();
this._applyBodyDerivedHeaders(headers, message);
- const isHandshake = Array.isArray(message) ? message.some((m) => isInitializeRequest(m)) : isInitializeRequest(message);
if (isHandshake) headers.delete("mcp-session-id");
+ const requestSessionId = headers.get("mcp-session-id") || void 0;
if (options?.headers !== void 0) for (const [name, value] of Object.entries(options.headers)) {
if (RESERVED_REQUEST_HEADER_NAMES.has(name.toLowerCase())) continue;
headers.set(name, value);
@@ -5246,8 +5271,14 @@ var StreamableHTTPClientTransport = class {
signal
};
const response = await (this._fetch ?? fetch)(this._url, init);
- if (isHandshake && response.ok) this._sessionId = response.headers.get("mcp-session-id") || void 0;
+ if (isHandshake && response.ok && (requestSessionId === void 0 || this._sessionId === requestSessionId)) this._sessionId = response.headers.get("mcp-session-id") || void 0;
if (!response.ok) {
+ if (response.status === 404 && requestSessionId && !isSessionRetry && !isInitialized) {
+ if (await this._recoverSession(requestSessionId)) {
+ options?.requestSignal?.throwIfAborted();
+ return this._send(message, options, isAuthRetry, stepUpRetries, true);
+ }
+ }
if (response.status === 401 && this._authProvider) {
if (response.headers.has("www-authenticate")) {
const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response);
@@ -5261,7 +5292,7 @@ var StreamableHTTPClientTransport = class {
fetchFn: this._fetchWithInit
});
await response.text?.().catch(() => {});
- return this._send(message, options, true, stepUpRetries);
+ return this._send(message, options, true, stepUpRetries, isSessionRetry);
}
await response.text?.().catch(() => {});
if (isAuthRetry) throw new SdkHttpError(SdkErrorCode.ClientHttpAuthentication, "Server returned 401 after re-authentication", {
@@ -5281,7 +5312,7 @@ var StreamableHTTPClientTransport = class {
statusText: response.statusText,
text
}, stepUpRetries) !== "AUTHORIZED") throw new UnauthorizedError();
- return this._send(message, options, isAuthRetry, stepUpRetries + 1);
+ return this._send(message, options, isAuthRetry, stepUpRetries + 1, isSessionRetry);
}
}
if (response.status === 400 && typeof text === "string" && this._isModernEnvelopedRequest(message)) try {
@@ -1,629 +0,0 @@
diff --git a/dist/cjs/client/index.d.ts b/dist/cjs/client/index.d.ts
index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644
--- a/dist/cjs/client/index.d.ts
+++ b/dist/cjs/client/index.d.ts
@@ -428,6 +428,8 @@ export declare class Client<RequestT extends Request = Request, NotificationT ex
*
* For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
*/
+ callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
+ callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{
[x: string]: unknown;
content: ({
diff --git a/dist/esm/client/index.d.ts b/dist/esm/client/index.d.ts
index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644
--- a/dist/esm/client/index.d.ts
+++ b/dist/esm/client/index.d.ts
@@ -428,6 +428,8 @@ export declare class Client<RequestT extends Request = Request, NotificationT ex
*
* For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
*/
+ callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
+ callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{
[x: string]: unknown;
content: ({
diff --git a/dist/cjs/client/index.js b/dist/cjs/client/index.js
index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c101584c84 100644
--- a/dist/cjs/client/index.js
+++ b/dist/cjs/client/index.js
@@ -288,41 +288,16 @@ class Client extends protocol_js_1.Protocol {
}
async connect(transport, options) {
await super.connect(transport);
+ transport.onsessionexpired = async () => {
+ await this._initialize(transport);
+ };
// When transport sessionId is already set this means we are trying to reconnect.
// In this case we don't need to initialize again.
if (transport.sessionId !== undefined) {
return;
}
try {
- const result = await this.request({
- method: 'initialize',
- params: {
- protocolVersion: types_js_1.LATEST_PROTOCOL_VERSION,
- capabilities: this._capabilities,
- clientInfo: this._clientInfo
- }
- }, types_js_1.InitializeResultSchema, options);
- if (result === undefined) {
- throw new Error(`Server sent invalid initialize result: ${result}`);
- }
- if (!types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
- throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
- }
- this._serverCapabilities = result.capabilities;
- this._serverVersion = result.serverInfo;
- // HTTP transports must set the protocol version in each header after initialization.
- if (transport.setProtocolVersion) {
- transport.setProtocolVersion(result.protocolVersion);
- }
- this._instructions = result.instructions;
- await this.notification({
- method: 'notifications/initialized'
- });
- // Set up list changed handlers now that we know server capabilities
- if (this._pendingListChangedConfig) {
- this._setupListChangedHandlers(this._pendingListChangedConfig);
- this._pendingListChangedConfig = undefined;
- }
+ await this._initialize(transport, options);
}
catch (error) {
// Disconnect if initialization fails.
@@ -330,6 +305,37 @@ class Client extends protocol_js_1.Protocol {
throw error;
}
}
+ async _initialize(transport, options) {
+ const result = await this.request({
+ method: 'initialize',
+ params: {
+ protocolVersion: types_js_1.LATEST_PROTOCOL_VERSION,
+ capabilities: this._capabilities,
+ clientInfo: this._clientInfo
+ }
+ }, types_js_1.InitializeResultSchema, options);
+ if (result === undefined) {
+ throw new Error(`Server sent invalid initialize result: ${result}`);
+ }
+ if (!types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
+ throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
+ }
+ this._serverCapabilities = result.capabilities;
+ this._serverVersion = result.serverInfo;
+ // HTTP transports must set the protocol version in each header after initialization.
+ if (transport.setProtocolVersion) {
+ transport.setProtocolVersion(result.protocolVersion);
+ }
+ this._instructions = result.instructions;
+ await this.notification({
+ method: 'notifications/initialized'
+ });
+ // Set up list changed handlers now that we know server capabilities
+ if (this._pendingListChangedConfig) {
+ this._setupListChangedHandlers(this._pendingListChangedConfig);
+ this._pendingListChangedConfig = undefined;
+ }
+ }
/**
* After initialization has completed, this will be populated with the server's reported capabilities.
*/
@@ -541,9 +547,11 @@ class Client extends protocol_js_1.Protocol {
* Called after listTools() to pre-compile validators for better performance.
*/
- cacheToolMetadata(tools) {
- this._cachedToolOutputValidators.clear();
- this._cachedKnownTaskTools.clear();
- this._cachedRequiredTaskTools.clear();
+ cacheToolMetadata(tools, reset = true) {
+ if (reset) {
+ this._cachedToolOutputValidators.clear();
+ this._cachedKnownTaskTools.clear();
+ this._cachedRequiredTaskTools.clear();
+ }
for (const tool of tools) {
// If the tool has an outputSchema, create and cache the validator
if (tool.outputSchema) {
@@ -569,7 +577,7 @@ class Client extends protocol_js_1.Protocol {
async listTools(params, options) {
const result = await this.request({ method: 'tools/list', params }, types_js_1.ListToolsResultSchema, options);
// Cache the tools and their output schemas for future validation
- this.cacheToolMetadata(result.tools);
+ this.cacheToolMetadata(result.tools, params?.cursor === undefined);
return result;
}
/**
diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp.js
index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eedff5443c9 100644
--- a/dist/cjs/client/streamableHttp.js
+++ b/dist/cjs/client/streamableHttp.js
@@ -290,7 +290,38 @@ class StreamableHTTPClientTransport {
this.onclose?.();
}
async send(message, options) {
+ return this._send(message, options, false);
+ }
+ async _recoverSession(expiredSessionId) {
+ if (this._sessionRecovery) {
+ await this._sessionRecovery;
+ return true;
+ }
+ if (this._sessionId !== expiredSessionId)
+ return true;
+ this._sessionId = undefined;
+ this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired?.());
try {
+ await this._sessionRecovery;
+ }
+ catch (error) {
+ this._sessionId = undefined;
+ await this.close();
+ throw error;
+ }
+ finally {
+ this._sessionRecovery = undefined;
+ }
+ return true;
+ }
+ async _send(message, options, isSessionRetry) {
+ try {
+ if (this._sessionRecovery && !(0, types_js_1.isInitializeRequest)(message) && !(0, types_js_1.isInitializedNotification)(message)) {
+ await this._sessionRecovery;
+ if (options?.isRequestActive?.() === false) {
+ throw new Error('Request is no longer active');
+ }
+ }
const { resumptionToken, onresumptiontoken } = options || {};
if (resumptionToken) {
// If we have at last event ID, we need to reconnect the SSE stream
@@ -298,6 +329,7 @@ class StreamableHTTPClientTransport {
return;
}
const headers = await this._commonHeaders();
+ const requestSessionId = headers.get('mcp-session-id') ?? undefined;
headers.set('content-type', 'application/json');
headers.set('accept', 'application/json, text/event-stream');
const init = {
@@ -310,11 +342,20 @@ class StreamableHTTPClientTransport {
const response = await (this._fetch ?? fetch)(this._url, init);
// Handle session ID received during initialization
const sessionId = response.headers.get('mcp-session-id');
- if (sessionId) {
+ if (sessionId && (requestSessionId === undefined || this._sessionId === requestSessionId)) {
this._sessionId = sessionId;
}
if (!response.ok) {
const text = await response.text().catch(() => null);
+ if (response.status === 404 && requestSessionId && !isSessionRetry && !(0, types_js_1.isInitializedNotification)(message)) {
+ const recovered = await this._recoverSession(requestSessionId);
+ if (options?.isRequestActive?.() === false) {
+ throw new Error('Request is no longer active');
+ }
+ if (recovered) {
+ return this._send(message, options, true);
+ }
+ }
if (response.status === 401 && this._authProvider) {
// Prevent infinite recursion when server returns 401 after successful auth
if (this._hasCompletedAuthFlow) {
@@ -335,7 +376,7 @@ class StreamableHTTPClientTransport {
// Mark that we completed auth flow
this._hasCompletedAuthFlow = true;
// Purposely _not_ awaited, so we don't call onerror twice
- return this.send(message);
+ return this._send(message, options, isSessionRetry);
}
if (response.status === 403 && this._authProvider) {
const { resourceMetadataUrl, scope, error } = (0, auth_js_1.extractWWWAuthenticateParams)(response);
@@ -362,7 +403,7 @@ class StreamableHTTPClientTransport {
if (result !== 'AUTHORIZED') {
throw new auth_js_1.UnauthorizedError();
}
- return this.send(message);
+ return this._send(message, options, isSessionRetry);
}
}
throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
diff --git a/dist/cjs/shared/protocol.js b/dist/cjs/shared/protocol.js
index 3617e787f0ba70447c99501aee7aa67584d89758..4a96d6a0328fa348b96f3869ab7e0bb77538182b 100644
--- a/dist/cjs/shared/protocol.js
+++ b/dist/cjs/shared/protocol.js
@@ -744,7 +744,12 @@ class Protocol {
}
else {
// No related task - send through transport normally
- this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => {
+ this._transport.send(jsonrpcRequest, {
+ relatedRequestId,
+ resumptionToken,
+ onresumptiontoken,
+ isRequestActive: () => this._responseHandlers.has(messageId)
+ }).catch(error => {
this._cleanupTimeout(messageId);
reject(error);
});
diff --git a/dist/cjs/client/auth.d.ts b/dist/cjs/client/auth.d.ts
index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433beea1b32f04 100644
--- a/dist/cjs/client/auth.d.ts
+++ b/dist/cjs/client/auth.d.ts
@@ -205,6 +205,15 @@ export declare function parseErrorResponse(input: Response | string): Promise<OA
* @returns A Promise that resolves to an OAuthError instance
*/
export declare function parseErrorResponse(input: Response | string): Promise<OAuthError>;
+/**
+ * Selects scopes per the MCP spec and augments them for refresh token support.
+ */
+export declare function determineScope(options: {
+ requestedScope?: string;
+ resourceMetadata?: OAuthProtectedResourceMetadata;
+ authServerMetadata?: AuthorizationServerMetadata;
+ clientMetadata: OAuthClientMetadata;
+}): string | undefined;
/**
* Orchestrates the full auth flow with a server.
*
diff --git a/dist/cjs/client/auth.js b/dist/cjs/client/auth.js
index c2e4fa91d26f5336889f6afa416147db75fc4872..178d7cfd96412d53bc14bbc13a8f76c11f727ee7 100644
--- a/dist/cjs/client/auth.js
+++ b/dist/cjs/client/auth.js
@@ -7,6 +7,7 @@ exports.UnauthorizedError = void 0;
exports.selectClientAuthMethod = selectClientAuthMethod;
exports.parseErrorResponse = parseErrorResponse;
exports.auth = auth;
+exports.determineScope = determineScope;
exports.isHttpsUrl = isHttpsUrl;
exports.selectResourceURL = selectResourceURL;
exports.extractWWWAuthenticateParams = extractWWWAuthenticateParams;
@@ -186,6 +187,19 @@ async function auth(provider, options) {
throw error;
}
}
+/**
+ * Selects scopes per the MCP spec and augments them for refresh token support.
+ */
+function determineScope({ requestedScope, resourceMetadata, authServerMetadata, clientMetadata }) {
+ let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(' ') || clientMetadata.scope;
+ if (effectiveScope &&
+ authServerMetadata?.scopes_supported?.includes('offline_access') &&
+ !effectiveScope.split(' ').includes('offline_access') &&
+ clientMetadata.grant_types?.includes('refresh_token')) {
+ effectiveScope = `${effectiveScope} offline_access`;
+ }
+ return effectiveScope;
+}
async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
// Check if the provider has cached discovery state to skip discovery
const cachedState = await provider.discoveryState?.();
@@ -241,12 +255,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
});
}
const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
- // Apply scope selection strategy (SEP-835):
- // 1. WWW-Authenticate scope (passed via `scope` param)
- // 2. PRM scopes_supported
- // 3. Client metadata scope (user-configured fallback)
- // The resolved scope is used consistently for both DCR and the authorization request.
- const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope;
+ const resolvedScope = determineScope({
+ requestedScope: scope,
+ resourceMetadata,
+ authServerMetadata: metadata,
+ clientMetadata: provider.clientMetadata
+ });
// Handle client registration if needed
let clientInformation = await Promise.resolve(provider.clientInformation());
if (!clientInformation) {
@@ -741,7 +755,7 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo
if (scope) {
authorizationUrl.searchParams.set('scope', scope);
}
- if (scope?.includes('offline_access')) {
+ if (scope?.split(' ').includes('offline_access')) {
// if the request includes the OIDC-only "offline_access" scope,
// we need to set the prompt to "consent" to ensure the user is prompted to grant offline access
// https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
diff --git a/dist/esm/client/auth.d.ts b/dist/esm/client/auth.d.ts
index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433beea1b32f04 100644
--- a/dist/esm/client/auth.d.ts
+++ b/dist/esm/client/auth.d.ts
@@ -205,6 +205,15 @@ export declare function parseErrorResponse(input: Response | string): Promise<OA
* @returns A Promise that resolves to an OAuthError instance
*/
export declare function parseErrorResponse(input: Response | string): Promise<OAuthError>;
+/**
+ * Selects scopes per the MCP spec and augments them for refresh token support.
+ */
+export declare function determineScope(options: {
+ requestedScope?: string;
+ resourceMetadata?: OAuthProtectedResourceMetadata;
+ authServerMetadata?: AuthorizationServerMetadata;
+ clientMetadata: OAuthClientMetadata;
+}): string | undefined;
/**
* Orchestrates the full auth flow with a server.
*
diff --git a/dist/esm/client/auth.js b/dist/esm/client/auth.js
index e183040fc2bba22ca1ccc784984f3310854403b7..d367661e580ee61a96654f7af78b2af61dcad98b 100644
--- a/dist/esm/client/auth.js
+++ b/dist/esm/client/auth.js
@@ -161,6 +161,19 @@ export async function auth(provider, options) {
throw error;
}
}
+/**
+ * Selects scopes per the MCP spec and augments them for refresh token support.
+ */
+export function determineScope({ requestedScope, resourceMetadata, authServerMetadata, clientMetadata }) {
+ let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(' ') || clientMetadata.scope;
+ if (effectiveScope &&
+ authServerMetadata?.scopes_supported?.includes('offline_access') &&
+ !effectiveScope.split(' ').includes('offline_access') &&
+ clientMetadata.grant_types?.includes('refresh_token')) {
+ effectiveScope = `${effectiveScope} offline_access`;
+ }
+ return effectiveScope;
+}
async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
// Check if the provider has cached discovery state to skip discovery
const cachedState = await provider.discoveryState?.();
@@ -216,12 +229,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
});
}
const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
- // Apply scope selection strategy (SEP-835):
- // 1. WWW-Authenticate scope (passed via `scope` param)
- // 2. PRM scopes_supported
- // 3. Client metadata scope (user-configured fallback)
- // The resolved scope is used consistently for both DCR and the authorization request.
- const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope;
+ const resolvedScope = determineScope({
+ requestedScope: scope,
+ resourceMetadata,
+ authServerMetadata: metadata,
+ clientMetadata: provider.clientMetadata
+ });
// Handle client registration if needed
let clientInformation = await Promise.resolve(provider.clientInformation());
if (!clientInformation) {
@@ -716,7 +729,7 @@ export async function startAuthorization(authorizationServerUrl, { metadata, cli
if (scope) {
authorizationUrl.searchParams.set('scope', scope);
}
- if (scope?.includes('offline_access')) {
+ if (scope?.split(' ').includes('offline_access')) {
// if the request includes the OIDC-only "offline_access" scope,
// we need to set the prompt to "consent" to ensure the user is prompted to grant offline access
// https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
diff --git a/dist/esm/client/index.js b/dist/esm/client/index.js
index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8eb9e4caff 100644
--- a/dist/esm/client/index.js
+++ b/dist/esm/client/index.js
@@ -284,41 +284,16 @@ export class Client extends Protocol {
}
async connect(transport, options) {
await super.connect(transport);
+ transport.onsessionexpired = async () => {
+ await this._initialize(transport);
+ };
// When transport sessionId is already set this means we are trying to reconnect.
// In this case we don't need to initialize again.
if (transport.sessionId !== undefined) {
return;
}
try {
- const result = await this.request({
- method: 'initialize',
- params: {
- protocolVersion: LATEST_PROTOCOL_VERSION,
- capabilities: this._capabilities,
- clientInfo: this._clientInfo
- }
- }, InitializeResultSchema, options);
- if (result === undefined) {
- throw new Error(`Server sent invalid initialize result: ${result}`);
- }
- if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
- throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
- }
- this._serverCapabilities = result.capabilities;
- this._serverVersion = result.serverInfo;
- // HTTP transports must set the protocol version in each header after initialization.
- if (transport.setProtocolVersion) {
- transport.setProtocolVersion(result.protocolVersion);
- }
- this._instructions = result.instructions;
- await this.notification({
- method: 'notifications/initialized'
- });
- // Set up list changed handlers now that we know server capabilities
- if (this._pendingListChangedConfig) {
- this._setupListChangedHandlers(this._pendingListChangedConfig);
- this._pendingListChangedConfig = undefined;
- }
+ await this._initialize(transport, options);
}
catch (error) {
// Disconnect if initialization fails.
@@ -326,6 +301,37 @@ export class Client extends Protocol {
throw error;
}
}
+ async _initialize(transport, options) {
+ const result = await this.request({
+ method: 'initialize',
+ params: {
+ protocolVersion: LATEST_PROTOCOL_VERSION,
+ capabilities: this._capabilities,
+ clientInfo: this._clientInfo
+ }
+ }, InitializeResultSchema, options);
+ if (result === undefined) {
+ throw new Error(`Server sent invalid initialize result: ${result}`);
+ }
+ if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
+ throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
+ }
+ this._serverCapabilities = result.capabilities;
+ this._serverVersion = result.serverInfo;
+ // HTTP transports must set the protocol version in each header after initialization.
+ if (transport.setProtocolVersion) {
+ transport.setProtocolVersion(result.protocolVersion);
+ }
+ this._instructions = result.instructions;
+ await this.notification({
+ method: 'notifications/initialized'
+ });
+ // Set up list changed handlers now that we know server capabilities
+ if (this._pendingListChangedConfig) {
+ this._setupListChangedHandlers(this._pendingListChangedConfig);
+ this._pendingListChangedConfig = undefined;
+ }
+ }
/**
* After initialization has completed, this will be populated with the server's reported capabilities.
*/
@@ -537,9 +543,11 @@ export class Client extends Protocol {
* Called after listTools() to pre-compile validators for better performance.
*/
- cacheToolMetadata(tools) {
- this._cachedToolOutputValidators.clear();
- this._cachedKnownTaskTools.clear();
- this._cachedRequiredTaskTools.clear();
+ cacheToolMetadata(tools, reset = true) {
+ if (reset) {
+ this._cachedToolOutputValidators.clear();
+ this._cachedKnownTaskTools.clear();
+ this._cachedRequiredTaskTools.clear();
+ }
for (const tool of tools) {
// If the tool has an outputSchema, create and cache the validator
if (tool.outputSchema) {
@@ -565,7 +573,7 @@ export class Client extends Protocol {
async listTools(params, options) {
const result = await this.request({ method: 'tools/list', params }, ListToolsResultSchema, options);
// Cache the tools and their output schemas for future validation
- this.cacheToolMetadata(result.tools);
+ this.cacheToolMetadata(result.tools, params?.cursor === undefined);
return result;
}
/**
diff --git a/dist/esm/client/streamableHttp.js b/dist/esm/client/streamableHttp.js
index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da884fcc627a 100644
--- a/dist/esm/client/streamableHttp.js
+++ b/dist/esm/client/streamableHttp.js
@@ -1,5 +1,5 @@
import { createFetchWithInit, normalizeHeaders } from '../shared/transport.js';
-import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js';
+import { isInitializedNotification, isInitializeRequest, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js';
import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js';
import { EventSourceParserStream } from 'eventsource-parser/stream';
// Default reconnection options for StreamableHTTP connections
@@ -286,7 +286,38 @@ export class StreamableHTTPClientTransport {
this.onclose?.();
}
async send(message, options) {
+ return this._send(message, options, false);
+ }
+ async _recoverSession(expiredSessionId) {
+ if (this._sessionRecovery) {
+ await this._sessionRecovery;
+ return true;
+ }
+ if (this._sessionId !== expiredSessionId)
+ return true;
+ this._sessionId = undefined;
+ this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired?.());
try {
+ await this._sessionRecovery;
+ }
+ catch (error) {
+ this._sessionId = undefined;
+ await this.close();
+ throw error;
+ }
+ finally {
+ this._sessionRecovery = undefined;
+ }
+ return true;
+ }
+ async _send(message, options, isSessionRetry) {
+ try {
+ if (this._sessionRecovery && !isInitializeRequest(message) && !isInitializedNotification(message)) {
+ await this._sessionRecovery;
+ if (options?.isRequestActive?.() === false) {
+ throw new Error('Request is no longer active');
+ }
+ }
const { resumptionToken, onresumptiontoken } = options || {};
if (resumptionToken) {
// If we have at last event ID, we need to reconnect the SSE stream
@@ -294,6 +325,7 @@ export class StreamableHTTPClientTransport {
return;
}
const headers = await this._commonHeaders();
+ const requestSessionId = headers.get('mcp-session-id') ?? undefined;
headers.set('content-type', 'application/json');
headers.set('accept', 'application/json, text/event-stream');
const init = {
@@ -306,11 +338,20 @@ export class StreamableHTTPClientTransport {
const response = await (this._fetch ?? fetch)(this._url, init);
// Handle session ID received during initialization
const sessionId = response.headers.get('mcp-session-id');
- if (sessionId) {
+ if (sessionId && (requestSessionId === undefined || this._sessionId === requestSessionId)) {
this._sessionId = sessionId;
}
if (!response.ok) {
const text = await response.text().catch(() => null);
+ if (response.status === 404 && requestSessionId && !isSessionRetry && !isInitializedNotification(message)) {
+ const recovered = await this._recoverSession(requestSessionId);
+ if (options?.isRequestActive?.() === false) {
+ throw new Error('Request is no longer active');
+ }
+ if (recovered) {
+ return this._send(message, options, true);
+ }
+ }
if (response.status === 401 && this._authProvider) {
// Prevent infinite recursion when server returns 401 after successful auth
if (this._hasCompletedAuthFlow) {
@@ -331,7 +372,7 @@ export class StreamableHTTPClientTransport {
// Mark that we completed auth flow
this._hasCompletedAuthFlow = true;
// Purposely _not_ awaited, so we don't call onerror twice
- return this.send(message);
+ return this._send(message, options, isSessionRetry);
}
if (response.status === 403 && this._authProvider) {
const { resourceMetadataUrl, scope, error } = extractWWWAuthenticateParams(response);
@@ -358,7 +399,7 @@ export class StreamableHTTPClientTransport {
if (result !== 'AUTHORIZED') {
throw new UnauthorizedError();
}
- return this.send(message);
+ return this._send(message, options, isSessionRetry);
}
}
throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
diff --git a/dist/esm/shared/protocol.js b/dist/esm/shared/protocol.js
index bfa2b7120a0f50c569364ea5264e6f811076f44f..abd8dfd707c155f71dae7aeeeeaf7547368ac749 100644
--- a/dist/esm/shared/protocol.js
+++ b/dist/esm/shared/protocol.js
@@ -740,7 +740,12 @@ export class Protocol {
}
else {
// No related task - send through transport normally
- this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => {
+ this._transport.send(jsonrpcRequest, {
+ relatedRequestId,
+ resumptionToken,
+ onresumptiontoken,
+ isRequestActive: () => this._responseHandlers.has(messageId)
+ }).catch(error => {
this._cleanupTimeout(messageId);
reject(error);
});
+146 -10
View File
@@ -1,11 +1,5 @@
diff --git a/Users/brendonovich/github.com/anomalyco/opencode/node_modules/solid-js/.bun-tag-6fcb6b48d6947d2c b/.bun-tag-6fcb6b48d6947d2c
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/Users/brendonovich/github.com/anomalyco/opencode/node_modules/solid-js/.bun-tag-b272f631c12927b0 b/.bun-tag-b272f631c12927b0
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/dist/dev.cjs b/dist/dev.cjs
index 7104749486e4361e8c4ee7836a8046582cec7aa1..0501eb1ec5d13b81ecb13a5ac1a82db42502b976 100644
index 7104749..dc3eac9 100644
--- a/dist/dev.cjs
+++ b/dist/dev.cjs
@@ -764,6 +764,8 @@ function runComputation(node, value, time) {
@@ -17,8 +11,33 @@ index 7104749486e4361e8c4ee7836a8046582cec7aa1..0501eb1ec5d13b81ecb13a5ac1a82db4
Transition.sources.add(node);
node.tValue = nextValue;
} else node.value = nextValue;
@@ -987,18 +989,21 @@ function cleanNode(node) {
}
}
if (node.tOwned) {
- for (i = node.tOwned.length - 1; i >= 0; i--) cleanNode(node.tOwned[i]);
+ const tOwned = node.tOwned;
delete node.tOwned;
+ for (i = tOwned.length - 1; i >= 0; i--) cleanNode(tOwned[i]);
}
if (Transition && Transition.running && node.pure) {
reset(node, true);
} else if (node.owned) {
- for (i = node.owned.length - 1; i >= 0; i--) cleanNode(node.owned[i]);
+ const owned = node.owned;
node.owned = null;
+ for (i = owned.length - 1; i >= 0; i--) cleanNode(owned[i]);
}
if (node.cleanups) {
- for (i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i]();
+ const cleanups = node.cleanups;
node.cleanups = null;
+ for (i = cleanups.length - 1; i >= 0; i--) cleanups[i]();
}
if (Transition && Transition.running) node.tState = 0;else node.state = 0;
delete node.sourceMap;
diff --git a/dist/dev.js b/dist/dev.js
index ea5e4bc2fd4f0b3922a73d9134439529dc81339f..4b3ec07e624d20fdd23d6941a4fdde6d3a78cca3 100644
index ea5e4bc..a2e2d59 100644
--- a/dist/dev.js
+++ b/dist/dev.js
@@ -762,6 +762,8 @@ function runComputation(node, value, time) {
@@ -30,8 +49,75 @@ index ea5e4bc2fd4f0b3922a73d9134439529dc81339f..4b3ec07e624d20fdd23d6941a4fdde6d
Transition.sources.add(node);
node.tValue = nextValue;
} else node.value = nextValue;
@@ -985,18 +987,21 @@ function cleanNode(node) {
}
}
if (node.tOwned) {
- for (i = node.tOwned.length - 1; i >= 0; i--) cleanNode(node.tOwned[i]);
+ const tOwned = node.tOwned;
delete node.tOwned;
+ for (i = tOwned.length - 1; i >= 0; i--) cleanNode(tOwned[i]);
}
if (Transition && Transition.running && node.pure) {
reset(node, true);
} else if (node.owned) {
- for (i = node.owned.length - 1; i >= 0; i--) cleanNode(node.owned[i]);
+ const owned = node.owned;
node.owned = null;
+ for (i = owned.length - 1; i >= 0; i--) cleanNode(owned[i]);
}
if (node.cleanups) {
- for (i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i]();
+ const cleanups = node.cleanups;
node.cleanups = null;
+ for (i = cleanups.length - 1; i >= 0; i--) cleanups[i]();
}
if (Transition && Transition.running) node.tState = 0;else node.state = 0;
delete node.sourceMap;
diff --git a/dist/server.cjs b/dist/server.cjs
index e715309..188ba81 100644
--- a/dist/server.cjs
+++ b/dist/server.cjs
@@ -127,12 +127,14 @@ function onCleanup(fn) {
}
function cleanNode(node) {
if (node.owned) {
- for (let i = 0; i < node.owned.length; i++) cleanNode(node.owned[i]);
+ const owned = node.owned;
node.owned = null;
+ for (let i = 0; i < owned.length; i++) cleanNode(owned[i]);
}
if (node.cleanups) {
- for (let i = 0; i < node.cleanups.length; i++) node.cleanups[i]();
+ const cleanups = node.cleanups;
node.cleanups = null;
+ for (let i = 0; i < cleanups.length; i++) cleanups[i]();
}
}
function catchError(fn, handler) {
diff --git a/dist/server.js b/dist/server.js
index d5f8803..320d9af 100644
--- a/dist/server.js
+++ b/dist/server.js
@@ -125,12 +125,14 @@ function onCleanup(fn) {
}
function cleanNode(node) {
if (node.owned) {
- for (let i = 0; i < node.owned.length; i++) cleanNode(node.owned[i]);
+ const owned = node.owned;
node.owned = null;
+ for (let i = 0; i < owned.length; i++) cleanNode(owned[i]);
}
if (node.cleanups) {
- for (let i = 0; i < node.cleanups.length; i++) node.cleanups[i]();
+ const cleanups = node.cleanups;
node.cleanups = null;
+ for (let i = 0; i < cleanups.length; i++) cleanups[i]();
}
}
function catchError(fn, handler) {
diff --git a/dist/solid.cjs b/dist/solid.cjs
index 7c133a2b254678a84fd61d719fbeffad766e1331..2f68c99f2698210cc0bac62f074cc8cd3beb2881 100644
index 7c133a2..5ef1501 100644
--- a/dist/solid.cjs
+++ b/dist/solid.cjs
@@ -717,6 +717,8 @@ function runComputation(node, value, time) {
@@ -43,8 +129,33 @@ index 7c133a2b254678a84fd61d719fbeffad766e1331..2f68c99f2698210cc0bac62f074cc8cd
Transition.sources.add(node);
node.tValue = nextValue;
} else node.value = nextValue;
@@ -938,18 +940,21 @@ function cleanNode(node) {
}
}
if (node.tOwned) {
- for (i = node.tOwned.length - 1; i >= 0; i--) cleanNode(node.tOwned[i]);
+ const tOwned = node.tOwned;
delete node.tOwned;
+ for (i = tOwned.length - 1; i >= 0; i--) cleanNode(tOwned[i]);
}
if (Transition && Transition.running && node.pure) {
reset(node, true);
} else if (node.owned) {
- for (i = node.owned.length - 1; i >= 0; i--) cleanNode(node.owned[i]);
+ const owned = node.owned;
node.owned = null;
+ for (i = owned.length - 1; i >= 0; i--) cleanNode(owned[i]);
}
if (node.cleanups) {
- for (i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i]();
+ const cleanups = node.cleanups;
node.cleanups = null;
+ for (i = cleanups.length - 1; i >= 0; i--) cleanups[i]();
}
if (Transition && Transition.running) node.tState = 0;else node.state = 0;
}
diff --git a/dist/solid.js b/dist/solid.js
index 656fd26e7e5c794aa22df19c2377ff5c0591fc29..f08e9f5a7157c3506e5b6922fe2ef991335a80be 100644
index 656fd26..6e0038c 100644
--- a/dist/solid.js
+++ b/dist/solid.js
@@ -715,6 +715,8 @@ function runComputation(node, value, time) {
@@ -56,3 +167,28 @@ index 656fd26e7e5c794aa22df19c2377ff5c0591fc29..f08e9f5a7157c3506e5b6922fe2ef991
Transition.sources.add(node);
node.tValue = nextValue;
} else node.value = nextValue;
@@ -936,18 +938,21 @@ function cleanNode(node) {
}
}
if (node.tOwned) {
- for (i = node.tOwned.length - 1; i >= 0; i--) cleanNode(node.tOwned[i]);
+ const tOwned = node.tOwned;
delete node.tOwned;
+ for (i = tOwned.length - 1; i >= 0; i--) cleanNode(tOwned[i]);
}
if (Transition && Transition.running && node.pure) {
reset(node, true);
} else if (node.owned) {
- for (i = node.owned.length - 1; i >= 0; i--) cleanNode(node.owned[i]);
+ const owned = node.owned;
node.owned = null;
+ for (i = owned.length - 1; i >= 0; i--) cleanNode(owned[i]);
}
if (node.cleanups) {
- for (i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i]();
+ const cleanups = node.cleanups;
node.cleanups = null;
+ for (i = cleanups.length - 1; i >= 0; i--) cleanups[i]();
}
if (Transition && Transition.running) node.tState = 0;else node.state = 0;
}
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.18.7",
"version": "1.18.8",
"publisher": "sst-dev",
"repository": {
"type": "git",