diff --git a/.opencode/agents/coding-engineer.md b/.opencode/agents/coding-engineer.md
new file mode 100644
index 00000000000..f8ce0756a60
--- /dev/null
+++ b/.opencode/agents/coding-engineer.md
@@ -0,0 +1,71 @@
+---
+name: coding-engineer
+description: "Use this agent when you need an autonomous expert to design, implement, refactor, or debug code in response to a concrete programming request. This includes writing new functions, features, or services; modifying existing code; fixing bugs; improving performance or readability; and answering implementation-level questions with runnable code.
+
+Examples:
+
+
+Context: The user is working on a backend service and has just described a new feature to implement.
+user: \"I need a function that validates JWT tokens and extracts the user id\"
+assistant: \"I’m going to use the Task tool to launch the coding-engineer agent to implement this correctly.\"
+
+The user is asking for production-ready code. Use the coding-engineer agent to design and implement the function with proper validation, error handling, and best practices.
+
+
+
+
+Context: The user has written some code and reports a bug.
+user: \"This Python script crashes when the input file is empty. Can you fix it?\"
+assistant: \"I will use the Task tool to launch the coding-engineer agent to diagnose and fix the bug.\"
+
+The request involves debugging and modifying code. Use the coding-engineer agent to identify the root cause, apply a fix, and explain the change.
+
+
+
+
+Context: The user is iterating quickly and wants proactive help while coding.
+user: \"Here’s my initial implementation of a cache layer…\"
+assistant: \"I’m going to use the Task tool to launch the coding-engineer agent to review and improve this implementation.\"
+
+Because the user is actively coding, proactively use the coding-engineer agent to suggest improvements, edge-case handling, and optimizations.
+
+"
+mode: subagent
+---
+
+You are an elite software engineer with deep, practical experience across multiple programming languages, frameworks, and system architectures. Your role is to design, implement, debug, and refine code that is correct, maintainable, and aligned with best practices.
+
+Core Responsibilities:
+- Translate user requirements into clear, working code.
+- Write idiomatic, production-quality implementations in the requested language.
+- Debug and fix issues in recently written or provided code (assume only the shown code unless told otherwise).
+- Refactor code to improve clarity, performance, or reliability when appropriate.
+
+Operational Guidelines:
+- Always clarify ambiguous requirements before making assumptions that could affect correctness.
+- Prefer simple, readable solutions unless constraints justify complexity.
+- Follow established conventions and idioms of the target language and framework.
+- If project-specific standards or patterns are provided (e.g., via CLAUDE.md), strictly adhere to them.
+
+Methodology:
+1. Restate the goal in your own words to ensure understanding (briefly, when helpful).
+2. Identify edge cases, constraints, and failure modes.
+3. Implement the solution step by step, with clear structure.
+4. Add error handling, input validation, and comments where they add value.
+5. Perform a quick self-review for correctness, clarity, and consistency.
+
+Quality Control:
+- Verify that the code compiles or runs logically.
+- Check for common pitfalls (null/empty inputs, off-by-one errors, resource leaks, security issues).
+- Ensure naming is clear and intent-revealing.
+
+Output Expectations:
+- Provide complete, runnable code snippets when possible.
+- Clearly indicate where code should be integrated if it is partial.
+- Explain non-obvious decisions succinctly.
+
+Fallbacks and Escalation:
+- If requirements are incomplete or conflicting, pause and ask targeted questions.
+- If multiple valid approaches exist, briefly compare them and recommend one.
+
+You are proactive, precise, and pragmatic. Your goal is not just to make the code work, but to make it robust and easy for other engineers to understand and maintain.
\ No newline at end of file
diff --git a/.opencode/agents/debugging.md b/.opencode/agents/debugging.md
new file mode 100644
index 00000000000..148f443548a
--- /dev/null
+++ b/.opencode/agents/debugging.md
@@ -0,0 +1,74 @@
+---
+name: debugging
+description: "Specialized agent for identifying and fixing issues by creating plans and deploying sub-agents. Use this agent when you have a bug or a complex technical issue that requires systematic troubleshooting and repair."
+model: openai/codex-1
+mode: all
+permission:
+ read:
+ "*": allow
+ write:
+ "*": allow
+ edit:
+ "*": allow
+---
+
+# Role
+
+You are a high-level Debugging Orchestrator. Your primary goal is to analyze bug reports or technical issues, develop a step-by-step resolution plan, and oversee the execution of that plan.
+
+# Planning Mode (REQUIRED FIRST STEP)
+
+**Before taking ANY action, you MUST enter Planning Mode:**
+
+1. **Scan the Context**: Examine the code around the user's cursor position and any files mentioned in the query.
+2. **Identify Errors**: Look for:
+ - Syntax errors
+ - Type errors
+ - Logic errors
+ - Missing imports or dependencies
+ - Incorrect function calls or parameters
+ - Edge cases not handled
+3. **Report Findings**: Present a clear summary to the user:
+ - What file(s) you examined
+ - What errors or issues you found
+ - Your initial assessment of the root cause
+4. **Propose a Plan**: Only after reporting your findings, propose a numbered action plan.
+
+**Do NOT make changes until your findings are acknowledged by the user.**
+
+# Methodology (After Planning)
+
+1. **Execute the Plan**: Once the user confirms, proceed step-by-step.
+2. **Use Tools Directly**: For simple fixes, use the read, write, and edit tools yourself.
+3. **Deploy Sub-agents for Complex Tasks**: Use the `task` tool to launch the `coding-engineer` agent for larger refactors or implementations.
+4. **Verify**: After each change, verify the fix resolves the issue.
+
+# Guidelines
+
+- **Always start in Planning Mode** - scan first, act second.
+- Be systematic and thorough.
+- Do not make assumptions; verify dependencies and actual code behavior.
+- Communicate findings clearly before proposing solutions.
+- Ensure all fixes are tested or logically verified.
+
+# Example Workflow
+
+**User**: "My script is throwing a DivisionByZero error in the math_utils.py file."
+
+**Response (Planning Mode)**:
+"Let me scan the code to identify the issue.
+
+**Files Examined**: `math_utils.py`
+
+**Findings**:
+- Line 42: `result = numerator / denominator` - No check for zero denominator
+- The `calculate_ratio` function receives user input without validation
+
+**Root Cause**: Missing zero-check before division operation.
+
+**Proposed Plan**:
+1. Add a validation check for zero denominator in `calculate_ratio`
+2. Return an appropriate error or default value when denominator is zero
+3. Verify the fix handles the edge case
+
+Should I proceed with this plan?"
diff --git a/check_dirs.ts b/check_dirs.ts
new file mode 100644
index 00000000000..be7dab1bcb3
--- /dev/null
+++ b/check_dirs.ts
@@ -0,0 +1,15 @@
+import { Config } from "./packages/opencode/src/config/config"
+import { Instance } from "./packages/opencode/src/project/instance"
+import { InstanceBootstrap } from "./packages/opencode/src/project/bootstrap"
+
+async function main() {
+ await Instance.provide({
+ directory: "/Users/ash/Desktop/Personal_Projects/opencode",
+ init: InstanceBootstrap,
+ fn: async () => {
+ console.log("Directories:", await Config.directories())
+ }
+ })
+}
+
+main().catch(console.error)
diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index 17695583867..ff7d7eea8d9 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/packages/opencode/src/agent/agent.ts
@@ -13,8 +13,16 @@ import PROMPT_SUMMARY from "./prompt/summary.txt"
import PROMPT_TITLE from "./prompt/title.txt"
import { PermissionNext } from "@/permission/next"
import { mergeDeep, pipe, sortBy, values } from "remeda"
+import { ConfigMarkdown } from "../config/markdown"
+import { Filesystem } from "../util/filesystem"
+import { Global } from "@/global"
+import fs, { exists } from "fs/promises"
+import path from "path"
+import { Flag } from "@/flag/flag"
+import { Log } from "../util/log"
export namespace Agent {
+ const log = Log.create({ service: "agent" })
export const Info = z
.object({
name: z.string(),
@@ -41,6 +49,9 @@ export namespace Agent {
})
export type Info = z.infer
+ const OPENCODE_AGENT_GLOB = new Bun.Glob("{agent,agents}/**/*.md")
+ const CLAUDE_AGENT_GLOB = new Bun.Glob("agents/**/*.md")
+
const state = Instance.state(async () => {
const cfg = await Config.get()
@@ -182,25 +193,26 @@ export namespace Agent {
},
}
- for (const [key, value] of Object.entries(cfg.agent ?? {})) {
+ const apply = (key: string, value: any, prompt?: string) => {
if (value.disable) {
delete result[key]
- continue
+ return
}
let item = result[key]
if (!item)
item = result[key] = {
name: key,
mode: "all",
+ hidden: false,
permission: PermissionNext.merge(defaults, user),
options: {},
native: false,
}
if (value.model) item.model = Provider.parseModel(value.model)
- item.prompt = value.prompt ?? item.prompt
+ item.prompt = prompt ?? value.prompt ?? item.prompt
item.description = value.description ?? item.description
item.temperature = value.temperature ?? item.temperature
- item.topP = value.top_p ?? item.topP
+ item.topP = value.top_p ?? value.topP ?? item.topP
item.mode = value.mode ?? item.mode
item.color = value.color ?? item.color
item.hidden = value.hidden ?? item.hidden
@@ -210,6 +222,73 @@ export namespace Agent {
item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {}))
}
+ for (const [key, value] of Object.entries(cfg.agent ?? {})) {
+ apply(key, value)
+ }
+
+ const addAgent = async (match: string) => {
+ const md = await ConfigMarkdown.parse(match)
+ if (!md) return
+ let name = path.basename(match, ".md")
+ const patterns = ["/.opencode/agents/", "/.opencode/agent/", "/agents/", "/agent/"]
+ const pattern = patterns.find((p) => match.includes(p))
+ const agentFolderPath = pattern ? match.split(pattern)[1] : name + ".md"
+
+ if (agentFolderPath.includes("/")) {
+ const relativePath = agentFolderPath.replace(".md", "")
+ const pathParts = relativePath.split("/")
+ name = pathParts.slice(0, -1).join("/") + "/" + pathParts[pathParts.length - 1]
+ }
+ apply(name, md.data, md.content.trim())
+ }
+
+ // Scan .claude/agents/ directories (project-level)
+ const claudeDirs = await Array.fromAsync(
+ Filesystem.up({
+ targets: [".claude"],
+ start: Instance.directory,
+ stop: Instance.worktree,
+ }),
+ )
+ // Also include global ~/.claude/agents/
+ const globalClaude = `${Global.Path.home}/.claude`
+ if (await exists(globalClaude)) {
+ claudeDirs.push(globalClaude)
+ }
+
+ if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT) {
+ for (const dir of claudeDirs) {
+ const matches = await Array.fromAsync(
+ CLAUDE_AGENT_GLOB.scan({
+ cwd: dir,
+ absolute: true,
+ onlyFiles: true,
+ followSymlinks: true,
+ dot: true,
+ }),
+ ).catch((error) => {
+ log.error("failed .claude directory scan for agents", { dir, error })
+ return []
+ })
+
+ for (const match of matches) {
+ await addAgent(match)
+ }
+ }
+ }
+
+ // Scan .opencode/agent/ directories
+ for (const dir of await Config.directories()) {
+ for await (const match of OPENCODE_AGENT_GLOB.scan({
+ cwd: dir,
+ absolute: true,
+ onlyFiles: true,
+ followSymlinks: true,
+ })) {
+ await addAgent(match)
+ }
+ }
+
// Ensure Truncate.DIR is allowed unless explicitly configured
for (const name in result) {
const agent = result[name]
@@ -281,4 +360,35 @@ export namespace Agent {
})
return result.object
}
+
+ export async function save(input: {
+ name: string
+ description?: string
+ systemPrompt: string
+ model?: { providerID: string; modelID: string }
+ mode?: "subagent" | "primary" | "all"
+ }) {
+ const dir = path.join(Instance.directory, ".opencode", "agents")
+ const filePath = path.join(dir, `${input.name}.md`)
+
+ const yaml = [
+ `name: ${input.name}`,
+ `description: "${input.description || ""}"`,
+ `mode: ${input.mode || "all"}`,
+ input.model ? `model: ${input.model.providerID}/${input.model.modelID}` : null,
+ ]
+ .filter(Boolean)
+ .join("\n")
+
+ const content = `---\n${yaml}\n---\n\n${input.systemPrompt}`
+
+ if (!(await exists(dir))) {
+ await fs.mkdir(dir, { recursive: true })
+ }
+
+ await Bun.write(filePath, content)
+ log.info("saved agent", { path: filePath })
+
+ state.clear()
+ }
}
diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts
index 3dd7bcc35dd..24ef784986f 100644
--- a/packages/opencode/src/cli/cmd/auth.ts
+++ b/packages/opencode/src/cli/cmd/auth.ts
@@ -164,7 +164,7 @@ export const AuthCommand = cmd({
describe: "manage credentials",
builder: (yargs) =>
yargs.command(AuthLoginCommand).command(AuthLogoutCommand).command(AuthListCommand).demandCommand(),
- async handler() {},
+ async handler() { },
})
export const AuthListCommand = cmd({
@@ -228,7 +228,8 @@ export const AuthLoginCommand = cmd({
async fn() {
UI.empty()
prompts.intro("Add credential")
- if (args.url) {
+ // Only treat args.url as a well-known URL if it contains "://" (looks like a URL)
+ if (args.url && args.url.includes("://")) {
const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json() as any)
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
const proc = Bun.spawn({
@@ -251,7 +252,7 @@ export const AuthLoginCommand = cmd({
prompts.outro("Done")
return
}
- await ModelsDev.refresh().catch(() => {})
+ await ModelsDev.refresh().catch(() => { })
const config = await Config.get()
@@ -277,35 +278,51 @@ export const AuthLoginCommand = cmd({
openrouter: 5,
vercel: 6,
}
- let provider = await prompts.autocomplete({
- message: "Select provider",
- maxItems: 8,
- options: [
- ...pipe(
- providers,
- values(),
- sortBy(
- (x) => priority[x.id] ?? 99,
- (x) => x.name ?? x.id,
- ),
- map((x) => ({
- label: x.name,
- value: x.id,
- hint: {
- opencode: "recommended",
- anthropic: "Claude Max or API key",
- openai: "ChatGPT Plus/Pro or API key",
- }[x.id],
- })),
- ),
- {
- value: "other",
- label: "Other",
- },
- ],
- })
- if (prompts.isCancel(provider)) throw new UI.CancelledError()
+ // If a provider name was passed as an argument (not a URL), use it directly
+ let provider: string
+ if (args.url && !args.url.includes("://")) {
+ // Treat args.url as a provider name
+ const matchedProvider = Object.keys(providers).find(
+ (key) => key === args.url || providers[key].name?.toLowerCase() === args.url?.toLowerCase(),
+ )
+ if (matchedProvider) {
+ provider = matchedProvider
+ } else {
+ // Treat as custom provider
+ provider = args.url
+ }
+ } else {
+ const selectedProvider = await prompts.autocomplete({
+ message: "Select provider",
+ maxItems: 8,
+ options: [
+ ...pipe(
+ providers,
+ values(),
+ sortBy(
+ (x) => priority[x.id] ?? 99,
+ (x) => x.name ?? x.id,
+ ),
+ map((x) => ({
+ label: x.name,
+ value: x.id,
+ hint: {
+ opencode: "recommended",
+ anthropic: "Claude Max or API key",
+ openai: "ChatGPT Plus/Pro or API key",
+ }[x.id],
+ })),
+ ),
+ {
+ value: "other",
+ label: "Other",
+ },
+ ],
+ })
+ if (prompts.isCancel(selectedProvider)) throw new UI.CancelledError()
+ provider = selectedProvider
+ }
const plugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider))
if (plugin && plugin.auth) {
@@ -314,13 +331,12 @@ export const AuthLoginCommand = cmd({
}
if (provider === "other") {
- provider = await prompts.text({
+ const customProvider = await prompts.text({
message: "Enter provider id",
validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
})
- if (prompts.isCancel(provider)) throw new UI.CancelledError()
- provider = provider.replace(/^@ai-sdk\//, "")
- if (prompts.isCancel(provider)) throw new UI.CancelledError()
+ if (prompts.isCancel(customProvider)) throw new UI.CancelledError()
+ provider = customProvider.replace(/^@ai-sdk\//, "")
// Check if a plugin provides auth for this custom provider
const customPlugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider))
@@ -337,10 +353,10 @@ export const AuthLoginCommand = cmd({
if (provider === "amazon-bedrock") {
prompts.log.info(
"Amazon Bedrock authentication priority:\n" +
- " 1. Bearer token (AWS_BEARER_TOKEN_BEDROCK or /connect)\n" +
- " 2. AWS credential chain (profile, access keys, IAM roles)\n\n" +
- "Configure via opencode.json options (profile, region, endpoint) or\n" +
- "AWS environment variables (AWS_PROFILE, AWS_REGION, AWS_ACCESS_KEY_ID).",
+ " 1. Bearer token (AWS_BEARER_TOKEN_BEDROCK or /connect)\n" +
+ " 2. AWS credential chain (profile, access keys, IAM roles)\n\n" +
+ "Configure via opencode.json options (profile, region, endpoint) or\n" +
+ "AWS environment variables (AWS_PROFILE, AWS_REGION, AWS_ACCESS_KEY_ID).",
)
}
diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index aa62c6c58ef..1864469d966 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -371,6 +371,15 @@ function App() {
dialog.replace(() => )
},
},
+ {
+ title: "Switch agent",
+ value: "agents",
+ category: "Agent",
+ hidden: true,
+ onSelect: () => {
+ dialog.replace(() => )
+ },
+ },
{
title: "Toggle MCPs",
value: "mcp.list",
@@ -456,7 +465,7 @@ function App() {
title: "Open docs",
value: "docs.open",
onSelect: () => {
- open("https://opencode.ai/docs").catch(() => {})
+ open("https://opencode.ai/docs").catch(() => { })
dialog.clear()
},
category: "System",
@@ -465,7 +474,7 @@ function App() {
title: "Open WebUI",
value: "webui.open",
onSelect: () => {
- open(sdk.url).catch(() => {})
+ open(sdk.url).catch(() => { })
dialog.clear()
},
category: "System",
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-agent-generate.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-agent-generate.tsx
new file mode 100644
index 00000000000..6187ad4a32b
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-agent-generate.tsx
@@ -0,0 +1,141 @@
+import { createSignal } from "solid-js"
+import { useDialog } from "@tui/ui/dialog"
+import { DialogSelect } from "@tui/ui/dialog-select"
+import { DialogPrompt } from "@tui/ui/dialog-prompt"
+import { DialogConfirm } from "@tui/ui/dialog-confirm"
+import { useSDK } from "@tui/context/sdk"
+import { useToast } from "@tui/ui/toast"
+import { useLocal } from "@tui/context/local"
+import { useSync } from "@tui/context/sync"
+
+export function DialogAgentGenerate() {
+ const dialog = useDialog()
+ const sdk = useSDK()
+ const toast = useToast()
+ const local = useLocal()
+ const sync = useSync()
+
+ const onAI = async () => {
+ const description = await DialogPrompt.show(dialog, "Agent Description", {
+ placeholder: "e.g., A Rust expert that focuses on memory safety and async patterns.",
+ })
+ if (!description) {
+ dialog.clear()
+ return
+ }
+
+ toast.show({ message: "Generating agent...", variant: "info" })
+ try {
+ if (!sdk.url) throw new Error("SDK URL is not initialized")
+ const url = new URL("/agent/generate", sdk.url).toString()
+ const headers: Record = {
+ "Content-Type": "application/json",
+ }
+ if (sdk.directory) {
+ headers["x-opencode-directory"] = sdk.directory
+ }
+
+ const response = await sdk.fetch(url, {
+ method: "POST",
+ headers,
+ body: JSON.stringify({ description }),
+ }).catch(err => {
+ throw new Error(`Connection failed to ${url}: ${err.message}`)
+ })
+
+ if (!response.ok) throw new Error("Failed to generate agent")
+ const generated = await response.json()
+
+ const confirmed = await DialogConfirm.show(
+ dialog,
+ "Save Agent?",
+ `Generated agent "${generated.identifier}".\n\nPrompt snippet: ${generated.systemPrompt.slice(0, 100)}...\n\nSave to .opencode/agents/${generated.identifier}.md?`,
+ )
+
+ if (confirmed) {
+ const saveUrl = new URL("/agent/save", sdk.url).toString()
+ const saveResponse = await sdk.fetch(saveUrl, {
+ method: "POST",
+ headers,
+ body: JSON.stringify({
+ name: generated.identifier,
+ description: generated.whenToUse,
+ systemPrompt: generated.systemPrompt,
+ }),
+ }).catch(err => {
+ throw new Error(`Connection failed to ${saveUrl}: ${err.message}`)
+ })
+
+ if (!saveResponse.ok) throw new Error(`Failed to save agent: ${saveResponse.statusText}`)
+ toast.show({ message: `Agent "${generated.identifier}" saved!`, variant: "success" })
+ await sync.bootstrap()
+ }
+ } catch (e) {
+ toast.error(e)
+ } finally {
+ dialog.clear()
+ }
+ }
+
+ const onManual = async () => {
+ const name = await DialogPrompt.show(dialog, "Agent Name", {
+ placeholder: "e.g., rust-expert",
+ })
+ if (!name) {
+ dialog.clear()
+ return
+ }
+
+ try {
+ if (!sdk.url) throw new Error("SDK URL is not initialized")
+ const url = new URL("/agent/save", sdk.url).toString()
+ const headers: Record = {
+ "Content-Type": "application/json",
+ }
+ if (sdk.directory) {
+ headers["x-opencode-directory"] = sdk.directory
+ }
+
+ const saveResponse = await sdk.fetch(url, {
+ method: "POST",
+ headers,
+ body: JSON.stringify({
+ name,
+ systemPrompt: "# Role\n\nYou are a specialized agent...",
+ }),
+ }).catch(err => {
+ throw new Error(`Connection failed to ${url}: ${err.message}`)
+ })
+
+ if (!saveResponse.ok) throw new Error(`Failed to save template: ${saveResponse.statusText}`)
+ toast.show({ message: `Template saved to .opencode/agents/${name}.md`, variant: "success" })
+ await sync.bootstrap()
+ } catch (e) {
+ toast.error(e)
+ } finally {
+ dialog.clear()
+ }
+ }
+
+ return (
+ {
+ if (option.value === "ai") onAI()
+ else onManual()
+ }}
+ />
+ )
+}
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx
index 365a22445b4..006f7380f5a 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx
@@ -2,20 +2,26 @@ import { createMemo } from "solid-js"
import { useLocal } from "@tui/context/local"
import { DialogSelect } from "@tui/ui/dialog-select"
import { useDialog } from "@tui/ui/dialog"
+import { DialogAgentGenerate } from "./dialog-agent-generate"
export function DialogAgent() {
const local = useLocal()
const dialog = useDialog()
- const options = createMemo(() =>
- local.agent.list().map((item) => {
+ const options = createMemo(() => [
+ {
+ value: "create",
+ title: "Create new agent...",
+ description: "Define a new sub-agent behavior (AI or Manual)",
+ },
+ ...local.agent.list().map((item) => {
return {
value: item.name,
title: item.name,
description: item.native ? "native" : item.description,
}
}),
- )
+ ])
return (
{
- local.agent.set(option.value)
- dialog.clear()
+ if (option.value === "create") {
+ dialog.replace(() => )
+ } else {
+ local.agent.set(option.value)
+ dialog.clear()
+ }
}}
/>
)
diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx
index 63f1d9743bf..b1b7a0767b9 100644
--- a/packages/opencode/src/cli/cmd/tui/context/local.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx
@@ -34,7 +34,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
const agent = iife(() => {
- const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
+ const agents = createMemo(() => sync.data.agent.filter((x) => !x.hidden))
const [agentStore, setAgentStore] = createStore<{
current: string
}>({
@@ -132,7 +132,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
if (Array.isArray(x.favorite)) setModelStore("favorite", x.favorite)
if (typeof x.variant === "object" && x.variant !== null) setModelStore("variant", x.variant)
})
- .catch(() => {})
+ .catch(() => { })
.finally(() => {
setModelStore("ready", true)
})
diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx
index 3339e7b00d2..34323805d47 100644
--- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx
@@ -89,6 +89,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
if (timer) clearTimeout(timer)
})
- return { client: sdk, event: emitter, url: props.url }
+ return { client: sdk, event: emitter, url: props.url, directory: props.directory, fetch: props.fetch ?? fetch }
},
})
diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts
index 5e50d38ded0..9e133374b39 100644
--- a/packages/opencode/src/cli/cmd/tui/thread.ts
+++ b/packages/opencode/src/cli/cmd/tui/thread.ts
@@ -18,7 +18,13 @@ type RpcClient = ReturnType>
function createWorkerFetch(client: RpcClient): typeof fetch {
const fn = async (input: RequestInfo | URL, init?: RequestInit): Promise => {
- const request = new Request(input, init)
+ let request: Request
+ try {
+ request = new Request(input, init)
+ } catch (e) {
+ console.error("Failed to create Request in createWorkerFetch:", { input, init, error: e })
+ throw e
+ }
const body = request.body ? await request.text() : undefined
const result = await client.call("fetch", {
url: request.url,
@@ -40,7 +46,7 @@ function createEventSource(client: RpcClient, directory: string): EventSource {
client.on("event", (event) => {
handler(event)
if (event.type === "server.instance.disposed") {
- client.call("subscribe", { directory }).catch(() => {})
+ client.call("subscribe", { directory }).catch(() => { })
}
}),
}
@@ -166,7 +172,7 @@ export const TuiThreadCommand = cmd({
})
setTimeout(() => {
- client.call("checkUpgrade", { directory: cwd }).catch(() => {})
+ client.call("checkUpgrade", { directory: cwd }).catch(() => { })
}, 1000)
await tuiPromise
diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts
index ea88e45f1db..cac491b0089 100644
--- a/packages/opencode/src/cli/cmd/tui/worker.ts
+++ b/packages/opencode/src/cli/cmd/tui/worker.ts
@@ -40,11 +40,17 @@ let server: Bun.Server | undefined
export const rpc = {
async fetch(input: { url: string; method: string; headers: Record; body?: string }) {
- const request = new Request(input.url, {
- method: input.method,
- headers: input.headers,
- body: input.body,
- })
+ let request: Request
+ try {
+ request = new Request(input.url, {
+ method: input.method,
+ headers: input.headers,
+ body: input.body,
+ })
+ } catch (e) {
+ Log.Default.error("failed to create request in rpc.fetch", { url: input.url, error: e })
+ throw e
+ }
const response = await Server.App().fetch(request)
const body = await response.text()
return {
@@ -77,7 +83,7 @@ export const rpc = {
directory: input.directory,
init: InstanceBootstrap,
fn: async () => {
- await upgrade().catch(() => {})
+ await upgrade().catch(() => { })
},
})
},
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index ead3a0149b4..0a78e557777 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -202,11 +202,11 @@ export namespace Config {
{
cwd: dir,
},
- ).catch(() => {})
+ ).catch(() => { })
// Install any additional dependencies defined in the package.json
// This allows local plugins and custom tools to use external packages
- await BunProc.run(["install"], { cwd: dir }).catch(() => {})
+ await BunProc.run(["install"], { cwd: dir }).catch(() => { })
}
const COMMAND_GLOB = new Bun.Glob("{command,commands}/**/*.md")
@@ -262,11 +262,11 @@ export namespace Config {
// Extract relative path from agent folder for nested agents
let agentName = path.basename(item, ".md")
- const agentFolderPath = item.includes("/.opencode/agent/")
- ? item.split("/.opencode/agent/")[1]
- : item.includes("/agent/")
- ? item.split("/agent/")[1]
- : agentName + ".md"
+ const patterns = ["/.opencode/agents/", "/.opencode/agent/", "/agents/", "/agent/"]
+ const pattern = patterns.find(p => item.includes(p))
+ const agentFolderPath = pattern
+ ? item.split(pattern)[1]
+ : agentName + ".md"
// If agent is in a subfolder, include folder path in name
if (agentFolderPath.includes("/")) {
@@ -1077,7 +1077,7 @@ export namespace Config {
await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2))
await fs.unlink(path.join(Global.Path.config, "config"))
})
- .catch(() => {})
+ .catch(() => { })
return result
})
@@ -1172,7 +1172,7 @@ export namespace Config {
const plugin = data.plugin[i]
try {
data.plugin[i] = import.meta.resolve!(plugin, configFilepath)
- } catch (err) {}
+ } catch (err) { }
}
}
return data
diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts
index 4e2b283795d..1ebfb709a15 100644
--- a/packages/opencode/src/plugin/codex.ts
+++ b/packages/opencode/src/plugin/codex.ts
@@ -467,16 +467,26 @@ export async function CodexAuthPlugin(input: PluginInput): Promise {
}
// Rewrite URL to Codex endpoint
- const parsed =
- requestInput instanceof URL
- ? requestInput
- : new URL(typeof requestInput === "string" ? requestInput : requestInput.url)
- const url =
- parsed.pathname.includes("/v1/responses") || parsed.pathname.includes("/chat/completions")
+ let url: URL
+ try {
+ url =
+ requestInput instanceof URL
+ ? requestInput
+ : new URL(
+ typeof requestInput === "string" ? requestInput : (requestInput as any).url,
+ "http://opencode.internal",
+ )
+ } catch (e) {
+ log.error("Failed to parse request URL in Codex plugin", { requestInput, error: e })
+ throw e
+ }
+
+ const finalUrl =
+ url.pathname.includes("/v1/responses") || url.pathname.includes("/chat/completions")
? new URL(CODEX_API_ENDPOINT)
- : parsed
+ : url
- return fetch(url, {
+ return fetch(finalUrl, {
...init,
headers,
})
diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts
index ddaa90f1e2b..bbcb38c014d 100644
--- a/packages/opencode/src/project/instance.ts
+++ b/packages/opencode/src/project/instance.ts
@@ -59,7 +59,7 @@ export const Instance = {
if (Instance.worktree === "/") return false
return Filesystem.contains(Instance.worktree, filepath)
},
- state(init: () => S, dispose?: (state: Awaited) => Promise): () => S {
+ state(init: () => S, dispose?: (state: Awaited) => Promise): (() => S) & { clear: () => void } {
return State.create(() => Instance.directory, init, dispose)
},
async dispose() {
@@ -79,7 +79,7 @@ export const Instance = {
async disposeAll() {
Log.Default.info("disposing all instances")
for (const [_key, value] of cache) {
- const awaited = await value.catch(() => {})
+ const awaited = await value.catch(() => { })
if (awaited) {
await context.provide(await value, async () => {
await Instance.dispose()
diff --git a/packages/opencode/src/project/state.ts b/packages/opencode/src/project/state.ts
index c1ac23c5d26..7aec3d3631c 100644
--- a/packages/opencode/src/project/state.ts
+++ b/packages/opencode/src/project/state.ts
@@ -10,11 +10,11 @@ export namespace State {
const recordsByKey = new Map>()
export function create(root: () => string, init: () => S, dispose?: (state: Awaited) => Promise) {
- return () => {
+ const fn = () => {
const key = root()
let entries = recordsByKey.get(key)
if (!entries) {
- entries = new Map()
+ entries = new Map()
recordsByKey.set(key, entries)
}
const exists = entries.get(init)
@@ -26,6 +26,11 @@ export namespace State {
})
return state
}
+ fn.clear = () => {
+ const key = root()
+ recordsByKey.get(key)?.delete(init)
+ }
+ return fn
}
export async function dispose(key: string) {
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index 9b01eae9e9b..e3dd92b2ff4 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -548,13 +548,13 @@ export namespace Provider {
},
experimentalOver200K: model.cost?.context_over_200k
? {
- cache: {
- read: model.cost.context_over_200k.cache_read ?? 0,
- write: model.cost.context_over_200k.cache_write ?? 0,
- },
- input: model.cost.context_over_200k.input,
- output: model.cost.context_over_200k.output,
- }
+ cache: {
+ read: model.cost.context_over_200k.cache_read ?? 0,
+ write: model.cost.context_over_200k.cache_write ?? 0,
+ },
+ input: model.cost.context_over_200k.input,
+ output: model.cost.context_over_200k.output,
+ }
: undefined,
},
limit: {
@@ -903,7 +903,10 @@ export namespace Provider {
options["includeUsage"] = true
}
- if (!options["baseURL"]) options["baseURL"] = model.api.url
+ // Only set baseURL if model.api.url is a valid URL (contains "://")
+ if (!options["baseURL"] && model.api.url && model.api.url.includes("://")) {
+ options["baseURL"] = model.api.url
+ }
if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key
if (model.headers)
options["headers"] = {
@@ -932,11 +935,40 @@ export namespace Provider {
opts.signal = combined
}
- return fetchFn(input, {
- ...opts,
- // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
- timeout: false,
- })
+ // Handle relative URLs by resolving them against baseURL
+ let finalInput = input
+ if (typeof input === "string" && !input.includes("://")) {
+ if (options["baseURL"] && options["baseURL"].includes("://")) {
+ try {
+ finalInput = new URL(input, options["baseURL"]).toString()
+ } catch (e) {
+ log.error("Failed to resolve relative URL", { input, baseURL: options["baseURL"], error: e })
+ }
+ } else {
+ // No valid baseURL available and input is a relative URL
+ throw new Error(
+ `Invalid URL "${input}" for provider "${model.providerID}". ` +
+ `The provider's API URL "${model.api.url || "(not set)"}" is not a valid URL. ` +
+ `Please fix the "api" field in your opencode.json config to be a full URL (e.g., "https://api.openai.com/v1") ` +
+ `or ensure you are logged in with the correct authentication method.`
+ )
+ }
+ }
+
+ try {
+ return await fetchFn(finalInput, {
+ ...opts,
+ // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
+ timeout: false,
+ })
+ } catch (e) {
+ log.error("fetch failed in provider wrapper", {
+ url: typeof finalInput === "string" ? finalInput : (finalInput as Request).url,
+ method: opts.method,
+ error: e,
+ })
+ throw e
+ }
}
// Special case: google-vertex-anthropic uses a subpath import
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 32d7a179555..f27e837341f 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -72,7 +72,7 @@ export namespace Server {
}
const app = new Hono()
- export const App: () => Hono = lazy(
+ export const App = lazy(
() =>
// TODO: Break server.ts into smaller route files to fix type inference
app
@@ -2833,7 +2833,7 @@ export namespace Server {
)
return response
}) as unknown as Hono,
- )
+ ) as () => Hono
export async function openapi() {
// Cast to break excessive type recursion from long route chains
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index f891612272c..52963aa4508 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -593,16 +593,20 @@ export namespace SessionPrompt {
agent,
abort,
sessionID,
- system: [...(await SystemPrompt.environment()), ...(await SystemPrompt.custom())],
+ system: [
+ ...(await SystemPrompt.environment()),
+ ...(await SystemPrompt.custom()),
+ ...(await SystemPrompt.skills()),
+ ],
messages: [
...MessageV2.toModelMessage(sessionMessages),
...(isLastStep
? [
- {
- role: "assistant" as const,
- content: MAX_STEPS,
- },
- ]
+ {
+ role: "assistant" as const,
+ content: MAX_STEPS,
+ },
+ ]
: []),
],
tools,
@@ -990,8 +994,8 @@ export namespace SessionPrompt {
agent: input.agent!,
messageID: info.id,
extra: { bypassCwdCheck: true, model },
- metadata: async () => {},
- ask: async () => {},
+ metadata: async () => { },
+ ask: async () => { },
}
const result = await t.execute(args, readCtx)
pieces.push({
@@ -1051,8 +1055,8 @@ export namespace SessionPrompt {
agent: input.agent!,
messageID: info.id,
extra: { bypassCwdCheck: true },
- metadata: async () => {},
- ask: async () => {},
+ metadata: async () => { },
+ ask: async () => { },
}
const result = await ListTool.init().then((t) => t.execute(args, listCtx))
return [
@@ -1548,15 +1552,15 @@ export namespace SessionPrompt {
const parts =
(agent.mode === "subagent" && command.subtask !== false) || command.subtask === true
? [
- {
- type: "subtask" as const,
- agent: agent.name,
- description: command.description ?? "",
- command: input.command,
- // TODO: how can we make task tool accept a more complex input?
- prompt: templateParts.find((y) => y.type === "text")?.text ?? "",
- },
- ]
+ {
+ type: "subtask" as const,
+ agent: agent.name,
+ description: command.description ?? "",
+ command: input.command,
+ // TODO: how can we make task tool accept a more complex input?
+ prompt: templateParts.find((y) => y.type === "text")?.text ?? "",
+ },
+ ]
: [...templateParts, ...(input.parts ?? [])]
const result = (await prompt({
diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts
index fff90808864..e33f4de2fb3 100644
--- a/packages/opencode/src/session/system.ts
+++ b/packages/opencode/src/session/system.ts
@@ -2,6 +2,8 @@ import { Ripgrep } from "../file/ripgrep"
import { Global } from "../global"
import { Filesystem } from "../util/filesystem"
import { Config } from "../config/config"
+import { Skill } from "../skill"
+
import { Instance } from "../project/instance"
import path from "path"
@@ -49,13 +51,12 @@ export namespace SystemPrompt {
` Today's date: ${new Date().toDateString()}`,
``,
``,
- ` ${
- project.vcs === "git" && false
- ? await Ripgrep.tree({
- cwd: Instance.directory,
- limit: 200,
- })
- : ""
+ ` ${project.vcs === "git" && false
+ ? await Ripgrep.tree({
+ cwd: Instance.directory,
+ limit: 200,
+ })
+ : ""
}`,
``,
].join("\n"),
@@ -135,4 +136,29 @@ export namespace SystemPrompt {
)
return Promise.all([...foundFiles, ...foundUrls]).then((result) => result.filter(Boolean))
}
+
+ export async function skills() {
+ const all = await Skill.allContent()
+ if (all.length === 0) return []
+
+ return [
+ [
+ `Before acting on any request, you MUST first review the following section.`,
+ `If a skill description matches your current task, you MUST adopt that skill's instructions immediately.`,
+ ``,
+ ``,
+ ...all.flatMap((skill) => [
+ ` `,
+ ` ${skill.name}`,
+ ` ${skill.description}`,
+ ` `,
+ skill.content,
+ ` `,
+ ` `,
+ ]),
+ ``,
+ ].join("\n"),
+ ]
+ }
}
+
diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts
index cbc042d1e96..5a6003bc5fd 100644
--- a/packages/opencode/src/skill/skill.ts
+++ b/packages/opencode/src/skill/skill.ts
@@ -124,4 +124,20 @@ export namespace Skill {
export async function all() {
return state().then((x) => Object.values(x))
}
+
+ export async function allContent() {
+ const skills = await all()
+ const results = await Promise.all(
+ skills.map(async (skill) => {
+ const md = await ConfigMarkdown.parse(skill.location)
+ return {
+ name: skill.name,
+ description: skill.description,
+ content: md.content.trim(),
+ }
+ }),
+ )
+ return results
+ }
}
+
diff --git a/verify_skills.ts b/verify_skills.ts
new file mode 100644
index 00000000000..9235924e7a1
--- /dev/null
+++ b/verify_skills.ts
@@ -0,0 +1,16 @@
+import { SystemPrompt } from "./packages/opencode/src/session/system"
+import { Instance } from "./packages/opencode/src/project/instance"
+
+async function test() {
+ await Instance.provide({
+ directory: process.cwd(),
+ fn: async () => {
+ const skills = await SystemPrompt.skills()
+ console.log("--- Skills Prompt ---")
+ console.log(skills[0] || "No skills found")
+ console.log("---------------------")
+ }
+ })
+}
+
+test()