فهرست منبع

chore: generate

opencode-agent[bot] 4 ماه پیش
والد
کامیت
3f1ce36418
3فایلهای تغییر یافته به همراه629 افزوده شده و 632 حذف شده
  1. 138 141
      packages/opencode/src/cli/cmd/agent.ts
  2. 270 270
      packages/opencode/src/cli/cmd/mcp.ts
  3. 221 221
      packages/opencode/src/cli/cmd/providers.ts

+ 138 - 141
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")
+      }
     })
   }),
 })

+ 270 - 270
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")
     })
   }),
 })

+ 221 - 221
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")
     })
   }),
 })