summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoropencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>2026-05-03 03:23:47 +0000
committeropencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>2026-05-03 03:23:47 +0000
commit3f1ce36418835423b79cf4a50f9086a538c37f12 (patch)
tree63db2ef8f37f570adeeb1ebeeccf39ec4b4b594c
parent0e13279545f443f0186aee59e868ca9f781e875b (diff)
downloadopencode-3f1ce36418835423b79cf4a50f9086a538c37f12.tar.gz
opencode-3f1ce36418835423b79cf4a50f9086a538c37f12.zip
chore: generate
-rw-r--r--packages/opencode/src/cli/cmd/agent.ts279
-rw-r--r--packages/opencode/src/cli/cmd/mcp.ts540
-rw-r--r--packages/opencode/src/cli/cmd/providers.ts442
3 files changed, 629 insertions, 632 deletions
diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts
index e2565c627..a5bcd7873 100644
--- a/packages/opencode/src/cli/cmd/agent.ts
+++ b/packages/opencode/src/cli/cmd/agent.ts
@@ -67,169 +67,166 @@ const AgentCreateCommand = effectCmd({
if (!maybeCtx) return yield* Effect.die("InstanceRef not provided")
const ctx = maybeCtx
yield* Effect.promise(async () => {
- const cliPath = args.path
- const cliDescription = args.description
- const cliMode = args.mode as AgentMode | undefined
- const perms = args.permissions
+ const cliPath = args.path
+ const cliDescription = args.description
+ const cliMode = args.mode as AgentMode | undefined
+ const perms = args.permissions
- const isFullyNonInteractive = cliPath && cliDescription && cliMode && perms !== undefined
+ const isFullyNonInteractive = cliPath && cliDescription && cliMode && perms !== undefined
- if (!isFullyNonInteractive) {
- UI.empty()
- prompts.intro("Create agent")
- }
-
- const project = ctx.project
-
- // Determine scope/path
- let targetPath: string
- if (cliPath) {
- targetPath = path.join(cliPath, "agent")
- } else {
- let scope: "global" | "project" = "global"
- if (project.vcs === "git") {
- const scopeResult = await prompts.select({
- message: "Location",
- options: [
- {
- label: "Current project",
- value: "project" as const,
- hint: ctx.worktree,
- },
- {
- label: "Global",
- value: "global" as const,
- hint: Global.Path.config,
- },
- ],
- })
- if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
- scope = scopeResult
- }
- targetPath = path.join(
- scope === "global" ? Global.Path.config : path.join(ctx.worktree, ".opencode"),
- "agent",
- )
- }
-
- // Get description
- let description: string
- if (cliDescription) {
- description = cliDescription
- } else {
- const query = await prompts.text({
- message: "Description",
- placeholder: "What should this agent do?",
- validate: (x) => (x && x.length > 0 ? undefined : "Required"),
- })
- if (prompts.isCancel(query)) throw new UI.CancelledError()
- description = query
- }
-
- // Generate agent
- const spinner = prompts.spinner()
- spinner.start("Generating agent configuration...")
- const model = args.model ? Provider.parseModel(args.model) : undefined
- const generated = await AppRuntime.runPromise(
- Agent.Service.use((svc) => svc.generate({ description, model })),
- ).catch((error) => {
- spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
- if (isFullyNonInteractive) process.exit(1)
- throw new UI.CancelledError()
- })
- spinner.stop(`Agent ${generated.identifier} generated`)
+ if (!isFullyNonInteractive) {
+ UI.empty()
+ prompts.intro("Create agent")
+ }
- // Select permissions to allow
- let selected: string[]
- if (perms !== undefined) {
- selected = perms ? perms.split(",").map((t) => t.trim()) : AVAILABLE_PERMISSIONS
- } else {
- const result = await prompts.multiselect({
- message: "Select permissions to allow (Space to toggle)",
- options: AVAILABLE_PERMISSIONS.map((permission) => ({
- label: permission,
- value: permission,
- })),
- initialValues: AVAILABLE_PERMISSIONS,
- })
- if (prompts.isCancel(result)) throw new UI.CancelledError()
- selected = result
- }
+ const project = ctx.project
- // Get mode
- let mode: AgentMode
- if (cliMode) {
- mode = cliMode
- } else {
- const modeResult = await prompts.select({
- message: "Agent mode",
+ // Determine scope/path
+ let targetPath: string
+ if (cliPath) {
+ targetPath = path.join(cliPath, "agent")
+ } else {
+ let scope: "global" | "project" = "global"
+ if (project.vcs === "git") {
+ const scopeResult = await prompts.select({
+ message: "Location",
options: [
{
- label: "All",
- value: "all" as const,
- hint: "Can function in both primary and subagent roles",
- },
- {
- label: "Primary",
- value: "primary" as const,
- hint: "Acts as a primary/main agent",
+ label: "Current project",
+ value: "project" as const,
+ hint: ctx.worktree,
},
{
- label: "Subagent",
- value: "subagent" as const,
- hint: "Can be used as a subagent by other agents",
+ label: "Global",
+ value: "global" as const,
+ hint: Global.Path.config,
},
],
- initialValue: "all" as const,
})
- if (prompts.isCancel(modeResult)) throw new UI.CancelledError()
- mode = modeResult
+ if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
+ scope = scopeResult
}
+ targetPath = path.join(scope === "global" ? Global.Path.config : path.join(ctx.worktree, ".opencode"), "agent")
+ }
- // Build permissions config — deny anything not explicitly selected.
- const permissions: Record<string, "deny"> = {}
- for (const permission of AVAILABLE_PERMISSIONS) {
- if (!selected.includes(permission)) {
- permissions[permission] = "deny"
- }
- }
+ // Get description
+ let description: string
+ if (cliDescription) {
+ description = cliDescription
+ } else {
+ const query = await prompts.text({
+ message: "Description",
+ placeholder: "What should this agent do?",
+ validate: (x) => (x && x.length > 0 ? undefined : "Required"),
+ })
+ if (prompts.isCancel(query)) throw new UI.CancelledError()
+ description = query
+ }
- // Build frontmatter
- const frontmatter: {
- description: string
- mode: AgentMode
- permission?: Record<string, "deny">
- } = {
- description: generated.whenToUse,
- mode,
- }
- if (Object.keys(permissions).length > 0) {
- frontmatter.permission = permissions
- }
+ // Generate agent
+ const spinner = prompts.spinner()
+ spinner.start("Generating agent configuration...")
+ const model = args.model ? Provider.parseModel(args.model) : undefined
+ const generated = await AppRuntime.runPromise(
+ Agent.Service.use((svc) => svc.generate({ description, model })),
+ ).catch((error) => {
+ spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
+ if (isFullyNonInteractive) process.exit(1)
+ throw new UI.CancelledError()
+ })
+ spinner.stop(`Agent ${generated.identifier} generated`)
- // Write file
- const content = matter.stringify(generated.systemPrompt, frontmatter)
- const filePath = path.join(targetPath, `${generated.identifier}.md`)
+ // Select permissions to allow
+ let selected: string[]
+ if (perms !== undefined) {
+ selected = perms ? perms.split(",").map((t) => t.trim()) : AVAILABLE_PERMISSIONS
+ } else {
+ const result = await prompts.multiselect({
+ message: "Select permissions to allow (Space to toggle)",
+ options: AVAILABLE_PERMISSIONS.map((permission) => ({
+ label: permission,
+ value: permission,
+ })),
+ initialValues: AVAILABLE_PERMISSIONS,
+ })
+ if (prompts.isCancel(result)) throw new UI.CancelledError()
+ selected = result
+ }
- await fs.mkdir(targetPath, { recursive: true })
+ // Get mode
+ let mode: AgentMode
+ if (cliMode) {
+ mode = cliMode
+ } else {
+ const modeResult = await prompts.select({
+ message: "Agent mode",
+ options: [
+ {
+ label: "All",
+ value: "all" as const,
+ hint: "Can function in both primary and subagent roles",
+ },
+ {
+ label: "Primary",
+ value: "primary" as const,
+ hint: "Acts as a primary/main agent",
+ },
+ {
+ label: "Subagent",
+ value: "subagent" as const,
+ hint: "Can be used as a subagent by other agents",
+ },
+ ],
+ initialValue: "all" as const,
+ })
+ if (prompts.isCancel(modeResult)) throw new UI.CancelledError()
+ mode = modeResult
+ }
- if (await Filesystem.exists(filePath)) {
- if (isFullyNonInteractive) {
- console.error(`Error: Agent file already exists: ${filePath}`)
- process.exit(1)
- }
- prompts.log.error(`Agent file already exists: ${filePath}`)
- throw new UI.CancelledError()
+ // Build permissions config — deny anything not explicitly selected.
+ const permissions: Record<string, "deny"> = {}
+ for (const permission of AVAILABLE_PERMISSIONS) {
+ if (!selected.includes(permission)) {
+ permissions[permission] = "deny"
}
+ }
- await Filesystem.write(filePath, content)
+ // Build frontmatter
+ const frontmatter: {
+ description: string
+ mode: AgentMode
+ permission?: Record<string, "deny">
+ } = {
+ description: generated.whenToUse,
+ mode,
+ }
+ if (Object.keys(permissions).length > 0) {
+ frontmatter.permission = permissions
+ }
+
+ // Write file
+ const content = matter.stringify(generated.systemPrompt, frontmatter)
+ const filePath = path.join(targetPath, `${generated.identifier}.md`)
+
+ await fs.mkdir(targetPath, { recursive: true })
+ if (await Filesystem.exists(filePath)) {
if (isFullyNonInteractive) {
- console.log(filePath)
- } else {
- prompts.log.success(`Agent created: ${filePath}`)
- prompts.outro("Done")
+ console.error(`Error: Agent file already exists: ${filePath}`)
+ process.exit(1)
}
+ prompts.log.error(`Agent file already exists: ${filePath}`)
+ throw new UI.CancelledError()
+ }
+
+ await Filesystem.write(filePath, content)
+
+ if (isFullyNonInteractive) {
+ console.log(filePath)
+ } else {
+ prompts.log.success(`Agent created: ${filePath}`)
+ prompts.outro("Done")
+ }
})
}),
})
diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts
index d1e8b33be..d9927e287 100644
--- a/packages/opencode/src/cli/cmd/mcp.ts
+++ b/packages/opencode/src/cli/cmd/mcp.ts
@@ -440,158 +440,158 @@ export const McpAddCommand = effectCmd({
if (!maybeCtx) return yield* Effect.die("InstanceRef not provided")
const ctx = maybeCtx
yield* Effect.promise(async () => {
- UI.empty()
- prompts.intro("Add MCP server")
-
- const project = ctx.project
-
- // Resolve config paths eagerly for hints
- const [projectConfigPath, globalConfigPath] = await Promise.all([
- resolveConfigPath(ctx.worktree),
- resolveConfigPath(Global.Path.config, true),
- ])
-
- // Determine scope
- let configPath = globalConfigPath
- if (project.vcs === "git") {
- const scopeResult = await prompts.select({
- message: "Location",
- options: [
- {
- label: "Current project",
- value: projectConfigPath,
- hint: projectConfigPath,
- },
- {
- label: "Global",
- value: globalConfigPath,
- hint: globalConfigPath,
- },
- ],
- })
- if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
- configPath = scopeResult
- }
-
- const name = await prompts.text({
- message: "Enter MCP server name",
- validate: (x) => (x && x.length > 0 ? undefined : "Required"),
- })
- if (prompts.isCancel(name)) throw new UI.CancelledError()
-
- const type = await prompts.select({
- message: "Select MCP server type",
+ UI.empty()
+ prompts.intro("Add MCP server")
+
+ const project = ctx.project
+
+ // Resolve config paths eagerly for hints
+ const [projectConfigPath, globalConfigPath] = await Promise.all([
+ resolveConfigPath(ctx.worktree),
+ resolveConfigPath(Global.Path.config, true),
+ ])
+
+ // Determine scope
+ let configPath = globalConfigPath
+ if (project.vcs === "git") {
+ const scopeResult = await prompts.select({
+ message: "Location",
options: [
{
- label: "Local",
- value: "local",
- hint: "Run a local command",
+ label: "Current project",
+ value: projectConfigPath,
+ hint: projectConfigPath,
},
{
- label: "Remote",
- value: "remote",
- hint: "Connect to a remote URL",
+ label: "Global",
+ value: globalConfigPath,
+ hint: globalConfigPath,
},
],
})
- if (prompts.isCancel(type)) throw new UI.CancelledError()
+ if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
+ configPath = scopeResult
+ }
- if (type === "local") {
- const command = await prompts.text({
- message: "Enter command to run",
- placeholder: "e.g., opencode x @modelcontextprotocol/server-filesystem",
- validate: (x) => (x && x.length > 0 ? undefined : "Required"),
- })
- if (prompts.isCancel(command)) throw new UI.CancelledError()
+ const name = await prompts.text({
+ message: "Enter MCP server name",
+ validate: (x) => (x && x.length > 0 ? undefined : "Required"),
+ })
+ if (prompts.isCancel(name)) throw new UI.CancelledError()
+
+ const type = await prompts.select({
+ message: "Select MCP server type",
+ options: [
+ {
+ label: "Local",
+ value: "local",
+ hint: "Run a local command",
+ },
+ {
+ label: "Remote",
+ value: "remote",
+ hint: "Connect to a remote URL",
+ },
+ ],
+ })
+ if (prompts.isCancel(type)) throw new UI.CancelledError()
- const mcpConfig: ConfigMCP.Info = {
- type: "local",
- command: command.split(" "),
- }
+ if (type === "local") {
+ const command = await prompts.text({
+ message: "Enter command to run",
+ placeholder: "e.g., opencode x @modelcontextprotocol/server-filesystem",
+ validate: (x) => (x && x.length > 0 ? undefined : "Required"),
+ })
+ if (prompts.isCancel(command)) throw new UI.CancelledError()
- await addMcpToConfig(name, mcpConfig, configPath)
- prompts.log.success(`MCP server "${name}" added to ${configPath}`)
- prompts.outro("MCP server added successfully")
- return
+ const mcpConfig: ConfigMCP.Info = {
+ type: "local",
+ command: command.split(" "),
}
- if (type === "remote") {
- const url = await prompts.text({
- message: "Enter MCP server URL",
- placeholder: "e.g., https://example.com/mcp",
- validate: (x) => {
- if (!x) return "Required"
- if (x.length === 0) return "Required"
- const isValid = URL.canParse(x)
- return isValid ? undefined : "Invalid URL"
- },
- })
- if (prompts.isCancel(url)) throw new UI.CancelledError()
+ await addMcpToConfig(name, mcpConfig, configPath)
+ prompts.log.success(`MCP server "${name}" added to ${configPath}`)
+ prompts.outro("MCP server added successfully")
+ return
+ }
- const useOAuth = await prompts.confirm({
- message: "Does this server require OAuth authentication?",
+ if (type === "remote") {
+ const url = await prompts.text({
+ message: "Enter MCP server URL",
+ placeholder: "e.g., https://example.com/mcp",
+ validate: (x) => {
+ if (!x) return "Required"
+ if (x.length === 0) return "Required"
+ const isValid = URL.canParse(x)
+ return isValid ? undefined : "Invalid URL"
+ },
+ })
+ if (prompts.isCancel(url)) throw new UI.CancelledError()
+
+ const useOAuth = await prompts.confirm({
+ message: "Does this server require OAuth authentication?",
+ initialValue: false,
+ })
+ if (prompts.isCancel(useOAuth)) throw new UI.CancelledError()
+
+ let mcpConfig: ConfigMCP.Info
+
+ if (useOAuth) {
+ const hasClientId = await prompts.confirm({
+ message: "Do you have a pre-registered client ID?",
initialValue: false,
})
- if (prompts.isCancel(useOAuth)) throw new UI.CancelledError()
+ if (prompts.isCancel(hasClientId)) throw new UI.CancelledError()
- let mcpConfig: ConfigMCP.Info
+ if (hasClientId) {
+ const clientId = await prompts.text({
+ message: "Enter client ID",
+ validate: (x) => (x && x.length > 0 ? undefined : "Required"),
+ })
+ if (prompts.isCancel(clientId)) throw new UI.CancelledError()
- if (useOAuth) {
- const hasClientId = await prompts.confirm({
- message: "Do you have a pre-registered client ID?",
+ const hasSecret = await prompts.confirm({
+ message: "Do you have a client secret?",
initialValue: false,
})
- if (prompts.isCancel(hasClientId)) throw new UI.CancelledError()
+ if (prompts.isCancel(hasSecret)) throw new UI.CancelledError()
- if (hasClientId) {
- const clientId = await prompts.text({
- message: "Enter client ID",
- validate: (x) => (x && x.length > 0 ? undefined : "Required"),
+ let clientSecret: string | undefined
+ if (hasSecret) {
+ const secret = await prompts.password({
+ message: "Enter client secret",
})
- if (prompts.isCancel(clientId)) throw new UI.CancelledError()
-
- const hasSecret = await prompts.confirm({
- message: "Do you have a client secret?",
- initialValue: false,
- })
- if (prompts.isCancel(hasSecret)) throw new UI.CancelledError()
-
- let clientSecret: string | undefined
- if (hasSecret) {
- const secret = await prompts.password({
- message: "Enter client secret",
- })
- if (prompts.isCancel(secret)) throw new UI.CancelledError()
- clientSecret = secret
- }
+ if (prompts.isCancel(secret)) throw new UI.CancelledError()
+ clientSecret = secret
+ }
- mcpConfig = {
- type: "remote",
- url,
- oauth: {
- clientId,
- ...(clientSecret && { clientSecret }),
- },
- }
- } else {
- mcpConfig = {
- type: "remote",
- url,
- oauth: {},
- }
+ mcpConfig = {
+ type: "remote",
+ url,
+ oauth: {
+ clientId,
+ ...(clientSecret && { clientSecret }),
+ },
}
} else {
mcpConfig = {
type: "remote",
url,
+ oauth: {},
}
}
-
- await addMcpToConfig(name, mcpConfig, configPath)
- prompts.log.success(`MCP server "${name}" added to ${configPath}`)
+ } else {
+ mcpConfig = {
+ type: "remote",
+ url,
+ }
}
- prompts.outro("MCP server added successfully")
+ await addMcpToConfig(name, mcpConfig, configPath)
+ prompts.log.success(`MCP server "${name}" added to ${configPath}`)
+ }
+
+ prompts.outro("MCP server added successfully")
})
}),
})
@@ -607,177 +607,177 @@ export const McpDebugCommand = effectCmd({
}),
handler: Effect.fn("Cli.mcp.debug")(function* (args) {
yield* Effect.promise(async () => {
- UI.empty()
- prompts.intro("MCP OAuth Debug")
-
- const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
- const mcpServers = config.mcp ?? {}
- const serverName = args.name
-
- const serverConfig = mcpServers[serverName]
- if (!serverConfig) {
- prompts.log.error(`MCP server not found: ${serverName}`)
- prompts.outro("Done")
- return
- }
+ UI.empty()
+ prompts.intro("MCP OAuth Debug")
- if (!isMcpRemote(serverConfig)) {
- prompts.log.error(`MCP server ${serverName} is not a remote server`)
- prompts.outro("Done")
- return
- }
+ const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
+ const mcpServers = config.mcp ?? {}
+ const serverName = args.name
- if (serverConfig.oauth === false) {
- prompts.log.warn(`MCP server ${serverName} has OAuth explicitly disabled`)
- prompts.outro("Done")
- return
- }
+ const serverConfig = mcpServers[serverName]
+ if (!serverConfig) {
+ prompts.log.error(`MCP server not found: ${serverName}`)
+ prompts.outro("Done")
+ return
+ }
- prompts.log.info(`Server: ${serverName}`)
- prompts.log.info(`URL: ${serverConfig.url}`)
+ if (!isMcpRemote(serverConfig)) {
+ prompts.log.error(`MCP server ${serverName} is not a remote server`)
+ prompts.outro("Done")
+ return
+ }
- // Check stored auth status
- const { authStatus, entry } = await AppRuntime.runPromise(
- Effect.gen(function* () {
- const mcp = yield* MCP.Service
- const auth = yield* McpAuth.Service
- return {
- authStatus: yield* mcp.getAuthStatus(serverName),
- entry: yield* auth.get(serverName),
- }
- }),
- )
- prompts.log.info(`Auth status: ${getAuthStatusIcon(authStatus)} ${getAuthStatusText(authStatus)}`)
-
- if (entry?.tokens) {
- prompts.log.info(` Access token: ${entry.tokens.accessToken.substring(0, 20)}...`)
- if (entry.tokens.expiresAt) {
- const expiresDate = new Date(entry.tokens.expiresAt * 1000)
- const isExpired = entry.tokens.expiresAt < Date.now() / 1000
- prompts.log.info(` Expires: ${expiresDate.toISOString()} ${isExpired ? "(EXPIRED)" : ""}`)
- }
- if (entry.tokens.refreshToken) {
- prompts.log.info(` Refresh token: present`)
+ if (serverConfig.oauth === false) {
+ prompts.log.warn(`MCP server ${serverName} has OAuth explicitly disabled`)
+ prompts.outro("Done")
+ return
+ }
+
+ prompts.log.info(`Server: ${serverName}`)
+ prompts.log.info(`URL: ${serverConfig.url}`)
+
+ // Check stored auth status
+ const { authStatus, entry } = await AppRuntime.runPromise(
+ Effect.gen(function* () {
+ const mcp = yield* MCP.Service
+ const auth = yield* McpAuth.Service
+ return {
+ authStatus: yield* mcp.getAuthStatus(serverName),
+ entry: yield* auth.get(serverName),
}
+ }),
+ )
+ prompts.log.info(`Auth status: ${getAuthStatusIcon(authStatus)} ${getAuthStatusText(authStatus)}`)
+
+ if (entry?.tokens) {
+ prompts.log.info(` Access token: ${entry.tokens.accessToken.substring(0, 20)}...`)
+ if (entry.tokens.expiresAt) {
+ const expiresDate = new Date(entry.tokens.expiresAt * 1000)
+ const isExpired = entry.tokens.expiresAt < Date.now() / 1000
+ prompts.log.info(` Expires: ${expiresDate.toISOString()} ${isExpired ? "(EXPIRED)" : ""}`)
}
- if (entry?.clientInfo) {
- prompts.log.info(` Client ID: ${entry.clientInfo.clientId}`)
- if (entry.clientInfo.clientSecretExpiresAt) {
- const expiresDate = new Date(entry.clientInfo.clientSecretExpiresAt * 1000)
- prompts.log.info(` Client secret expires: ${expiresDate.toISOString()}`)
- }
+ if (entry.tokens.refreshToken) {
+ prompts.log.info(` Refresh token: present`)
}
+ }
+ if (entry?.clientInfo) {
+ prompts.log.info(` Client ID: ${entry.clientInfo.clientId}`)
+ if (entry.clientInfo.clientSecretExpiresAt) {
+ const expiresDate = new Date(entry.clientInfo.clientSecretExpiresAt * 1000)
+ prompts.log.info(` Client secret expires: ${expiresDate.toISOString()}`)
+ }
+ }
- const spinner = prompts.spinner()
- spinner.start("Testing connection...")
-
- // Test basic HTTP connectivity first
- try {
- const response = await fetch(serverConfig.url, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Accept: "application/json, text/event-stream",
+ const spinner = prompts.spinner()
+ spinner.start("Testing connection...")
+
+ // Test basic HTTP connectivity first
+ try {
+ const response = await fetch(serverConfig.url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json, text/event-stream",
+ },
+ body: JSON.stringify({
+ jsonrpc: "2.0",
+ method: "initialize",
+ params: {
+ protocolVersion: "2024-11-05",
+ capabilities: {},
+ clientInfo: { name: "opencode-debug", version: InstallationVersion },
},
- body: JSON.stringify({
- jsonrpc: "2.0",
- method: "initialize",
- params: {
- protocolVersion: "2024-11-05",
- capabilities: {},
- clientInfo: { name: "opencode-debug", version: InstallationVersion },
- },
- id: 1,
- }),
- })
+ id: 1,
+ }),
+ })
- spinner.stop(`HTTP response: ${response.status} ${response.statusText}`)
+ spinner.stop(`HTTP response: ${response.status} ${response.statusText}`)
- // Check for WWW-Authenticate header
- const wwwAuth = response.headers.get("www-authenticate")
- if (wwwAuth) {
- prompts.log.info(`WWW-Authenticate: ${wwwAuth}`)
- }
+ // Check for WWW-Authenticate header
+ const wwwAuth = response.headers.get("www-authenticate")
+ if (wwwAuth) {
+ prompts.log.info(`WWW-Authenticate: ${wwwAuth}`)
+ }
- if (response.status === 401) {
- prompts.log.warn("Server returned 401 Unauthorized")
-
- // Try to discover OAuth metadata
- const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined
- const auth = await AppRuntime.runPromise(
- Effect.gen(function* () {
- return yield* McpAuth.Service
- }),
- )
- const authProvider = new McpOAuthProvider(
- serverName,
- serverConfig.url,
- {
- clientId: oauthConfig?.clientId,
- clientSecret: oauthConfig?.clientSecret,
- scope: oauthConfig?.scope,
- redirectUri: oauthConfig?.redirectUri,
- },
- {
- onRedirect: async () => {},
- },
- auth,
- )
+ if (response.status === 401) {
+ prompts.log.warn("Server returned 401 Unauthorized")
- prompts.log.info("Testing OAuth flow (without completing authorization)...")
+ // Try to discover OAuth metadata
+ const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined
+ const auth = await AppRuntime.runPromise(
+ Effect.gen(function* () {
+ return yield* McpAuth.Service
+ }),
+ )
+ const authProvider = new McpOAuthProvider(
+ serverName,
+ serverConfig.url,
+ {
+ clientId: oauthConfig?.clientId,
+ clientSecret: oauthConfig?.clientSecret,
+ scope: oauthConfig?.scope,
+ redirectUri: oauthConfig?.redirectUri,
+ },
+ {
+ onRedirect: async () => {},
+ },
+ auth,
+ )
- // Try creating transport with auth provider to trigger discovery
- const transport = new StreamableHTTPClientTransport(new URL(serverConfig.url), {
- authProvider,
- })
+ prompts.log.info("Testing OAuth flow (without completing authorization)...")
- try {
- const client = new Client({
- name: "opencode-debug",
- version: InstallationVersion,
- })
- await client.connect(transport)
- prompts.log.success("Connection successful (already authenticated)")
- await client.close()
- } catch (error) {
- if (error instanceof UnauthorizedError) {
- prompts.log.info(`OAuth flow triggered: ${error.message}`)
-
- // Check if dynamic registration would be attempted
- const clientInfo = await authProvider.clientInformation()
- if (clientInfo) {
- prompts.log.info(`Client ID available: ${clientInfo.client_id}`)
- } else {
- prompts.log.info("No client ID - dynamic registration will be attempted")
- }
+ // Try creating transport with auth provider to trigger discovery
+ const transport = new StreamableHTTPClientTransport(new URL(serverConfig.url), {
+ authProvider,
+ })
+
+ try {
+ const client = new Client({
+ name: "opencode-debug",
+ version: InstallationVersion,
+ })
+ await client.connect(transport)
+ prompts.log.success("Connection successful (already authenticated)")
+ await client.close()
+ } catch (error) {
+ if (error instanceof UnauthorizedError) {
+ prompts.log.info(`OAuth flow triggered: ${error.message}`)
+
+ // Check if dynamic registration would be attempted
+ const clientInfo = await authProvider.clientInformation()
+ if (clientInfo) {
+ prompts.log.info(`Client ID available: ${clientInfo.client_id}`)
} else {
- prompts.log.error(`Connection error: ${error instanceof Error ? error.message : String(error)}`)
- }
- }
- } else if (response.status >= 200 && response.status < 300) {
- prompts.log.success("Server responded successfully (no auth required or already authenticated)")
- const body = await response.text()
- try {
- const json = JSON.parse(body)
- if (json.result?.serverInfo) {
- prompts.log.info(`Server info: ${JSON.stringify(json.result.serverInfo)}`)
+ prompts.log.info("No client ID - dynamic registration will be attempted")
}
- } catch {
- // Not JSON, ignore
+ } else {
+ prompts.log.error(`Connection error: ${error instanceof Error ? error.message : String(error)}`)
}
- } else {
- prompts.log.warn(`Unexpected status: ${response.status}`)
- const body = await response.text().catch(() => "")
- if (body) {
- prompts.log.info(`Response body: ${body.substring(0, 500)}`)
+ }
+ } else if (response.status >= 200 && response.status < 300) {
+ prompts.log.success("Server responded successfully (no auth required or already authenticated)")
+ const body = await response.text()
+ try {
+ const json = JSON.parse(body)
+ if (json.result?.serverInfo) {
+ prompts.log.info(`Server info: ${JSON.stringify(json.result.serverInfo)}`)
}
+ } catch {
+ // Not JSON, ignore
+ }
+ } else {
+ prompts.log.warn(`Unexpected status: ${response.status}`)
+ const body = await response.text().catch(() => "")
+ if (body) {
+ prompts.log.info(`Response body: ${body.substring(0, 500)}`)
}
- } catch (error) {
- spinner.stop("Connection failed", 1)
- prompts.log.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
}
+ } catch (error) {
+ spinner.stop("Connection failed", 1)
+ prompts.log.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
+ }
- prompts.outro("Debug complete")
+ prompts.outro("Debug complete")
})
}),
})
diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts
index 93541114b..71e03c7e7 100644
--- a/packages/opencode/src/cli/cmd/providers.ts
+++ b/packages/opencode/src/cli/cmd/providers.ts
@@ -240,49 +240,49 @@ export const ProvidersListCommand = effectCmd({
instance: false,
handler: Effect.fn("Cli.providers.list")(function* (_args) {
yield* Effect.promise(async () => {
- UI.empty()
- const authPath = path.join(Global.Path.data, "auth.json")
- const homedir = os.homedir()
- const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
- prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
- const results = await AppRuntime.runPromise(
- Effect.gen(function* () {
- const auth = yield* Auth.Service
- return Object.entries(yield* auth.all())
- }),
- )
- const database = await getModels()
+ UI.empty()
+ const authPath = path.join(Global.Path.data, "auth.json")
+ const homedir = os.homedir()
+ const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
+ prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
+ const results = await AppRuntime.runPromise(
+ Effect.gen(function* () {
+ const auth = yield* Auth.Service
+ return Object.entries(yield* auth.all())
+ }),
+ )
+ const database = await getModels()
- for (const [providerID, result] of results) {
- const name = database[providerID]?.name || providerID
- prompts.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}`)
- }
+ for (const [providerID, result] of results) {
+ const name = database[providerID]?.name || providerID
+ prompts.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}`)
+ }
- prompts.outro(`${results.length} credentials`)
+ prompts.outro(`${results.length} credentials`)
- const activeEnvVars: Array<{ provider: string; envVar: string }> = []
+ const activeEnvVars: Array<{ provider: string; envVar: string }> = []
- for (const [providerID, provider] of Object.entries(database)) {
- for (const envVar of provider.env) {
- if (process.env[envVar]) {
- activeEnvVars.push({
- provider: provider.name || providerID,
- envVar,
- })
+ for (const [providerID, provider] of Object.entries(database)) {
+ for (const envVar of provider.env) {
+ if (process.env[envVar]) {
+ activeEnvVars.push({
+ provider: provider.name || providerID,
+ envVar,
+ })
+ }
}
}
- }
- if (activeEnvVars.length > 0) {
- UI.empty()
- prompts.intro("Environment")
+ if (activeEnvVars.length > 0) {
+ UI.empty()
+ prompts.intro("Environment")
- for (const { provider, envVar } of activeEnvVars) {
- prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
- }
+ for (const { provider, envVar } of activeEnvVars) {
+ prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
+ }
- prompts.outro(`${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s"))
- }
+ prompts.outro(`${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s"))
+ }
})
}),
})
@@ -308,187 +308,187 @@ export const ProvidersLoginCommand = effectCmd({
}),
handler: Effect.fn("Cli.providers.login")(function* (args) {
yield* Effect.promise(async () => {
- UI.empty()
- prompts.intro("Add credential")
- if (args.url) {
- const url = args.url.replace(/\/+$/, "")
- const wellknown = (await fetch(`${url}/.well-known/opencode`).then((x) => x.json())) as {
- auth: { command: string[]; env: string }
- }
- prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
- const proc = Process.spawn(wellknown.auth.command, {
- stdout: "pipe",
- })
- if (!proc.stdout) {
- prompts.log.error("Failed")
- prompts.outro("Done")
- return
- }
- const [exit, token] = await Promise.all([proc.exited, text(proc.stdout)])
- if (exit !== 0) {
- prompts.log.error("Failed")
- prompts.outro("Done")
- return
- }
- await put(url, {
- type: "wellknown",
- key: wellknown.auth.env,
- token: token.trim(),
- })
- prompts.log.success("Logged into " + url)
+ UI.empty()
+ prompts.intro("Add credential")
+ if (args.url) {
+ const url = args.url.replace(/\/+$/, "")
+ const wellknown = (await fetch(`${url}/.well-known/opencode`).then((x) => x.json())) as {
+ auth: { command: string[]; env: string }
+ }
+ prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
+ const proc = Process.spawn(wellknown.auth.command, {
+ stdout: "pipe",
+ })
+ if (!proc.stdout) {
+ prompts.log.error("Failed")
+ prompts.outro("Done")
+ return
+ }
+ const [exit, token] = await Promise.all([proc.exited, text(proc.stdout)])
+ if (exit !== 0) {
+ prompts.log.error("Failed")
prompts.outro("Done")
return
}
- await refreshModels().catch(() => {})
+ await put(url, {
+ type: "wellknown",
+ key: wellknown.auth.env,
+ token: token.trim(),
+ })
+ prompts.log.success("Logged into " + url)
+ prompts.outro("Done")
+ return
+ }
+ await refreshModels().catch(() => {})
- const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
+ const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
- const disabled = new Set(config.disabled_providers ?? [])
- const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
+ const disabled = new Set(config.disabled_providers ?? [])
+ const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
- const providers = await getModels().then((x) => {
- const filtered: Record<string, (typeof x)[string]> = {}
- for (const [key, value] of Object.entries(x)) {
- if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
- filtered[key] = value
- }
+ const providers = await getModels().then((x) => {
+ const filtered: Record<string, (typeof x)[string]> = {}
+ for (const [key, value] of Object.entries(x)) {
+ if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
+ filtered[key] = value
}
- return filtered
- })
- const hooks = await AppRuntime.runPromise(
- Effect.gen(function* () {
- const plugin = yield* Plugin.Service
- return yield* plugin.list()
- }),
- )
-
- const priority: Record<string, number> = {
- opencode: 0,
- openai: 1,
- "github-copilot": 2,
- google: 3,
- anthropic: 4,
- openrouter: 5,
- vercel: 6,
}
- const pluginProviders = resolvePluginProviders({
- hooks,
- existingProviders: providers,
- disabled,
- enabled,
- providerNames: Object.fromEntries(Object.entries(config.provider ?? {}).map(([id, p]) => [id, p.name])),
- })
- const 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",
- openai: "ChatGPT Plus/Pro or API key",
- }[x.id],
- })),
+ return filtered
+ })
+ const hooks = await AppRuntime.runPromise(
+ Effect.gen(function* () {
+ const plugin = yield* Plugin.Service
+ return yield* plugin.list()
+ }),
+ )
+
+ const priority: Record<string, number> = {
+ opencode: 0,
+ openai: 1,
+ "github-copilot": 2,
+ google: 3,
+ anthropic: 4,
+ openrouter: 5,
+ vercel: 6,
+ }
+ const pluginProviders = resolvePluginProviders({
+ hooks,
+ existingProviders: providers,
+ disabled,
+ enabled,
+ providerNames: Object.fromEntries(Object.entries(config.provider ?? {}).map(([id, p]) => [id, p.name])),
+ })
+ const options = [
+ ...pipe(
+ providers,
+ values(),
+ sortBy(
+ (x) => priority[x.id] ?? 99,
+ (x) => x.name ?? x.id,
),
- ...pluginProviders.map((x) => ({
+ map((x) => ({
label: x.name,
value: x.id,
- hint: "plugin",
+ hint: {
+ opencode: "recommended",
+ openai: "ChatGPT Plus/Pro or API key",
+ }[x.id],
})),
- ]
-
- let provider: string
- if (args.provider) {
- const input = args.provider
- const byID = options.find((x) => x.value === input)
- const byName = options.find((x) => x.label.toLowerCase() === input.toLowerCase())
- const match = byID ?? byName
- if (!match) {
- prompts.log.error(`Unknown provider "${input}"`)
- process.exit(1)
- }
- provider = match.value
- } else {
- const selected = await prompts.autocomplete({
- message: "Select provider",
- maxItems: 8,
- options: [
- ...options,
- {
- value: "other",
- label: "Other",
- },
- ],
- })
- if (prompts.isCancel(selected)) throw new UI.CancelledError()
- provider = selected as string
- }
-
- const plugin = hooks.findLast((x) => x.auth?.provider === provider)
- if (plugin && plugin.auth) {
- const handled = await handlePluginAuth({ auth: plugin.auth }, provider, args.method)
- if (handled) return
+ ),
+ ...pluginProviders.map((x) => ({
+ label: x.name,
+ value: x.id,
+ hint: "plugin",
+ })),
+ ]
+
+ let provider: string
+ if (args.provider) {
+ const input = args.provider
+ const byID = options.find((x) => x.value === input)
+ const byName = options.find((x) => x.label.toLowerCase() === input.toLowerCase())
+ const match = byID ?? byName
+ if (!match) {
+ prompts.log.error(`Unknown provider "${input}"`)
+ process.exit(1)
}
+ provider = match.value
+ } else {
+ const selected = await prompts.autocomplete({
+ message: "Select provider",
+ maxItems: 8,
+ options: [
+ ...options,
+ {
+ value: "other",
+ label: "Other",
+ },
+ ],
+ })
+ if (prompts.isCancel(selected)) throw new UI.CancelledError()
+ provider = selected as string
+ }
- if (provider === "other") {
- const custom = 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(custom)) throw new UI.CancelledError()
- provider = custom.replace(/^@ai-sdk\//, "")
+ const plugin = hooks.findLast((x) => x.auth?.provider === provider)
+ if (plugin && plugin.auth) {
+ const handled = await handlePluginAuth({ auth: plugin.auth }, provider, args.method)
+ if (handled) return
+ }
- const customPlugin = hooks.findLast((x) => x.auth?.provider === provider)
- if (customPlugin && customPlugin.auth) {
- const handled = await handlePluginAuth({ auth: customPlugin.auth }, provider, args.method)
- if (handled) return
- }
+ if (provider === "other") {
+ const custom = 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(custom)) throw new UI.CancelledError()
+ provider = custom.replace(/^@ai-sdk\//, "")
- prompts.log.warn(
- `This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
- )
+ const customPlugin = hooks.findLast((x) => x.auth?.provider === provider)
+ if (customPlugin && customPlugin.auth) {
+ const handled = await handlePluginAuth({ auth: customPlugin.auth }, provider, args.method)
+ if (handled) return
}
- 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, EKS IRSA)\n\n" +
- "Configure via opencode.json options (profile, region, endpoint) or\n" +
- "AWS environment variables (AWS_PROFILE, AWS_REGION, AWS_ACCESS_KEY_ID, AWS_WEB_IDENTITY_TOKEN_FILE).",
- )
- }
+ prompts.log.warn(
+ `This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
+ )
+ }
- if (provider === "opencode") {
- prompts.log.info("Create an api key at https://opencode.ai/auth")
- }
+ 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, EKS IRSA)\n\n" +
+ "Configure via opencode.json options (profile, region, endpoint) or\n" +
+ "AWS environment variables (AWS_PROFILE, AWS_REGION, AWS_ACCESS_KEY_ID, AWS_WEB_IDENTITY_TOKEN_FILE).",
+ )
+ }
- if (provider === "vercel") {
- prompts.log.info("You can create an api key at https://vercel.link/ai-gateway-token")
- }
+ if (provider === "opencode") {
+ prompts.log.info("Create an api key at https://opencode.ai/auth")
+ }
- if (["cloudflare", "cloudflare-ai-gateway"].includes(provider)) {
- prompts.log.info(
- "Cloudflare AI Gateway can be configured with CLOUDFLARE_GATEWAY_ID, CLOUDFLARE_ACCOUNT_ID, and CLOUDFLARE_API_TOKEN environment variables. Read more: https://opencode.ai/docs/providers/#cloudflare-ai-gateway",
- )
- }
+ if (provider === "vercel") {
+ prompts.log.info("You can create an api key at https://vercel.link/ai-gateway-token")
+ }
- const key = await prompts.password({
- message: "Enter your API key",
- validate: (x) => (x && x.length > 0 ? undefined : "Required"),
- })
- if (prompts.isCancel(key)) throw new UI.CancelledError()
- await put(provider, {
- type: "api",
- key,
- })
+ if (["cloudflare", "cloudflare-ai-gateway"].includes(provider)) {
+ prompts.log.info(
+ "Cloudflare AI Gateway can be configured with CLOUDFLARE_GATEWAY_ID, CLOUDFLARE_ACCOUNT_ID, and CLOUDFLARE_API_TOKEN environment variables. Read more: https://opencode.ai/docs/providers/#cloudflare-ai-gateway",
+ )
+ }
- prompts.outro("Done")
+ const key = await prompts.password({
+ message: "Enter your API key",
+ validate: (x) => (x && x.length > 0 ? undefined : "Required"),
+ })
+ if (prompts.isCancel(key)) throw new UI.CancelledError()
+ await put(provider, {
+ type: "api",
+ key,
+ })
+
+ prompts.outro("Done")
})
}),
})
@@ -500,35 +500,35 @@ export const ProvidersLogoutCommand = effectCmd({
instance: false,
handler: Effect.fn("Cli.providers.logout")(function* (_args) {
yield* Effect.promise(async () => {
- UI.empty()
- const credentials: Array<[string, Auth.Info]> = await AppRuntime.runPromise(
- Effect.gen(function* () {
- const auth = yield* Auth.Service
- return Object.entries(yield* auth.all())
- }),
- )
- prompts.intro("Remove credential")
- if (credentials.length === 0) {
- prompts.log.error("No credentials found")
- return
- }
- const database = await getModels()
- const selected = await prompts.select({
- message: "Select provider",
- options: credentials.map(([key, value]) => ({
- label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
- value: key,
- })),
- })
- if (prompts.isCancel(selected)) throw new UI.CancelledError()
- const providerID = selected as string
- await AppRuntime.runPromise(
- Effect.gen(function* () {
- const auth = yield* Auth.Service
- yield* auth.remove(providerID)
- }),
- )
- prompts.outro("Logout successful")
+ UI.empty()
+ const credentials: Array<[string, Auth.Info]> = await AppRuntime.runPromise(
+ Effect.gen(function* () {
+ const auth = yield* Auth.Service
+ return Object.entries(yield* auth.all())
+ }),
+ )
+ prompts.intro("Remove credential")
+ if (credentials.length === 0) {
+ prompts.log.error("No credentials found")
+ return
+ }
+ const database = await getModels()
+ const selected = await prompts.select({
+ message: "Select provider",
+ options: credentials.map(([key, value]) => ({
+ label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
+ value: key,
+ })),
+ })
+ if (prompts.isCancel(selected)) throw new UI.CancelledError()
+ const providerID = selected as string
+ await AppRuntime.runPromise(
+ Effect.gen(function* () {
+ const auth = yield* Auth.Service
+ yield* auth.remove(providerID)
+ }),
+ )
+ prompts.outro("Logout successful")
})
}),
})