From d554e7aaefba37bb62c7fb4711a52841f8003a8a Mon Sep 17 00:00:00 2001 From: Ian Maurer Date: Tue, 11 Nov 2025 17:13:55 -0500 Subject: [PATCH 1/7] fix(cli): always dispose instance on error to prevent hanging; add friendly ProviderModelNotFoundError/InitError messages\n\n- Wrap bootstrap callback in try/finally to guarantee Instance.dispose()\n- Format provider/model errors into actionable guidance (opencode models, config)\n\nRepro: running `opencode run --model typo/claude-haiku-4-5` prints stack and hangs until SIGINT due to lingering watchers.\nFix: disposing Instance tears down watchers/subscriptions, allowing process to exit.\n\nNotes: Prior attempt (#3083) explicitly exited; this approach addresses root cause without forcing exit and improves UX for common misconfigurations. --- packages/opencode/src/cli/bootstrap.ts | 10 +++++++--- packages/opencode/src/cli/error.ts | 12 ++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/bootstrap.ts b/packages/opencode/src/cli/bootstrap.ts index 2114cbc565..d5b646dd5c 100644 --- a/packages/opencode/src/cli/bootstrap.ts +++ b/packages/opencode/src/cli/bootstrap.ts @@ -6,9 +6,13 @@ export async function bootstrap(directory: string, cb: () => Promise) { directory, init: InstanceBootstrap, fn: async () => { - const result = await cb() - await Instance.dispose() - return result + // Ensure we always dispose instance state, even on errors, + // so the CLI does not hang due to lingering watchers/subscriptions. + try { + return await cb() + } finally { + await Instance.dispose() + } }, }) } diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts index 1bc20de323..7a8f40bfe5 100644 --- a/packages/opencode/src/cli/error.ts +++ b/packages/opencode/src/cli/error.ts @@ -1,10 +1,22 @@ import { Config } from "../config/config" import { MCP } from "../mcp" +import { Provider } from "../provider/provider" import { UI } from "./ui" export function FormatError(input: unknown) { if (MCP.Failed.isInstance(input)) return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.` + if (Provider.ModelNotFoundError.isInstance(input)) { + const { providerID, modelID } = input.data + return [ + `Model not found: ${providerID}/${modelID}`, + `Try: \`opencode models\` to list available models`, + `Or check your config (opencode.json) provider/model names`, + ].join("\n") + } + if (Provider.InitError.isInstance(input)) { + return `Failed to initialize provider "${input.data.providerID}". Check credentials and configuration.` + } if (Config.JsonError.isInstance(input)) { return ( `Config file at ${input.data.path} is not valid JSON(C)` + (input.data.message ? `: ${input.data.message}` : "") From 7fd81dd93e4a4e2e5c81fd74fd8c9a0477abe488 Mon Sep 17 00:00:00 2001 From: Ian Maurer Date: Tue, 11 Nov 2025 17:16:47 -0500 Subject: [PATCH 2/7] chore(cli): resolve merge conflict with dev in bootstrap; keep try/finally with explicit return --- packages/opencode/src/cli/bootstrap.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/bootstrap.ts b/packages/opencode/src/cli/bootstrap.ts index d5b646dd5c..7f5c9b7bf4 100644 --- a/packages/opencode/src/cli/bootstrap.ts +++ b/packages/opencode/src/cli/bootstrap.ts @@ -6,10 +6,10 @@ export async function bootstrap(directory: string, cb: () => Promise) { directory, init: InstanceBootstrap, fn: async () => { - // Ensure we always dispose instance state, even on errors, - // so the CLI does not hang due to lingering watchers/subscriptions. + // Guarantee teardown of process-scoped state even on errors try { - return await cb() + const result = await cb() + return result } finally { await Instance.dispose() } From 2be8b2269fea19bf0f3286063b4337a72e795fcb Mon Sep 17 00:00:00 2001 From: Ian Maurer Date: Wed, 12 Nov 2025 10:41:38 -0500 Subject: [PATCH 3/7] =?UTF-8?q?feat(cli):=20suggest=20closest=20provider/m?= =?UTF-8?q?odel=20on=20not=20found=20("Did=20you=20mean=E2=80=A6")\n\nSumm?= =?UTF-8?q?ary\n-=20Add=20fuzzy=20suggestions=20to=20ProviderModelNotFound?= =?UTF-8?q?Error=20with=20up=20to=203=20candidates\n-=20Normalize=20punctu?= =?UTF-8?q?ation=20(e.g.,=204.5=20vs=204-5)=20and=20case=20to=20better=20m?= =?UTF-8?q?atch=20common=20typos\n-=20Support=20model-only=20input=20(no?= =?UTF-8?q?=20provider)=20by=20searching=20across=20all=20providers\n-=20E?= =?UTF-8?q?nhance=20CLI=20error=20formatter=20to=20display=20suggestions?= =?UTF-8?q?=20when=20present\n\nImplementation\n-=20provider.ts:=20use=20f?= =?UTF-8?q?uzzysort;=20add=20normalization=20by=20stripping=20non-alphanum?= =?UTF-8?q?erics;=20search=20by=20key=20for=20robust=20matches\n-=20provid?= =?UTF-8?q?er.ts:=20when=20provider=20is=20unknown=20and=20model=20is=20em?= =?UTF-8?q?pty,=20treat=20token=20as=20unqualified=20model=20and=20search?= =?UTF-8?q?=20across=20all=20providers'=20models;=20otherwise=20suggest=20?= =?UTF-8?q?provider=20matches\n-=20error.ts:=20print=20"Did=20you=20mean:?= =?UTF-8?q?=20,=20=E2=80=A6"=20when=20suggestions=20exist\?= =?UTF-8?q?n\nExamples\n1)=20Typo=20in=20model=20ID\n=20=20=20$=20bun=20ru?= =?UTF-8?q?n=20./src/index.ts=20run=20--model=20anthropic/claude-haiu-4-5?= =?UTF-8?q?=20"hi"\n=20=20=20Error:=20Model=20not=20found:=20anthropic/cla?= =?UTF-8?q?ude-haiu-4-5\n=20=20=20Did=20you=20mean:=20anthropic/claude-hai?= =?UTF-8?q?ku-4-5,=20anthropic/claude-haiku-4-5-20251001\n=20=20=20Try:=20?= =?UTF-8?q?zai-coding-plan/glm-4.5-flash=20zai-coding-plan/glm-4.5=20zai-c?= =?UTF-8?q?oding-plan/glm-4.5-air=20zai-coding-plan/glm-4.5v=20zai-coding-?= =?UTF-8?q?plan/glm-4.6=20opencode/big-pickle=20opencode/grok-code=20anthr?= =?UTF-8?q?opic/claude-opus-4-0=20anthropic/claude-3-5-sonnet-20241022=20a?= =?UTF-8?q?nthropic/claude-opus-4-1=20anthropic/claude-haiku-4-5=20anthrop?= =?UTF-8?q?ic/claude-3-5-sonnet-20240620=20anthropic/claude-3-5-haiku-late?= =?UTF-8?q?st=20anthropic/claude-3-opus-20240229=20anthropic/claude-sonnet?= =?UTF-8?q?-4-5=20anthropic/claude-sonnet-4-5-20250929=20anthropic/claude-?= =?UTF-8?q?sonnet-4-20250514=20anthropic/claude-opus-4-20250514=20anthropi?= =?UTF-8?q?c/claude-3-5-haiku-20241022=20anthropic/claude-3-haiku-20240307?= =?UTF-8?q?=20anthropic/claude-3-7-sonnet-20250219=20anthropic/claude-3-7-?= =?UTF-8?q?sonnet-latest=20anthropic/claude-sonnet-4-0=20anthropic/claude-?= =?UTF-8?q?opus-4-1-20250805=20anthropic/claude-3-sonnet-20240229=20anthro?= =?UTF-8?q?pic/claude-haiku-4-5-20251001=20openai/gpt-4.1-nano=20openai/te?= =?UTF-8?q?xt-embedding-3-small=20openai/gpt-4=20openai/o1-pro=20openai/gp?= =?UTF-8?q?t-4o-2024-05-13=20openai/gpt-4o-2024-08-06=20openai/gpt-4.1-min?= =?UTF-8?q?i=20openai/o3-deep-research=20openai/gpt-3.5-turbo=20openai/tex?= =?UTF-8?q?t-embedding-3-large=20openai/gpt-4-turbo=20openai/o1-preview=20?= =?UTF-8?q?openai/o3-mini=20openai/codex-mini-latest=20openai/gpt-5-nano?= =?UTF-8?q?=20openai/gpt-5-codex=20openai/gpt-4o=20openai/gpt-4.1=20openai?= =?UTF-8?q?/o4-mini=20openai/o1=20openai/gpt-5-mini=20openai/o1-mini=20ope?= =?UTF-8?q?nai/text-embedding-ada-002=20openai/o3-pro=20openai/gpt-4o-2024?= =?UTF-8?q?-11-20=20openai/o3=20openai/o4-mini-deep-research=20openai/gpt-?= =?UTF-8?q?4o-mini=20openai/gpt-5=20openai/gpt-5-pro=20to=20list=20availab?= =?UTF-8?q?le=20models\n=20=20=20Or=20check=20your=20config=20(opencode.js?= =?UTF-8?q?on)=20provider/model=20names\n\n2)=20Dot=20vs=20dash=20(punctua?= =?UTF-8?q?tion=20normalization)\n=20=20=20$=20bun=20run=20./src/index.ts?= =?UTF-8?q?=20run=20--model=20anthropic/claude-haiku-4.5=20"hi"\n=20=20=20?= =?UTF-8?q?Error:=20Model=20not=20found:=20anthropic/claude-haiku-4.5\n=20?= =?UTF-8?q?=20=20Did=20you=20mean:=20anthropic/claude-haiku-4-5,=20anthrop?= =?UTF-8?q?ic/claude-haiku-4-5-20251001\n=20=20=20Try:=20zai-coding-plan/g?= =?UTF-8?q?lm-4.5-flash=20zai-coding-plan/glm-4.5=20zai-coding-plan/glm-4.?= =?UTF-8?q?5-air=20zai-coding-plan/glm-4.5v=20zai-coding-plan/glm-4.6=20op?= =?UTF-8?q?encode/big-pickle=20opencode/grok-code=20anthropic/claude-opus-?= =?UTF-8?q?4-0=20anthropic/claude-3-5-sonnet-20241022=20anthropic/claude-o?= =?UTF-8?q?pus-4-1=20anthropic/claude-haiku-4-5=20anthropic/claude-3-5-son?= =?UTF-8?q?net-20240620=20anthropic/claude-3-5-haiku-latest=20anthropic/cl?= =?UTF-8?q?aude-3-opus-20240229=20anthropic/claude-sonnet-4-5=20anthropic/?= =?UTF-8?q?claude-sonnet-4-5-20250929=20anthropic/claude-sonnet-4-20250514?= =?UTF-8?q?=20anthropic/claude-opus-4-20250514=20anthropic/claude-3-5-haik?= =?UTF-8?q?u-20241022=20anthropic/claude-3-haiku-20240307=20anthropic/clau?= =?UTF-8?q?de-3-7-sonnet-20250219=20anthropic/claude-3-7-sonnet-latest=20a?= =?UTF-8?q?nthropic/claude-sonnet-4-0=20anthropic/claude-opus-4-1-20250805?= =?UTF-8?q?=20anthropic/claude-3-sonnet-20240229=20anthropic/claude-haiku-?= =?UTF-8?q?4-5-20251001=20openai/gpt-4.1-nano=20openai/text-embedding-3-sm?= =?UTF-8?q?all=20openai/gpt-4=20openai/o1-pro=20openai/gpt-4o-2024-05-13?= =?UTF-8?q?=20openai/gpt-4o-2024-08-06=20openai/gpt-4.1-mini=20openai/o3-d?= =?UTF-8?q?eep-research=20openai/gpt-3.5-turbo=20openai/text-embedding-3-l?= =?UTF-8?q?arge=20openai/gpt-4-turbo=20openai/o1-preview=20openai/o3-mini?= =?UTF-8?q?=20openai/codex-mini-latest=20openai/gpt-5-nano=20openai/gpt-5-?= =?UTF-8?q?codex=20openai/gpt-4o=20openai/gpt-4.1=20openai/o4-mini=20opena?= =?UTF-8?q?i/o1=20openai/gpt-5-mini=20openai/o1-mini=20openai/text-embeddi?= =?UTF-8?q?ng-ada-002=20openai/o3-pro=20openai/gpt-4o-2024-11-20=20openai/?= =?UTF-8?q?o3=20openai/o4-mini-deep-research=20openai/gpt-4o-mini=20openai?= =?UTF-8?q?/gpt-5=20openai/gpt-5-pro=20to=20list=20available=20models\n=20?= =?UTF-8?q?=20=20Or=20check=20your=20config=20(opencode.json)=20provider/m?= =?UTF-8?q?odel=20names\n\n3)=20Missing=20provider=20(model-only=20input)\?= =?UTF-8?q?n=20=20=20$=20bun=20run=20./src/index.ts=20run=20--model=20big-?= =?UTF-8?q?pickle=20"hi"\n=20=20=20Error:=20Model=20not=20found:=20big-pic?= =?UTF-8?q?kle/\n=20=20=20Did=20you=20mean:=20opencode/big-pickle\n\n4)=20?= =?UTF-8?q?Correct=20model=20after=20suggestion\n=20=20=20$=20bun=20run=20?= =?UTF-8?q?./src/index.ts=20run=20--model=20opencode/big-pickle=20"hi"\n?= =?UTF-8?q?=20=20=20Hi!=20How=20can=20I=20help=20you=20with=20your=20openc?= =?UTF-8?q?ode=20project=20today=3F\n\nNotes\n-=20Suggestions=20are=20hint?= =?UTF-8?q?s=20only;=20behavior=20is=20unchanged=20(no=20auto-selection).\?= =?UTF-8?q?n-=20This=20runs=20locally=20as=20part=20of=20the=20CLI=20error?= =?UTF-8?q?=20path;=20performance=20impact=20is=20negligible=20(small=20in?= =?UTF-8?q?-memory=20scans).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/opencode/src/cli/error.ts | 5 ++- packages/opencode/src/provider/provider.ts | 41 ++++++++++++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts index 5892344e7d..4f2e0ab827 100644 --- a/packages/opencode/src/cli/error.ts +++ b/packages/opencode/src/cli/error.ts @@ -8,9 +8,12 @@ export function FormatError(input: unknown) { if (MCP.Failed.isInstance(input)) return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.` if (Provider.ModelNotFoundError.isInstance(input)) { - const { providerID, modelID } = input.data + const { providerID, modelID, suggestions } = input.data return [ `Model not found: ${providerID}/${modelID}`, + ...(Array.isArray(suggestions) && suggestions.length + ? ["Did you mean: " + suggestions.join(", ")] + : []), `Try: \`opencode models\` to list available models`, `Or check your config (opencode.json) provider/model names`, ].join("\n") diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index e30576bf7c..5e3da288c1 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1,4 +1,5 @@ import z from "zod" +import fuzzysort from "fuzzysort" import path from "path" import { Config } from "../config/config" import { mergeDeep, sortBy } from "remeda" @@ -522,9 +523,44 @@ export namespace Provider { }) const provider = s.providers[providerID] - if (!provider) throw new ModelNotFoundError({ providerID, modelID }) + if (!provider) { + let suggestions: string[] = [] + const normalize = (str: string) => str.toLowerCase().replace(/[^a-z0-9]/g, "") + if (!modelID || modelID.trim() === "") { + // Treat single-token input as an unqualified model; search across all providers' models. + const q = normalize(providerID) + const entries: { combo: string; norm: string }[] = [] + for (const [pid, prov] of Object.entries(s.providers)) { + for (const mid of Object.keys(prov.info.models)) { + entries.push({ combo: pid + "/" + mid, norm: normalize(mid) }) + } + } + const byNorm = fuzzysort.go(q, entries as any, { limit: 5, key: "norm" }).map((r: any) => r.obj.combo) + const combos = entries.map((e) => e.combo) + const byRaw = fuzzysort.go(providerID, combos, { limit: 5 }).map((r) => r.target) + suggestions = Array.from(new Set([...byNorm, ...byRaw])).slice(0, 3) + } else { + const providerSuggestions = fuzzysort + .go(providerID, Object.keys(s.providers), { limit: 3 }) + .map((r) => r.target + "/" + modelID) + suggestions = providerSuggestions + } + throw new ModelNotFoundError({ providerID, modelID, suggestions }) + } const info = provider.info.models[modelID] - if (!info) throw new ModelNotFoundError({ providerID, modelID }) + if (!info) { + const candidates = Object.keys(provider.info.models) + // Normalize punctuation differences like '-' vs '.' by stripping non-alphanumerics + const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, "") + const corpus = candidates.map((raw) => ({ raw, norm: normalize(raw) })) + const query = normalize(modelID) + const results = fuzzysort.go(query, corpus as any, { limit: 5, key: "norm" }) + const ranked = results.map((r) => ("obj" in r ? (r as any).obj.raw : (r as any).target)) as string[] + const fallback = fuzzysort.go(modelID, candidates, { limit: 5 }).map((r) => r.target) + const merged = Array.from(new Set([...ranked, ...fallback])) + const suggestions = merged.slice(0, 3).map((m) => providerID + "/" + m) + throw new ModelNotFoundError({ providerID, modelID, suggestions }) + } const sdk = await getSDK(provider.info, info) try { @@ -658,6 +694,7 @@ export namespace Provider { z.object({ providerID: z.string(), modelID: z.string(), + suggestions: z.array(z.string()).optional(), }), ) From cbffbcdd3dfee428355b009aea5e14a645a771b0 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 12 Nov 2025 15:43:30 +0000 Subject: [PATCH 4/7] chore: format code --- packages/opencode/src/cli/error.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts index 4f2e0ab827..dfcc360819 100644 --- a/packages/opencode/src/cli/error.ts +++ b/packages/opencode/src/cli/error.ts @@ -11,9 +11,7 @@ export function FormatError(input: unknown) { const { providerID, modelID, suggestions } = input.data return [ `Model not found: ${providerID}/${modelID}`, - ...(Array.isArray(suggestions) && suggestions.length - ? ["Did you mean: " + suggestions.join(", ")] - : []), + ...(Array.isArray(suggestions) && suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []), `Try: \`opencode models\` to list available models`, `Or check your config (opencode.json) provider/model names`, ].join("\n") From bd198d8550ac0ace785613ed694b3903990748bb Mon Sep 17 00:00:00 2001 From: Ian Maurer Date: Wed, 12 Nov 2025 11:13:24 -0500 Subject: [PATCH 5/7] fix(cli): robust suggestions for unknown provider and model-only input; ignore bun build artifacts --- packages/opencode/.gitignore | 1 + packages/opencode/src/provider/provider.ts | 41 +++++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/opencode/.gitignore b/packages/opencode/.gitignore index e057ca61f9..a389d213b0 100644 --- a/packages/opencode/.gitignore +++ b/packages/opencode/.gitignore @@ -2,3 +2,4 @@ research dist gen app.log +*.bun-build diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 5e3da288c1..c177514a36 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -526,6 +526,19 @@ export namespace Provider { if (!provider) { let suggestions: string[] = [] const normalize = (str: string) => str.toLowerCase().replace(/[^a-z0-9]/g, "") + const levenshtein = (a: string, b: string) => { + const m = a.length, n = b.length + const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)) + for (let i = 0; i <= m; i++) dp[i][0] = i + for (let j = 0; j <= n; j++) dp[0][j] = j + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + const cost = a[i - 1] === b[j - 1] ? 0 : 1 + dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost) + } + } + return dp[m][n] + } if (!modelID || modelID.trim() === "") { // Treat single-token input as an unqualified model; search across all providers' models. const q = normalize(providerID) @@ -538,11 +551,31 @@ export namespace Provider { const byNorm = fuzzysort.go(q, entries as any, { limit: 5, key: "norm" }).map((r: any) => r.obj.combo) const combos = entries.map((e) => e.combo) const byRaw = fuzzysort.go(providerID, combos, { limit: 5 }).map((r) => r.target) - suggestions = Array.from(new Set([...byNorm, ...byRaw])).slice(0, 3) + let merged = Array.from(new Set([...byNorm, ...byRaw])) + if (merged.length === 0) { + // fallback to edit distance on normalized mid + const scored = entries + .map((e) => ({ combo: e.combo, d: levenshtein(q, e.norm) })) + .sort((a, b) => a.d - b.d) + .slice(0, 3) + .map((x) => x.combo) + merged = scored + } + suggestions = merged.slice(0, 3) } else { - const providerSuggestions = fuzzysort - .go(providerID, Object.keys(s.providers), { limit: 3 }) - .map((r) => r.target + "/" + modelID) + const pcands = Object.keys(s.providers) + const corpus = pcands.map((raw) => ({ raw, norm: normalize(raw) })) + const q = normalize(providerID) + const hits = fuzzysort.go(q, corpus as any, { limit: 5, key: "norm" }) + let ranked = hits.map((r: any) => r.obj.raw) + if (ranked.length === 0) { + ranked = pcands + .map((p) => ({ p, d: levenshtein(q, normalize(p)) })) + .sort((a, b) => a.d - b.d) + .slice(0, 3) + .map((x) => x.p) + } + const providerSuggestions = ranked.map((r) => r + "/" + modelID) suggestions = providerSuggestions } throw new ModelNotFoundError({ providerID, modelID, suggestions }) From 2d7ba43a215e1ea1f0c1672f12e9af3e0f9600ba Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 12 Nov 2025 16:14:12 +0000 Subject: [PATCH 6/7] chore: format code --- packages/opencode/src/provider/provider.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index c177514a36..ad90304e83 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -527,7 +527,8 @@ export namespace Provider { let suggestions: string[] = [] const normalize = (str: string) => str.toLowerCase().replace(/[^a-z0-9]/g, "") const levenshtein = (a: string, b: string) => { - const m = a.length, n = b.length + const m = a.length, + n = b.length const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)) for (let i = 0; i <= m; i++) dp[i][0] = i for (let j = 0; j <= n; j++) dp[0][j] = j From 715265de4be02b1a034d136430317c3032fdc038 Mon Sep 17 00:00:00 2001 From: Ian Maurer Date: Wed, 12 Nov 2025 11:33:59 -0500 Subject: [PATCH 7/7] Update .gitignore to remove '*.bun-build' Remove '*.bun-build' from the .gitignore file --- packages/opencode/.gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/opencode/.gitignore b/packages/opencode/.gitignore index a389d213b0..e057ca61f9 100644 --- a/packages/opencode/.gitignore +++ b/packages/opencode/.gitignore @@ -2,4 +2,3 @@ research dist gen app.log -*.bun-build