Merge branch 'dev' of https://github.com/sst/opencode into dev

This commit is contained in:
David Hill
2025-10-30 12:16:15 +00:00
29 changed files with 821 additions and 250 deletions

View File

@@ -123,3 +123,5 @@
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) | | 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) | | 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) | | 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |

View File

@@ -37,7 +37,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "0.15.28", "version": "0.15.29",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@@ -64,7 +64,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "0.15.28", "version": "0.15.29",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "2.0.0", "@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2", "@ai-sdk/openai": "2.0.2",
@@ -88,7 +88,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "0.15.28", "version": "0.15.29",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@@ -109,7 +109,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "0.15.28", "version": "0.15.29",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -150,7 +150,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "0.15.28", "version": "0.15.29",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
@@ -166,7 +166,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "0.15.28", "version": "0.15.29",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -234,7 +234,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "0.15.28", "version": "0.15.29",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"zod": "catalog:", "zod": "catalog:",
@@ -254,7 +254,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "0.15.28", "version": "0.15.29",
"devDependencies": { "devDependencies": {
"@hey-api/openapi-ts": "0.81.0", "@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:", "@tsconfig/node22": "catalog:",
@@ -265,7 +265,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "0.15.28", "version": "0.15.29",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@@ -278,7 +278,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "0.15.28", "version": "0.15.29",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@pierre/precision-diffs": "catalog:", "@pierre/precision-diffs": "catalog:",
@@ -301,7 +301,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "0.15.28", "version": "0.15.29",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",

View File

@@ -7,7 +7,7 @@
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev", "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json", "build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start", "start": "vinxi start",
"version": "0.15.28" "version": "0.15.29"
}, },
"dependencies": { "dependencies": {
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",

View File

@@ -25,19 +25,15 @@ export async function POST(event: APIEvent) {
// Create email content // Create email content
const emailContent = ` const emailContent = `
New Enterprise Inquiry ${body.message}<br><br>
--
Name: ${body.name} ${body.name}<br>
Role: ${body.role} ${body.role}<br>
Email: ${body.email} ${body.email}`.trim()
Message:
${body.message}
`.trim()
// Send email using AWS SES // Send email using AWS SES
await AWS.sendEmail({ await AWS.sendEmail({
to: "enterprise@opencode.ai", to: "contact@anoma.ly",
subject: `Enterprise Inquiry from ${body.name}`, subject: `Enterprise Inquiry from ${body.name}`,
body: emailContent, body: emailContent,
}) })

View File

@@ -1,6 +1,6 @@
import "./index.css" import "./index.css"
import { Title, Meta } from "@solidjs/meta" import { Title, Meta } from "@solidjs/meta"
import { createSignal } from "solid-js" import { createSignal, Show } from "solid-js"
import { Header } from "~/component/header" import { Header } from "~/component/header"
import { Footer } from "~/component/footer" import { Footer } from "~/component/footer"
import { Legal } from "~/component/legal" import { Legal } from "~/component/legal"
@@ -64,60 +64,96 @@ export default function Enterprise() {
<div data-component="enterprise-column-1"> <div data-component="enterprise-column-1">
<h2>Your code is yours</h2> <h2>Your code is yours</h2>
<p> <p>
OpenCode operates securely inside your organization with no data or context stored and no licensing restrictions or ownership claims. Start a trial with your team today, then scale confidently with enterprise-grade features including SSO, private registries, and self-hosting. OpenCode operates securely inside your organization with no data or context stored
</p> and no licensing restrictions or ownership claims. Start a trial with your team
<p> , then deploy it across your organization by integrating it with your SSO and internal AI gateway.
Let us know and how we can help.
</p> </p>
<p>Let us know and how we can help.</p>
<Show when={false}>
<div data-component="testimonial"> <div data-component="testimonial">
<div data-component="quotation"> <div data-component="quotation">
<svg width="20" height="17" viewBox="0 0 20 17" fill="none" <svg
xmlns="http://www.w3.org/2000/svg"> width="20"
height="17"
viewBox="0 0 20 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M19.4118 0L16.5882 9.20833H20V17H12.2353V10.0938L16 0H19.4118ZM7.17647 0L4.35294 9.20833H7.76471V17H0V10.0938L3.76471 0H7.17647Z" d="M19.4118 0L16.5882 9.20833H20V17H12.2353V10.0938L16 0H19.4118ZM7.17647 0L4.35294 9.20833H7.76471V17H0V10.0938L3.76471 0H7.17647Z"
fill="currentColor" /> fill="currentColor"
/>
</svg> </svg>
</div> </div>
Thanks to OpenCode, we found a way to create software to track all our assets
Thanks to OpenCode, we found a way to create software to track all our assets even the imaginary ones. even the imaginary ones.
<div data-component="testimonial-logo"> <div data-component="testimonial-logo">
<svg width="80" height="79" viewBox="0 0 80 79" fill="none" <svg
xmlns="http://www.w3.org/2000/svg"> width="80"
<path fill-rule="evenodd" clip-rule="evenodd" height="79"
viewBox="0 0 80 79"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 39.3087L10.0579 29.251L15.6862 34.7868L13.7488 36.7248L10.3345 33.2186L8.48897 35.0639L11.8111 38.4781L9.96557 40.4156L6.55181 37.0018L4.06028 39.4928L7.56674 42.9991L5.62884 44.845L0 39.3087Z" d="M0 39.3087L10.0579 29.251L15.6862 34.7868L13.7488 36.7248L10.3345 33.2186L8.48897 35.0639L11.8111 38.4781L9.96557 40.4156L6.55181 37.0018L4.06028 39.4928L7.56674 42.9991L5.62884 44.845L0 39.3087Z"
fill="#0083C6" /> fill="#0083C6"
<path fill-rule="evenodd" clip-rule="evenodd" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M17.7182 36.8164L20.2094 39.4003L16.6108 46.9666L22.2393 41.3374L24.3615 43.46L14.2118 53.5179L11.9047 51.1187L15.4112 43.3677L9.78254 49.0888L7.66016 46.9666L17.7182 36.8164Z" d="M17.7182 36.8164L20.2094 39.4003L16.6108 46.9666L22.2393 41.3374L24.3615 43.46L14.2118 53.5179L11.9047 51.1187L15.4112 43.3677L9.78254 49.0888L7.66016 46.9666L17.7182 36.8164Z"
fill="#0083C6" /> fill="#0083C6"
<path fill-rule="evenodd" clip-rule="evenodd" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M42.8139 61.915L45.3055 64.4064L41.6145 71.9731L47.243 66.3441L49.3652 68.4663L39.3077 78.5244L36.9088 76.1252L40.5072 68.374L34.7866 74.0953L32.6641 71.9731L42.8139 61.915Z" d="M42.8139 61.915L45.3055 64.4064L41.6145 71.9731L47.243 66.3441L49.3652 68.4663L39.3077 78.5244L36.9088 76.1252L40.5072 68.374L34.7866 74.0953L32.6641 71.9731L42.8139 61.915Z"
fill="#0083C6" /> fill="#0083C6"
<path fill-rule="evenodd" clip-rule="evenodd" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M16.4258 55.7324L26.4833 45.582L28.6061 47.7042C31.0049 50.1034 32.3892 51.9497 30.1746 54.1642C28.7902 55.548 27.6831 56.0094 26.1145 54.9016L26.0222 54.994C27.2218 56.1941 26.9448 57.1162 25.4688 58.5931L23.9 60.1615C23.4383 60.6232 22.8847 61.2693 22.7927 62.0067L20.6705 59.8845C20.7625 59.146 21.3161 58.5008 21.778 58.1316L23.5307 56.3788C24.269 55.6403 23.715 54.2555 23.254 53.8872L22.8847 53.4256L18.548 57.7623L16.4258 55.7324ZM24.3611 51.9495C25.4689 53.0563 26.4833 53.3332 27.4984 52.3178C28.5134 51.3957 28.2367 50.3802 27.1295 49.1812L24.3611 51.9495Z" d="M16.4258 55.7324L26.4833 45.582L28.6061 47.7042C31.0049 50.1034 32.3892 51.9497 30.1746 54.1642C28.7902 55.548 27.6831 56.0094 26.1145 54.9016L26.0222 54.994C27.2218 56.1941 26.9448 57.1162 25.4688 58.5931L23.9 60.1615C23.4383 60.6232 22.8847 61.2693 22.7927 62.0067L20.6705 59.8845C20.7625 59.146 21.3161 58.5008 21.778 58.1316L23.5307 56.3788C24.269 55.6403 23.715 54.2555 23.254 53.8872L22.8847 53.4256L18.548 57.7623L16.4258 55.7324ZM24.3611 51.9495C25.4689 53.0563 26.4833 53.3332 27.4984 52.3178C28.5134 51.3957 28.2367 50.3802 27.1295 49.1812L24.3611 51.9495Z"
fill="#0083C6" /> fill="#0083C6"
<path fill-rule="evenodd" clip-rule="evenodd" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M33.4952 66.9899C31.096 69.3891 28.8815 68.4659 27.4047 66.9899C26.021 65.6062 25.0978 63.3907 27.4972 60.9003L31.8336 56.6548C34.2333 54.2556 36.4478 55.0864 37.9241 56.5635C39.308 58.0396 40.2311 60.2541 37.8315 62.6531L33.4952 66.9899ZM29.0659 63.5752C28.6048 64.0369 28.6048 64.7753 29.1583 65.3292C29.6196 65.8821 30.4502 65.7897 30.8194 65.4215L36.2633 59.9769C36.7246 59.6076 36.7246 58.7779 36.171 58.3164C35.7097 57.7626 34.8791 57.7626 34.5101 58.2241L29.0659 63.5752Z" d="M33.4952 66.9899C31.096 69.3891 28.8815 68.4659 27.4047 66.9899C26.021 65.6062 25.0978 63.3907 27.4972 60.9003L31.8336 56.6548C34.2333 54.2556 36.4478 55.0864 37.9241 56.5635C39.308 58.0396 40.2311 60.2541 37.8315 62.6531L33.4952 66.9899ZM29.0659 63.5752C28.6048 64.0369 28.6048 64.7753 29.1583 65.3292C29.6196 65.8821 30.4502 65.7897 30.8194 65.4215L36.2633 59.9769C36.7246 59.6076 36.7246 58.7779 36.171 58.3164C35.7097 57.7626 34.8791 57.7626 34.5101 58.2241L29.0659 63.5752Z"
fill="#0083C6" /> fill="#0083C6"
<path fill-rule="evenodd" clip-rule="evenodd" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M78.5267 39.308L68.2845 29.0654L47.5231 49.735L49.6453 51.8572L68.2845 33.2179L74.3746 39.308L47.2461 66.3435L49.3683 68.4657L78.5267 39.308Z" d="M78.5267 39.308L68.2845 29.0654L47.5231 49.735L49.6453 51.8572L68.2845 33.2179L74.3746 39.308L47.2461 66.3435L49.3683 68.4657L78.5267 39.308Z"
fill="#0083C6" /> fill="#0083C6"
<path fill-rule="evenodd" clip-rule="evenodd" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M49.6443 51.8577L43.3695 45.4902L64.0386 24.8215L53.7969 14.4873L33.0352 35.2482L35.1574 37.3705L53.7969 18.7315L59.7947 24.8215L39.1251 45.4902L47.5221 53.9799L49.6443 51.8577Z" d="M49.6443 51.8577L43.3695 45.4902L64.0386 24.8215L53.7969 14.4873L33.0352 35.2482L35.1574 37.3705L53.7969 18.7315L59.7947 24.8215L39.1251 45.4902L47.5221 53.9799L49.6443 51.8577Z"
fill="#2D9C5C" /> fill="#2D9C5C"
<path fill-rule="evenodd" clip-rule="evenodd" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M35.1564 37.3706L28.7896 31.0038L49.5515 10.3347L39.3088 0L10.0586 29.2507L12.1804 31.2804L39.3088 4.24476L45.3066 10.3347L24.6377 31.0038L33.0342 39.4008L35.1564 37.3706Z" d="M35.1564 37.3706L28.7896 31.0038L49.5515 10.3347L39.3088 0L10.0586 29.2507L12.1804 31.2804L39.3088 4.24476L45.3066 10.3347L24.6377 31.0038L33.0342 39.4008L35.1564 37.3706Z"
fill="#E92A35" /> fill="#E92A35"
<path fill-rule="evenodd" clip-rule="evenodd" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M77.2332 52.4105C76.0336 52.4105 75.111 51.4884 75.111 50.196C75.111 48.9046 76.0336 47.9814 77.2332 47.9814C78.3405 47.9814 79.263 48.9046 79.263 50.196C79.263 51.4884 78.3405 52.4105 77.2332 52.4105ZM77.2332 52.9643C78.7098 52.9643 80.0015 51.6729 80.0015 50.196C80.0015 48.6276 78.7096 47.4287 77.2332 47.4287C75.6644 47.4287 74.4648 48.6278 74.4648 50.196C74.4647 51.6731 75.6643 52.9643 77.2332 52.9643ZM76.1259 51.7653H76.6797V50.3804H77.0485L77.8788 51.7653H78.4332L77.6023 50.3804C78.1558 50.2881 78.4332 50.0122 78.4332 49.5507C78.4332 48.9046 78.0633 48.6276 77.3253 48.6276H76.1257V51.7653H76.1259ZM76.6797 49.0892H77.2332C77.5102 49.0892 77.8788 49.0892 77.8788 49.4586C77.8788 49.9202 77.6023 49.9202 77.2332 49.9202H76.6797V49.0892Z" d="M77.2332 52.4105C76.0336 52.4105 75.111 51.4884 75.111 50.196C75.111 48.9046 76.0336 47.9814 77.2332 47.9814C78.3405 47.9814 79.263 48.9046 79.263 50.196C79.263 51.4884 78.3405 52.4105 77.2332 52.4105ZM77.2332 52.9643C78.7098 52.9643 80.0015 51.6729 80.0015 50.196C80.0015 48.6276 78.7096 47.4287 77.2332 47.4287C75.6644 47.4287 74.4648 48.6278 74.4648 50.196C74.4647 51.6731 75.6643 52.9643 77.2332 52.9643ZM76.1259 51.7653H76.6797V50.3804H77.0485L77.8788 51.7653H78.4332L77.6023 50.3804C78.1558 50.2881 78.4332 50.0122 78.4332 49.5507C78.4332 48.9046 78.0633 48.6276 77.3253 48.6276H76.1257V51.7653H76.1259ZM76.6797 49.0892H77.2332C77.5102 49.0892 77.8788 49.0892 77.8788 49.4586C77.8788 49.9202 77.6023 49.9202 77.2332 49.9202H76.6797V49.0892Z"
fill="#0083C6" /> fill="#0083C6"
/>
</svg> </svg>
</div> </div>
</div> </div>
</Show>
</div> </div>
<div data-component="enterprise-column-2"> <div data-component="enterprise-column-2">
@@ -130,7 +166,7 @@ export default function Enterprise() {
type="text" type="text"
required required
value={formData().name} value={formData().name}
onInput={handleInputChange('name')} onInput={handleInputChange("name")}
placeholder="Jeff Bezos" placeholder="Jeff Bezos"
/> />
</div> </div>
@@ -142,7 +178,7 @@ export default function Enterprise() {
type="text" type="text"
required required
value={formData().role} value={formData().role}
onInput={handleInputChange('role')} onInput={handleInputChange("role")}
placeholder="Executive Chairman" placeholder="Executive Chairman"
/> />
</div> </div>
@@ -154,27 +190,25 @@ export default function Enterprise() {
type="email" type="email"
required required
value={formData().email} value={formData().email}
onInput={handleInputChange('email')} onInput={handleInputChange("email")}
placeholder="jeff@amazon.com" placeholder="jeff@amazon.com"
/> />
</div> </div>
<div data-component="form-group"> <div data-component="form-group">
<label for="message">What problem are you trying to <label for="message">What problem are you trying to solve?</label>
solve?</label>
<textarea <textarea
id="message" id="message"
required required
rows={5} rows={5}
value={formData().message} value={formData().message}
onInput={handleInputChange('message')} onInput={handleInputChange("message")}
placeholder="We need help with..." placeholder="We need help with..."
/> />
</div> </div>
<button type="submit" disabled={isSubmitting()} <button type="submit" disabled={isSubmitting()} data-component="submit-button">
data-component="submit-button"> {isSubmitting() ? "Sending..." : "Send"}
{isSubmitting() ? 'Sending...' : 'Send'}
</button> </button>
</form> </form>
@@ -194,53 +228,32 @@ export default function Enterprise() {
</div> </div>
<ul> <ul>
<li> <li>
<Faq question="Does Opencode store our code or context data?"> <Faq question="What is OpenCode Enterprise?">
No. OpenCode never stores your code or context data. All OpenCode Enterprise is for organizations that want to ensure that their code and
processing happens locally or directly with your AI provider. data never leaves their infrastructure. It can do this by using a centralized
config that integrates with your SSO and internal AI gateway.
</Faq> </Faq>
</li> </li>
<li> <li>
<Faq question="Who owns the code generated with OpenCode?"> <Faq question="How do I get started with OpenCode Enterprise?">
You do. All code produced is yours, with no licensing Simply start with an internal trial with your team. OpenCode by default does not
restrictions or ownership claims. store your code or context data, making it easy to get started. Then contact us to
discuss pricing and implementation options.
</Faq> </Faq>
</li> </li>
<li> <li>
<Faq <Faq question="How does enterprise pricing work?">
question="How can we trial OpenCode inside our organization?"> We offer per-seat enterprise pricing. If you have your own LLM gateway, we do not
Simply install and run an internal trial with your team. Since charge for tokens used. For further details, contact us for a custom quote based
OpenCode doesnt store any data, your developers can get on your organization's needs.
started right away.
</Faq> </Faq>
</li> </li>
<li> <li>
<Faq <Faq question="Is my data secure with OpenCode Enterprise?">
question="What happens if someone uses the `/share` feature?"> Yes. OpenCode does not store your code or context data. All processing happens
By default, sharing is disabled. If enabled, conversations are locally or through direct API calls to your AI provider. With central config and
sent to our share service and cached through our CDN. For SSO integration, your data remains secure within your organization's
enterprise use, we recommend disabling or self-hosting this infrastructure.
feature.
</Faq>
</li>
<li>
<Faq question="Can OpenCode integrate with our companys SSO?">
Yes. Enterprise deployments can include SSO integration so all
sessions and shared conversations are protected by your
authentication system.
</Faq>
</li>
<li>
<Faq question="Can OpenCode be self-hosted?">
Absolutely. You can fully self-host OpenCode, including the
share feature, ensuring that data and pages are accessible
only after authentication.
</Faq>
</li>
<li>
<Faq
question="How do we get started with enterprise deployment?">
Contact us to discuss pricing, implementation, and enterprise
options like SSO, private registries, and self-hosting.
</Faq> </Faq>
</li> </li>
</ul> </ul>

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "0.15.28", "version": "0.15.29",
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "0.15.28", "version": "0.15.29",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "0.15.28", "version": "0.15.29",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "0.15.28", "version": "0.15.29",
"description": "", "description": "",
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "0.15.28", "version": "0.15.29",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "0.15.28", "version": "0.15.29",
"name": "opencode", "name": "opencode",
"type": "module", "type": "module",
"private": true, "private": true,

View File

@@ -12,7 +12,11 @@ import { NamedError } from "../util/error"
import matter from "gray-matter" import matter from "gray-matter"
import { Flag } from "../flag/flag" import { Flag } from "../flag/flag"
import { Auth } from "../auth" import { Auth } from "../auth"
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser" import {
type ParseError as JsoncParseError,
parse as parseJsonc,
printParseErrorCode,
} from "jsonc-parser"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { LSPServer } from "../lsp/server" import { LSPServer } from "../lsp/server"
import { BunProc } from "@/bun" import { BunProc } from "@/bun"
@@ -46,7 +50,10 @@ export namespace Config {
if (value.type === "wellknown") { if (value.type === "wellknown") {
process.env[value.key] = value.token process.env[value.key] = value.token
const wellknown = await fetch(`${key}/.well-known/opencode`).then((x) => x.json()) const wellknown = await fetch(`${key}/.well-known/opencode`).then((x) => x.json())
result = mergeDeep(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd())) result = mergeDeep(
result,
await load(JSON.stringify(wellknown.config ?? {}), process.cwd()),
)
} }
} }
@@ -57,7 +64,11 @@ export namespace Config {
const directories = [ const directories = [
Global.Path.config, Global.Path.config,
...(await Array.fromAsync( ...(await Array.fromAsync(
Filesystem.up({ targets: [".opencode"], start: Instance.directory, stop: Instance.worktree }), Filesystem.up({
targets: [".opencode"],
start: Instance.directory,
stop: Instance.worktree,
}),
)), )),
] ]
@@ -153,10 +164,18 @@ export namespace Config {
const gitignore = path.join(dir, ".gitignore") const gitignore = path.join(dir, ".gitignore")
const hasGitIgnore = await Bun.file(gitignore).exists() const hasGitIgnore = await Bun.file(gitignore).exists()
if (!hasGitIgnore) await Bun.write(gitignore, ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n")) if (!hasGitIgnore)
await Bun.write(
gitignore,
["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"),
)
await BunProc.run( await BunProc.run(
["add", "@opencode-ai/plugin@" + (Installation.isLocal() ? "latest" : Installation.VERSION), "--exact"], [
"add",
"@opencode-ai/plugin@" + (Installation.isLocal() ? "latest" : Installation.VERSION),
"--exact",
],
{ {
cwd: dir, cwd: dir,
}, },
@@ -166,7 +185,12 @@ export namespace Config {
const COMMAND_GLOB = new Bun.Glob("command/**/*.md") const COMMAND_GLOB = new Bun.Glob("command/**/*.md")
async function loadCommand(dir: string) { async function loadCommand(dir: string) {
const result: Record<string, Command> = {} const result: Record<string, Command> = {}
for await (const item of COMMAND_GLOB.scan({ absolute: true, followSymlinks: true, dot: true, cwd: dir })) { for await (const item of COMMAND_GLOB.scan({
absolute: true,
followSymlinks: true,
dot: true,
cwd: dir,
})) {
const content = await Bun.file(item).text() const content = await Bun.file(item).text()
const md = matter(content) const md = matter(content)
if (!md.data) continue if (!md.data) continue
@@ -201,7 +225,12 @@ export namespace Config {
async function loadAgent(dir: string) { async function loadAgent(dir: string) {
const result: Record<string, Agent> = {} const result: Record<string, Agent> = {}
for await (const item of AGENT_GLOB.scan({ absolute: true, followSymlinks: true, dot: true, cwd: dir })) { for await (const item of AGENT_GLOB.scan({
absolute: true,
followSymlinks: true,
dot: true,
cwd: dir,
})) {
const content = await Bun.file(item).text() const content = await Bun.file(item).text()
const md = matter(content) const md = matter(content)
if (!md.data) continue if (!md.data) continue
@@ -239,7 +268,12 @@ export namespace Config {
const MODE_GLOB = new Bun.Glob("mode/*.md") const MODE_GLOB = new Bun.Glob("mode/*.md")
async function loadMode(dir: string) { async function loadMode(dir: string) {
const result: Record<string, Agent> = {} const result: Record<string, Agent> = {}
for await (const item of MODE_GLOB.scan({ absolute: true, followSymlinks: true, dot: true, cwd: dir })) { for await (const item of MODE_GLOB.scan({
absolute: true,
followSymlinks: true,
dot: true,
cwd: dir,
})) {
const content = await Bun.file(item).text() const content = await Bun.file(item).text()
const md = matter(content) const md = matter(content)
if (!md.data) continue if (!md.data) continue
@@ -265,7 +299,12 @@ export namespace Config {
async function loadPlugin(dir: string) { async function loadPlugin(dir: string) {
const plugins: string[] = [] const plugins: string[] = []
for await (const item of PLUGIN_GLOB.scan({ absolute: true, followSymlinks: true, dot: true, cwd: dir })) { for await (const item of PLUGIN_GLOB.scan({
absolute: true,
followSymlinks: true,
dot: true,
cwd: dir,
})) {
plugins.push("file://" + item) plugins.push("file://" + item)
} }
return plugins return plugins
@@ -280,6 +319,14 @@ export namespace Config {
.optional() .optional()
.describe("Environment variables to set when running the MCP server"), .describe("Environment variables to set when running the MCP server"),
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
timeout: z
.number()
.int()
.positive()
.optional()
.describe(
"Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.",
),
}) })
.strict() .strict()
.meta({ .meta({
@@ -291,7 +338,18 @@ export namespace Config {
type: z.literal("remote").describe("Type of MCP server connection"), type: z.literal("remote").describe("Type of MCP server connection"),
url: z.string().describe("URL of the remote MCP server"), url: z.string().describe("URL of the remote MCP server"),
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"), headers: z
.record(z.string(), z.string())
.optional()
.describe("Headers to send with the request"),
timeout: z
.number()
.int()
.positive()
.optional()
.describe(
"Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.",
),
}) })
.strict() .strict()
.meta({ .meta({
@@ -339,72 +397,148 @@ export namespace Config {
export const Keybinds = z export const Keybinds = z
.object({ .object({
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"), leader: z
.string()
.optional()
.default("ctrl+x")
.describe("Leader key for keybind combinations"),
app_help: z.string().optional().default("<leader>h").describe("Show help dialog"), app_help: z.string().optional().default("<leader>h").describe("Show help dialog"),
app_exit: z.string().optional().default("ctrl+c,<leader>q").describe("Exit the application"), app_exit: z.string().optional().default("ctrl+c,<leader>q").describe("Exit the application"),
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"), editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
theme_list: z.string().optional().default("<leader>t").describe("List available themes"), theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
project_init: z.string().optional().default("<leader>i").describe("Create/update AGENTS.md"), project_init: z.string().optional().default("<leader>i").describe("Create/update AGENTS.md"),
tool_details: z.string().optional().default("<leader>d").describe("Toggle tool details"), tool_details: z.string().optional().default("<leader>d").describe("Toggle tool details"),
thinking_blocks: z.string().optional().default("<leader>b").describe("Toggle thinking blocks"), thinking_blocks: z
session_export: z.string().optional().default("<leader>x").describe("Export session to editor"), .string()
.optional()
.default("<leader>b")
.describe("Toggle thinking blocks"),
session_export: z
.string()
.optional()
.default("<leader>x")
.describe("Export session to editor"),
session_new: z.string().optional().default("<leader>n").describe("Create a new session"), session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
session_list: z.string().optional().default("<leader>l").describe("List all sessions"), session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"), session_timeline: z
.string()
.optional()
.default("<leader>g")
.describe("Show session timeline"),
session_share: z.string().optional().default("<leader>s").describe("Share current session"), session_share: z.string().optional().default("<leader>s").describe("Share current session"),
session_unshare: z.string().optional().default("none").describe("Unshare current session"), session_unshare: z.string().optional().default("none").describe("Unshare current session"),
session_interrupt: z.string().optional().default("esc").describe("Interrupt current session"), session_interrupt: z.string().optional().default("esc").describe("Interrupt current session"),
session_compact: z.string().optional().default("<leader>c").describe("Compact the session"), session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
session_child_cycle: z.string().optional().default("ctrl+right").describe("Cycle to next child session"), session_child_cycle: z
.string()
.optional()
.default("ctrl+right")
.describe("Cycle to next child session"),
session_child_cycle_reverse: z session_child_cycle_reverse: z
.string() .string()
.optional() .optional()
.default("ctrl+left") .default("ctrl+left")
.describe("Cycle to previous child session"), .describe("Cycle to previous child session"),
messages_page_up: z.string().optional().default("pgup").describe("Scroll messages up by one page"), messages_page_up: z
messages_page_down: z.string().optional().default("pgdown").describe("Scroll messages down by one page"), .string()
messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"), .optional()
.default("pgup")
.describe("Scroll messages up by one page"),
messages_page_down: z
.string()
.optional()
.default("pgdown")
.describe("Scroll messages down by one page"),
messages_half_page_up: z
.string()
.optional()
.default("ctrl+alt+u")
.describe("Scroll messages up by half page"),
messages_half_page_down: z messages_half_page_down: z
.string() .string()
.optional() .optional()
.default("ctrl+alt+d") .default("ctrl+alt+d")
.describe("Scroll messages down by half page"), .describe("Scroll messages down by half page"),
messages_first: z.string().optional().default("ctrl+g").describe("Navigate to first message"), messages_first: z.string().optional().default("ctrl+g").describe("Navigate to first message"),
messages_last: z.string().optional().default("ctrl+alt+g").describe("Navigate to last message"), messages_last: z
.string()
.optional()
.default("ctrl+alt+g")
.describe("Navigate to last message"),
messages_copy: z.string().optional().default("<leader>y").describe("Copy message"), messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
messages_undo: z.string().optional().default("<leader>u").describe("Undo message"), messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
messages_redo: z.string().optional().default("<leader>r").describe("Redo message"), messages_redo: z.string().optional().default("<leader>r").describe("Redo message"),
model_list: z.string().optional().default("<leader>m").describe("List available models"), model_list: z.string().optional().default("<leader>m").describe("List available models"),
model_cycle_recent: z.string().optional().default("f2").describe("Next recent model"), model_cycle_recent: z.string().optional().default("f2").describe("Next recent model"),
model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recent model"), model_cycle_recent_reverse: z
.string()
.optional()
.default("shift+f2")
.describe("Previous recent model"),
agent_list: z.string().optional().default("<leader>a").describe("List agents"), agent_list: z.string().optional().default("<leader>a").describe("List agents"),
agent_cycle: z.string().optional().default("tab").describe("Next agent"), agent_cycle: z.string().optional().default("tab").describe("Next agent"),
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"), agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"), input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"), input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
input_submit: z.string().optional().default("enter").describe("Submit input"), input_submit: z.string().optional().default("enter").describe("Submit input"),
input_newline: z.string().optional().default("shift+enter,ctrl+j").describe("Insert newline in input"), input_newline: z
.string()
.optional()
.default("shift+enter,ctrl+j")
.describe("Insert newline in input"),
// Deprecated commands // Deprecated commands
switch_mode: z.string().optional().default("none").describe("@deprecated use agent_cycle. Next mode"), switch_mode: z
.string()
.optional()
.default("none")
.describe("@deprecated use agent_cycle. Next mode"),
switch_mode_reverse: z switch_mode_reverse: z
.string() .string()
.optional() .optional()
.default("none") .default("none")
.describe("@deprecated use agent_cycle_reverse. Previous mode"), .describe("@deprecated use agent_cycle_reverse. Previous mode"),
switch_agent: z.string().optional().default("tab").describe("@deprecated use agent_cycle. Next agent"), switch_agent: z
.string()
.optional()
.default("tab")
.describe("@deprecated use agent_cycle. Next agent"),
switch_agent_reverse: z switch_agent_reverse: z
.string() .string()
.optional() .optional()
.default("shift+tab") .default("shift+tab")
.describe("@deprecated use agent_cycle_reverse. Previous agent"), .describe("@deprecated use agent_cycle_reverse. Previous agent"),
file_list: z.string().optional().default("none").describe("@deprecated Currently not available. List files"), file_list: z
.string()
.optional()
.default("none")
.describe("@deprecated Currently not available. List files"),
file_close: z.string().optional().default("none").describe("@deprecated Close file"), file_close: z.string().optional().default("none").describe("@deprecated Close file"),
file_search: z.string().optional().default("none").describe("@deprecated Search file"), file_search: z.string().optional().default("none").describe("@deprecated Search file"),
file_diff_toggle: z.string().optional().default("none").describe("@deprecated Split/unified diff"), file_diff_toggle: z
messages_previous: z.string().optional().default("none").describe("@deprecated Navigate to previous message"), .string()
messages_next: z.string().optional().default("none").describe("@deprecated Navigate to next message"), .optional()
messages_layout_toggle: z.string().optional().default("none").describe("@deprecated Toggle layout"), .default("none")
messages_revert: z.string().optional().default("none").describe("@deprecated use messages_undo. Revert message"), .describe("@deprecated Split/unified diff"),
messages_previous: z
.string()
.optional()
.default("none")
.describe("@deprecated Navigate to previous message"),
messages_next: z
.string()
.optional()
.default("none")
.describe("@deprecated Navigate to next message"),
messages_layout_toggle: z
.string()
.optional()
.default("none")
.describe("@deprecated Toggle layout"),
messages_revert: z
.string()
.optional()
.default("none")
.describe("@deprecated use messages_undo. Revert message"),
}) })
.strict() .strict()
.meta({ .meta({
@@ -446,13 +580,23 @@ export namespace Config {
autoshare: z autoshare: z
.boolean() .boolean()
.optional() .optional()
.describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"), .describe(
"@deprecated Use 'share' field instead. Share newly created sessions automatically",
),
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"), autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"), disabled_providers: z
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(), .array(z.string())
.optional()
.describe("Disable providers that are loaded automatically"),
model: z
.string()
.describe("Model to use in the format of provider/model, eg anthropic/claude-2")
.optional(),
small_model: z small_model: z
.string() .string()
.describe("Small model to use for tasks like title generation in the format of provider/model") .describe(
"Small model to use for tasks like title generation in the format of provider/model",
)
.optional(), .optional(),
username: z username: z
.string() .string()
@@ -508,7 +652,10 @@ export namespace Config {
) )
.optional() .optional()
.describe("Custom provider configurations and model overrides"), .describe("Custom provider configurations and model overrides"),
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"), mcp: z
.record(z.string(), Mcp)
.optional()
.describe("MCP (Model Context Protocol) server configurations"),
formatter: z formatter: z
.record( .record(
z.string(), z.string(),
@@ -552,7 +699,10 @@ export namespace Config {
error: "For custom LSP servers, 'extensions' array is required.", error: "For custom LSP servers, 'extensions' array is required.",
}, },
), ),
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"), instructions: z
.array(z.string())
.optional()
.describe("Additional instruction files or patterns to include"),
layout: Layout.optional().describe("@deprecated Always uses stretch layout."), layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
permission: z permission: z
.object({ .object({
@@ -586,7 +736,10 @@ export namespace Config {
.optional(), .optional(),
}) })
.optional(), .optional(),
chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"), chatMaxRetries: z
.number()
.optional()
.describe("Number of retries for chat completions on failure"),
disable_paste_summary: z.boolean().optional(), disable_paste_summary: z.boolean().optional(),
}) })
.optional(), .optional(),
@@ -616,7 +769,10 @@ export namespace Config {
if (provider && model) result.model = `${provider}/${model}` if (provider && model) result.model = `${provider}/${model}`
result["$schema"] = "https://opencode.ai/config.json" result["$schema"] = "https://opencode.ai/config.json"
result = mergeDeep(result, rest) result = mergeDeep(result, rest)
await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2)) await Bun.write(
path.join(Global.Path.config, "config.json"),
JSON.stringify(result, null, 2),
)
await fs.unlink(path.join(Global.Path.config, "config")) await fs.unlink(path.join(Global.Path.config, "config"))
}) })
.catch(() => {}) .catch(() => {})
@@ -655,7 +811,9 @@ export namespace Config {
if (filePath.startsWith("~/")) { if (filePath.startsWith("~/")) {
filePath = path.join(os.homedir(), filePath.slice(2)) filePath = path.join(os.homedir(), filePath.slice(2))
} }
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath) const resolvedPath = path.isAbsolute(filePath)
? filePath
: path.resolve(configDir, filePath)
const fileContent = ( const fileContent = (
await Bun.file(resolvedPath) await Bun.file(resolvedPath)
.text() .text()

View File

@@ -167,7 +167,7 @@ export namespace MCP {
return return
} }
const result = await withTimeout(mcpClient.tools(), 5000).catch(() => {}) const result = await withTimeout(mcpClient.tools(), mcp.timeout ?? 5000).catch(() => { })
if (!result) { if (!result) {
log.warn("mcp client verification failed, dropping client", { name }) log.warn("mcp client verification failed, dropping client", { name })
return return

View File

@@ -56,6 +56,7 @@ export namespace SessionPrompt {
const log = Log.create({ service: "session.prompt" }) const log = Log.create({ service: "session.prompt" })
export const OUTPUT_TOKEN_MAX = 32_000 export const OUTPUT_TOKEN_MAX = 32_000
const MAX_RETRIES = 10 const MAX_RETRIES = 10
const DOOM_LOOP_THRESHOLD = 3
export const Event = { export const Event = {
Idle: Bus.event( Idle: Bus.event(
@@ -360,21 +361,21 @@ export namespace SessionPrompt {
const stop = await SessionRetry.sleep(delayMs, abort.signal) const stop = await SessionRetry.sleep(delayMs, abort.signal)
.then(() => false) .then(() => false)
.catch((error) => { .catch((error) => {
let err = error
if (error instanceof DOMException && error.name === "AbortError") { if (error instanceof DOMException && error.name === "AbortError") {
const err = new MessageV2.AbortedError( err = new MessageV2.AbortedError(
{ message: error.message }, { message: error.message },
{ {
cause: error, cause: error,
}, },
).toObject() ).toObject()
}
result.info.error = err result.info.error = err
Bus.publish(Session.Event.Error, { Bus.publish(Session.Event.Error, {
sessionID: result.info.sessionID, sessionID: result.info.sessionID,
error: result.info.error, error: result.info.error,
}) })
return true return true
}
throw error
}) })
if (stop) break if (stop) break
@@ -1068,6 +1069,32 @@ export namespace SessionPrompt {
metadata: value.providerMetadata, metadata: value.providerMetadata,
}) })
toolcalls[value.toolCallId] = part as MessageV2.ToolPart toolcalls[value.toolCallId] = part as MessageV2.ToolPart
const parts = await Session.getParts(assistantMsg.id)
const lastThree = parts.slice(-DOOM_LOOP_THRESHOLD)
if (
lastThree.length === DOOM_LOOP_THRESHOLD &&
lastThree.every(
(p) =>
p.type === "tool" &&
p.tool === value.toolName &&
p.state.status !== "pending" &&
JSON.stringify(p.state.input) === JSON.stringify(value.input),
)
) {
await Permission.ask({
type: "doom-loop",
pattern: value.toolName,
sessionID: assistantMsg.sessionID,
messageID: assistantMsg.id,
callID: value.toolCallId,
title: `Possible doom loop: "${value.toolName}" called ${DOOM_LOOP_THRESHOLD} times with identical arguments`,
metadata: {
tool: value.toolName,
input: value.input,
},
})
}
} }
break break
} }

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "0.15.28", "version": "0.15.29",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsgo --noEmit", "typecheck": "tsgo --noEmit",

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "0.15.28", "version": "0.15.29",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsgo --noEmit", "typecheck": "tsgo --noEmit",

View File

@@ -275,6 +275,10 @@ export type McpLocalConfig = {
* Enable or disable the MCP server on startup * Enable or disable the MCP server on startup
*/ */
enabled?: boolean enabled?: boolean
/**
* Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.
*/
timeout?: number
} }
export type McpRemoteConfig = { export type McpRemoteConfig = {
@@ -296,6 +300,10 @@ export type McpRemoteConfig = {
headers?: { headers?: {
[key: string]: string [key: string]: string
} }
/**
* Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.
*/
timeout?: number
} }
/** /**

View File

@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "0.15.28", "version": "0.15.29",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "bun run src/index.ts", "dev": "bun run src/index.ts",

View File

@@ -13,9 +13,11 @@ import (
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"github.com/sst/opencode-sdk-go" "github.com/sst/opencode-sdk-go"
"github.com/sst/opencode-sdk-go/option" "github.com/sst/opencode-sdk-go/option"
"github.com/sst/opencode-sdk-go/packages/ssestream"
"github.com/sst/opencode/internal/api" "github.com/sst/opencode/internal/api"
"github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/clipboard" "github.com/sst/opencode/internal/clipboard"
"github.com/sst/opencode/internal/decoders"
"github.com/sst/opencode/internal/tui" "github.com/sst/opencode/internal/tui"
"github.com/sst/opencode/internal/util" "github.com/sst/opencode/internal/util"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
@@ -61,6 +63,11 @@ func main() {
} }
} }
// Register custom SSE decoder to handle large events (>32MB)
// This is a workaround for the bufio.Scanner token size limit in the auto-generated SDK
// See: packages/tui/internal/decoders/decoder.go
ssestream.RegisterDecoder("text/event-stream", decoders.NewUnboundedDecoder)
httpClient := opencode.NewClient( httpClient := opencode.NewClient(
option.WithBaseURL(url), option.WithBaseURL(url),
) )

View File

@@ -504,7 +504,11 @@ func renderToolDetails(
base := styles.NewStyle().Background(backgroundColor) base := styles.NewStyle().Background(backgroundColor)
text := base.Foreground(t.Text()).Bold(true).Render text := base.Foreground(t.Text()).Bold(true).Render
muted := base.Foreground(t.TextMuted()).Render muted := base.Foreground(t.TextMuted()).Render
if permission.Type == "doom-loop" {
permissionContent = permission.Title + "\n\n"
} else {
permissionContent = "Permission required to run this tool:\n\n" permissionContent = "Permission required to run this tool:\n\n"
}
permissionContent += text( permissionContent += text(
"enter ", "enter ",
) + muted( ) + muted(

View File

@@ -656,6 +656,8 @@ func (m *messagesComponent) renderView() tea.Cmd {
case nil: case nil:
case opencode.AssistantMessageErrorMessageOutputLengthError: case opencode.AssistantMessageErrorMessageOutputLengthError:
error = "Message output length exceeded" error = "Message output length exceeded"
case opencode.AssistantMessageErrorAPIError:
error = err.Data.Message
case opencode.ProviderAuthError: case opencode.ProviderAuthError:
error = err.Data.Message error = err.Data.Message
case opencode.MessageAbortedError: case opencode.MessageAbortedError:

View File

@@ -0,0 +1,118 @@
package decoders
import (
"bufio"
"bytes"
"io"
"github.com/sst/opencode-sdk-go/packages/ssestream"
)
// UnboundedDecoder is an SSE decoder that uses bufio.Reader instead of bufio.Scanner
// to avoid the 32MB token size limit. This is a workaround for large SSE events until
// the upstream Stainless SDK is fixed.
//
// This decoder handles SSE events of unlimited size by reading line-by-line with
// bufio.Reader.ReadBytes('\n'), which dynamically grows the buffer as needed.
type UnboundedDecoder struct {
reader *bufio.Reader
closer io.ReadCloser
evt ssestream.Event
err error
}
// NewUnboundedDecoder creates a new unbounded SSE decoder with a 1MB initial buffer size
func NewUnboundedDecoder(rc io.ReadCloser) ssestream.Decoder {
reader := bufio.NewReaderSize(rc, 1024*1024) // 1MB initial buffer
return &UnboundedDecoder{
reader: reader,
closer: rc,
}
}
// Next reads and decodes the next SSE event from the stream
func (d *UnboundedDecoder) Next() bool {
if d.err != nil {
return false
}
event := ""
data := bytes.NewBuffer(nil)
for {
line, err := d.reader.ReadBytes('\n')
if err != nil {
if err == io.EOF && len(line) == 0 {
return false
}
if err != io.EOF {
d.err = err
return false
}
}
// Remove trailing newline characters
line = bytes.TrimRight(line, "\r\n")
// Empty line indicates end of event
if len(line) == 0 {
if data.Len() > 0 || event != "" {
d.evt = ssestream.Event{
Type: event,
Data: data.Bytes(),
}
return true
}
continue
}
// Skip comments (lines starting with ':')
if line[0] == ':' {
continue
}
// Parse field
name, value, found := bytes.Cut(line, []byte(":"))
if !found {
// Field with no value
continue
}
// Remove leading space from value
if len(value) > 0 && value[0] == ' ' {
value = value[1:]
}
switch string(name) {
case "":
// An empty line in the form ": something" is a comment and should be ignored
continue
case "event":
event = string(value)
case "data":
_, d.err = data.Write(value)
if d.err != nil {
return false
}
_, d.err = data.WriteRune('\n')
if d.err != nil {
return false
}
}
}
}
// Event returns the current event
func (d *UnboundedDecoder) Event() ssestream.Event {
return d.evt
}
// Close closes the underlying reader
func (d *UnboundedDecoder) Close() error {
return d.closer.Close()
}
// Err returns any error that occurred during decoding
func (d *UnboundedDecoder) Err() error {
return d.err
}

View File

@@ -0,0 +1,194 @@
package decoders
import (
"bytes"
"io"
"strings"
"testing"
)
func TestUnboundedDecoder_SmallEvent(t *testing.T) {
data := "event: test\ndata: hello world\n\n"
rc := io.NopCloser(strings.NewReader(data))
decoder := NewUnboundedDecoder(rc)
if !decoder.Next() {
t.Fatal("Expected Next() to return true")
}
evt := decoder.Event()
if evt.Type != "test" {
t.Errorf("Expected event type 'test', got '%s'", evt.Type)
}
if string(evt.Data) != "hello world\n" {
t.Errorf("Expected data 'hello world\\n', got '%s'", string(evt.Data))
}
if decoder.Next() {
t.Error("Expected Next() to return false at end of stream")
}
if err := decoder.Err(); err != nil {
t.Errorf("Expected no error, got %v", err)
}
}
func TestUnboundedDecoder_LargeEvent(t *testing.T) {
// Create a large event (50MB)
size := 50 * 1024 * 1024
largeData := strings.Repeat("x", size)
var buf bytes.Buffer
buf.WriteString("event: large\n")
buf.WriteString("data: ")
buf.WriteString(largeData)
buf.WriteString("\n\n")
rc := io.NopCloser(&buf)
decoder := NewUnboundedDecoder(rc)
if !decoder.Next() {
t.Fatal("Expected Next() to return true")
}
evt := decoder.Event()
if evt.Type != "large" {
t.Errorf("Expected event type 'large', got '%s'", evt.Type)
}
expectedData := largeData + "\n"
if string(evt.Data) != expectedData {
t.Errorf("Data size mismatch: expected %d, got %d", len(expectedData), len(evt.Data))
}
if decoder.Next() {
t.Error("Expected Next() to return false at end of stream")
}
if err := decoder.Err(); err != nil {
t.Errorf("Expected no error, got %v", err)
}
}
func TestUnboundedDecoder_MultipleEvents(t *testing.T) {
data := "event: first\ndata: data1\n\nevent: second\ndata: data2\n\n"
rc := io.NopCloser(strings.NewReader(data))
decoder := NewUnboundedDecoder(rc)
// First event
if !decoder.Next() {
t.Fatal("Expected Next() to return true for first event")
}
evt := decoder.Event()
if evt.Type != "first" {
t.Errorf("Expected event type 'first', got '%s'", evt.Type)
}
if string(evt.Data) != "data1\n" {
t.Errorf("Expected data 'data1\\n', got '%s'", string(evt.Data))
}
// Second event
if !decoder.Next() {
t.Fatal("Expected Next() to return true for second event")
}
evt = decoder.Event()
if evt.Type != "second" {
t.Errorf("Expected event type 'second', got '%s'", evt.Type)
}
if string(evt.Data) != "data2\n" {
t.Errorf("Expected data 'data2\\n', got '%s'", string(evt.Data))
}
// No more events
if decoder.Next() {
t.Error("Expected Next() to return false at end of stream")
}
if err := decoder.Err(); err != nil {
t.Errorf("Expected no error, got %v", err)
}
}
func TestUnboundedDecoder_MultilineData(t *testing.T) {
data := "event: multiline\ndata: line1\ndata: line2\ndata: line3\n\n"
rc := io.NopCloser(strings.NewReader(data))
decoder := NewUnboundedDecoder(rc)
if !decoder.Next() {
t.Fatal("Expected Next() to return true")
}
evt := decoder.Event()
if evt.Type != "multiline" {
t.Errorf("Expected event type 'multiline', got '%s'", evt.Type)
}
expectedData := "line1\nline2\nline3\n"
if string(evt.Data) != expectedData {
t.Errorf("Expected data '%s', got '%s'", expectedData, string(evt.Data))
}
}
func TestUnboundedDecoder_Comments(t *testing.T) {
data := ": this is a comment\nevent: test\n: another comment\ndata: hello\n\n"
rc := io.NopCloser(strings.NewReader(data))
decoder := NewUnboundedDecoder(rc)
if !decoder.Next() {
t.Fatal("Expected Next() to return true")
}
evt := decoder.Event()
if evt.Type != "test" {
t.Errorf("Expected event type 'test', got '%s'", evt.Type)
}
if string(evt.Data) != "hello\n" {
t.Errorf("Expected data 'hello\\n', got '%s'", string(evt.Data))
}
}
func TestUnboundedDecoder_NoEventType(t *testing.T) {
data := "data: hello\n\n"
rc := io.NopCloser(strings.NewReader(data))
decoder := NewUnboundedDecoder(rc)
if !decoder.Next() {
t.Fatal("Expected Next() to return true")
}
evt := decoder.Event()
if evt.Type != "" {
t.Errorf("Expected empty event type, got '%s'", evt.Type)
}
if string(evt.Data) != "hello\n" {
t.Errorf("Expected data 'hello\\n', got '%s'", string(evt.Data))
}
}
func BenchmarkUnboundedDecoder_LargeEvent(b *testing.B) {
// Create a 10MB event for benchmarking
size := 10 * 1024 * 1024
largeData := strings.Repeat("x", size)
var buf bytes.Buffer
buf.WriteString("event: bench\n")
buf.WriteString("data: ")
buf.WriteString(largeData)
buf.WriteString("\n\n")
data := buf.Bytes()
b.ResetTimer()
b.SetBytes(int64(len(data)))
for i := 0; i < b.N; i++ {
rc := io.NopCloser(bytes.NewReader(data))
decoder := NewUnboundedDecoder(rc)
if !decoder.Next() {
b.Fatal("Expected Next() to return true")
}
_ = decoder.Event()
}
}

View File

@@ -656,12 +656,26 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case opencode.EventListResponseEventSessionError: case opencode.EventListResponseEventSessionError:
switch err := msg.Properties.Error.AsUnion().(type) { switch err := msg.Properties.Error.AsUnion().(type) {
case nil: case nil:
// No error details provided
case opencode.ProviderAuthError: case opencode.ProviderAuthError:
slog.Error("Failed to authenticate with provider", "error", err.Data.Message) slog.Error("Failed to authenticate with provider", "error", err.Data.Message)
return a, toast.NewErrorToast("Provider error: " + err.Data.Message) return a, toast.NewErrorToast("Provider error: " + err.Data.Message)
case opencode.UnknownError: case opencode.UnknownError:
slog.Error("Server error", "name", err.Name, "message", err.Data.Message) slog.Error("Server error", "name", err.Name, "message", err.Data.Message)
return a, toast.NewErrorToast(err.Data.Message, toast.WithTitle(string(err.Name))) return a, toast.NewErrorToast(err.Data.Message, toast.WithTitle(string(err.Name)))
case opencode.EventListResponseEventSessionErrorPropertiesErrorAPIError:
slog.Error("API error", "message", err.Data.Message, "statusCode", err.Data.StatusCode)
return a, toast.NewErrorToast(err.Data.Message, toast.WithTitle(string(err.Name)))
case opencode.MessageAbortedError:
// Message was aborted - this is expected when user cancels, so just log it
slog.Debug("Message aborted", "message", err.Data.Message)
case opencode.EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError:
slog.Error("Message output length error")
return a, toast.NewErrorToast("Message output length exceeded limit")
default:
// Handle any unhandled error types
slog.Error("Unhandled session error type", "type", fmt.Sprintf("%T", err))
return a, toast.NewErrorToast("An unexpected error occurred")
} }
case opencode.EventListResponseEventSessionCompacted: case opencode.EventListResponseEventSessionCompacted:
if msg.Properties.SessionID == a.app.Session.ID { if msg.Properties.SessionID == a.app.Session.ID {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "0.15.28", "version": "0.15.29",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/components/index.ts", ".": "./src/components/index.ts",

View File

@@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"type": "module", "type": "module",
"version": "0.15.28", "version": "0.15.29",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",

View File

@@ -1,15 +1,18 @@
--- ---
title: Enterprise title: Enterprise
description: Using OpenCode in your organization. description: Using OpenCode securely in your organization.
--- ---
import config from "../../../config.mjs" import config from "../../../config.mjs"
export const email = `mailto:${config.email}` export const email = `mailto:${config.email}`
OpenCode does not store any of your code or context data. This makes it easy for OpenCode Enterprise is for organizations that want to ensure that their code and data never leaves their infrastructure. It can do this by using a centralized config that integrates with your SSO and internal AI gateway.
you to use OpenCode at your organization.
To get started, we recommend: :::note
OpenCode does not store any of your code or context data.
:::
To get started with OpenCode Enterprise:
1. Do a trial internally with your team. 1. Do a trial internally with your team.
2. **<a href={email}>Contact us</a>** to discuss pricing and implementation options. 2. **<a href={email}>Contact us</a>** to discuss pricing and implementation options.
@@ -18,13 +21,16 @@ To get started, we recommend:
## Trial ## Trial
Since OpenCode is open source and does not store any of your code or context data, your developers can simply [get started](/docs/) and carry out a trial. OpenCode is open source and does not store any of your code or context data, so your developers can simply [get started](/docs/) and carry out a trial.
--- ---
### Data handling ### Data handling
**opencode does not store your code or context data.** All processing happens locally or through direct API calls to your AI provider. **OpenCode does not store your code or context data.** All processing happens locally or through direct API calls to your AI provider.
This means that as long as you are using a provider you trust, or an internal
AI gateway, you can use OpenCode securely.
The only caveat here is the optional `/share` feature. The only caveat here is the optional `/share` feature.
@@ -32,7 +38,7 @@ The only caveat here is the optional `/share` feature.
#### Sharing conversations #### Sharing conversations
If a user enables the `/share` feature, the conversation and the data associated with it are sent to the service we use to host these shares pages at opencode.ai. If a user enables the `/share` feature, the conversation and the data associated with it are sent to the service we use to host these share pages at opencode.ai.
The data is currently served through our CDN's edge network, and is cached on the edge near your users. The data is currently served through our CDN's edge network, and is cached on the edge near your users.
@@ -51,7 +57,54 @@ We recommend you disable this for your trial.
### Code ownership ### Code ownership
**You own all code produced by opencode.** There are no licensing restrictions or ownership claims. **You own all code produced by OpenCode.** There are no licensing restrictions or ownership claims.
---
## Pricing
We use a per-seat model for OpenCode Enterprise. If you have your own LLM gateway, we do not charge for tokens used. For further details about pricing and implementation options, **<a href={email}>contact us</a>**.
---
## Deployment
Once you have completed your trial and you are ready to use OpenCode at
your organization, you can **<a href={email}>contact us</a>** to discuss
pricing and implementation options.
---
### Central Config
We can set up OpenCode to use a single central config for your entire organization.
This centralized config can integrate with your SSO provider and ensures all users access only your internal AI gateway.
---
### SSO integration
Through the central config, OpenCode can integrate with your organization's SSO provider for authentication.
This allows OpenCode to obtain credentials for your internal AI gateway through your existing identity management system.
---
### Internal AI gateway
With the central config, OpenCode can also be configured to use only your internal AI gateway.
You can also disable all other AI providers, ensuring all requests go through your organization's approved infrastructure.
---
### Self-hosting
While we recommend disabling the share pages to ensure your data never leaves
your organization, we can also help you self-host them on your infrastructure.
This is currently on our roadmap. If you're interested, **<a href={email}>let us know</a>**.
--- ---
@@ -60,59 +113,37 @@ We recommend you disable this for your trial.
<details> <details>
<summary>What is OpenCode Enterprise?</summary> <summary>What is OpenCode Enterprise?</summary>
OpenCode Enterprise provides self-hosted deployment options with enhanced security, SSO integration, and dedicated support for organizations that need to maintain full control over their development environment. OpenCode Enterprise is for organizations that want to ensure that their code and data never leaves their infrastructure. It can do this by using a centralized config that integrates with your SSO and internal AI gateway.
</details>
<details>
<summary>How do I get started with OpenCode Enterprise?</summary>
Simply start with an internal trial with your team. OpenCode by default does not store your code or context data, making it easy to get started.
Then **<a href={email}>contact us</a>** to discuss pricing and implementation options.
</details> </details>
<details> <details>
<summary>How does enterprise pricing work?</summary> <summary>How does enterprise pricing work?</summary>
Enterprise pricing is based on team size and deployment requirements. Contact us at <a href={email}>{config.email}</a> for a custom quote based on your organization's needs. We offer per-seat enterprise pricing. If you have your own LLM gateway, we do not charge for tokens used. For further details, **<a href={email}>contact us</a>** for a custom quote based on your organization's needs.
</details> </details>
<details> <details>
<summary>What deployment options are available?</summary> <summary>Is my data secure with OpenCode Enterprise?</summary>
We offer cloud-hosted, on-premises, and air-gapped deployment options. Each includes SSO integration, private package registry support, and customizable security configurations. Yes. OpenCode does not store your code or context data. All processing happens locally or through direct API calls to your AI provider. With central config and SSO integration, your data remains secure within your organization's infrastructure.
</details> </details>
<details> <details>
<summary>Is my data secure with enterprise?</summary> <summary>Can we use our own private NPM registry?</summary>
Yes. OpenCode does not store your code or context data. All processing happens locally or through direct API calls to your AI provider. Enterprise deployments add SSO protection and can be fully air-gapped for maximum security. OpenCode supports private npm registries through Bun's native `.npmrc` file support. If your organization uses a private registry, such as JFrog Artifactory, Nexus, or similar, ensure developers are authenticated before running OpenCode.
</details>
<details>
<summary>Can we integrate with existing tools?</summary>
Yes. OpenCode supports private npm registries, custom authentication providers, and can be integrated into your existing CI/CD pipelines and development workflows.
</details>
---
## Deployment
Once you have completed your trial and you are ready to self-host opencode at
your organization, you can **<a href={email}>contact us</a>** to discuss
pricing and implementation options.
---
### SSO
SSO integration can be implemented for enterprise deployments after your trial.
This will allow your team's session data and shared conversations to be protected
by your enterprise's authentication system.
---
### Private NPM
opencode supports private npm registries through Bun's native `.npmrc` file support. If your organization uses a private registry, such as JFrog Artifactory, Nexus, or similar, ensure developers are authenticated before running opencode.
To set up authentication with your private registry: To set up authentication with your private registry:
@@ -120,11 +151,11 @@ To set up authentication with your private registry:
npm login --registry=https://your-company.jfrog.io/api/npm/npm-virtual/ npm login --registry=https://your-company.jfrog.io/api/npm/npm-virtual/
``` ```
This creates `~/.npmrc` with authentication details. opencode will automatically This creates `~/.npmrc` with authentication details. OpenCode will automatically
pick this up. pick this up.
:::caution :::caution
You must be logged into the private registry before running opencode. You must be logged into the private registry before running OpenCode.
::: :::
Alternatively, you can manually configure a `.npmrc` file: Alternatively, you can manually configure a `.npmrc` file:
@@ -134,11 +165,6 @@ registry=https://your-company.jfrog.io/api/npm/npm-virtual/
//your-company.jfrog.io/api/npm/npm-virtual/:_authToken=${NPM_AUTH_TOKEN} //your-company.jfrog.io/api/npm/npm-virtual/:_authToken=${NPM_AUTH_TOKEN}
``` ```
Developers must be logged into the private registry before running opencode to ensure packages can be installed from your enterprise registry. Developers must be logged into the private registry before running OpenCode to ensure packages can be installed from your enterprise registry.
--- </details>
### Self-hosting
The share feature can be self-hosted and the share pages can be made accessible
only after the user has been authenticated.

View File

@@ -45,12 +45,12 @@ with a unique name. You can refer to that MCP by name when prompting the LLM.
"mcp": { "mcp": {
"name-of-mcp-server": { "name-of-mcp-server": {
// ... // ...
"enabled": true "enabled": true,
}, },
"name-of-other-mcp-server": { "name-of-other-mcp-server": {
// ... // ...
} },
} },
} }
``` ```
@@ -72,10 +72,10 @@ Add local MCP servers using `type` to `"local"` within the MCP object.
"command": ["npx", "-y", "my-mcp-command"], "command": ["npx", "-y", "my-mcp-command"],
"enabled": true, "enabled": true,
"environment": { "environment": {
"MY_ENV_VAR": "my_env_var_value" "MY_ENV_VAR": "my_env_var_value",
} },
} },
} },
} }
``` ```
@@ -91,8 +91,8 @@ For example, here's how I can add the test
"mcp_everything": { "mcp_everything": {
"type": "local", "type": "local",
"command": ["npx", "-y", "@modelcontextprotocol/server-everything"], "command": ["npx", "-y", "@modelcontextprotocol/server-everything"],
} },
} },
} }
``` ```
@@ -107,11 +107,12 @@ use the mcp_everything tool to add the number 3 and 4
Here are all the options for configuring a local MCP server. Here are all the options for configuring a local MCP server.
| Option | Type | Required | Description | | Option | Type | Required | Description |
| ------------- | ------- | -------- | ----------------------------------------------------- | | ------------- | ------- | -------- | ----------------------------------------------------------------------------------- |
| `type` | String | Y | Type of MCP server connection, must be `"local"`. | | `type` | String | Y | Type of MCP server connection, must be `"local"`. |
| `command` | Array | Y | Command and arguments to run the MCP server. | | `command` | Array | Y | Command and arguments to run the MCP server. |
| `environment` | Object | | Environment variables to set when running the server. | | `environment` | Object | | Environment variables to set when running the server. |
| `enabled` | Boolean | | Enable or disable the MCP server on startup. | | `enabled` | Boolean | | Enable or disable the MCP server on startup. |
| `timeout` | Number | | Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds). |
--- ---
@@ -140,11 +141,12 @@ Here the `url` is the URL of the remote MCP server and with the `headers` option
#### Options #### Options
| Option | Type | Required | Description | | Option | Type | Required | Description |
| --------- | ------- | -------- | -------------------------------------------------- | | --------- | ------- | -------- | ----------------------------------------------------------------------------------- |
| `type` | String | Y | Type of MCP server connection, must be `"remote"`. | | `type` | String | Y | Type of MCP server connection, must be `"remote"`. |
| `url` | String | Y | URL of the remote MCP server. | | `url` | String | Y | URL of the remote MCP server. |
| `enabled` | Boolean | | Enable or disable the MCP server on startup. | | `enabled` | Boolean | | Enable or disable the MCP server on startup. |
| `headers` | Object | | Headers to send with the request. | | `headers` | Object | | Headers to send with the request. |
| `timeout` | Number | | Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds). |
--- ---

View File

@@ -2,7 +2,7 @@
"name": "opencode", "name": "opencode",
"displayName": "opencode", "displayName": "opencode",
"description": "opencode for VS Code", "description": "opencode for VS Code",
"version": "0.15.28", "version": "0.15.29",
"publisher": "sst-dev", "publisher": "sst-dev",
"repository": { "repository": {
"type": "git", "type": "git",